Compare commits
38 Commits
ce66eeba6d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85f3387fd2 | ||
|
|
ff5ba25505 | ||
|
|
eab974d14c | ||
|
|
b509b1a2b4 | ||
|
|
83d113d669 | ||
|
|
0c1908e7d1 | ||
|
|
5333531be4 | ||
|
|
fb68f2023c | ||
|
|
79e91f59ee | ||
|
|
c23dc22d76 | ||
|
|
b79569c5e7 | ||
|
|
6be292e085 | ||
|
|
885276aad1 | ||
|
|
1e6a076904 | ||
|
|
a33c43a104 | ||
|
|
78d2ba57b9 | ||
|
|
1c0750604b | ||
|
|
bf304f08fc | ||
|
|
30b30b065b | ||
|
|
b2d5cdfb57 | ||
|
|
b40e5b671a | ||
|
|
61c67acca5 | ||
|
|
ffe6ea8e65 | ||
|
|
36d5cf56de | ||
|
|
23339e3349 | ||
|
|
caa6413bc6 | ||
|
|
ea695010f3 | ||
|
|
e4c79d3f26 | ||
|
|
f4b45b7e17 | ||
|
|
c28fedd6e7 | ||
|
|
14138e7762 | ||
|
|
8735962f52 | ||
|
|
7fb5eda108 | ||
|
|
4cdc4db397 | ||
|
|
c732b5af05 | ||
|
|
29a69617e4 | ||
|
|
9e37ea93eb | ||
|
|
7472dbad11 |
17
.claude/commands/openspec/apply.md
Normal file
17
.claude/commands/openspec/apply.md
Normal file
@@ -0,0 +1,17 @@
|
||||
Implement the OpenSpec change: $ARGUMENTS
|
||||
|
||||
**Guardrails**
|
||||
- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
|
||||
- Keep changes tightly scoped to the requested outcome.
|
||||
- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications.
|
||||
|
||||
**Steps**
|
||||
Track these steps as TODOs and complete them one by one.
|
||||
1. Read `changes/<id>/proposal.md`, `design.md` (if present), and `tasks.md` to confirm scope and acceptance criteria.
|
||||
2. Work through tasks sequentially, keeping edits minimal and focused on the requested change.
|
||||
3. Confirm completion before updating statuses—make sure every item in `tasks.md` is finished.
|
||||
4. Update the checklist after all work is done so each task is marked `- [x]` and reflects reality.
|
||||
5. Reference `openspec list` or `openspec show <item>` when additional context is required.
|
||||
|
||||
**Reference**
|
||||
- Use `openspec show <id> --json --deltas-only` if you need additional context from the proposal while implementing.
|
||||
21
.claude/commands/openspec/archive.md
Normal file
21
.claude/commands/openspec/archive.md
Normal file
@@ -0,0 +1,21 @@
|
||||
Archive the completed OpenSpec change: $ARGUMENTS
|
||||
|
||||
**Guardrails**
|
||||
- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
|
||||
- Keep changes tightly scoped to the requested outcome.
|
||||
- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications.
|
||||
|
||||
**Steps**
|
||||
1. Determine the change ID to archive:
|
||||
- If this prompt already includes a specific change ID (for example inside a `<ChangeId>` block populated by slash-command arguments), use that value after trimming whitespace.
|
||||
- If the conversation references a change loosely (for example by title or summary), run `openspec list` to surface likely IDs, share the relevant candidates, and confirm which one the user intends.
|
||||
- Otherwise, review the conversation, run `openspec list`, and ask the user which change to archive; wait for a confirmed change ID before proceeding.
|
||||
- If you still cannot identify a single change ID, stop and tell the user you cannot archive anything yet.
|
||||
2. Validate the change ID by running `openspec list` (or `openspec show <id>`) and stop if the change is missing, already archived, or otherwise not ready to archive.
|
||||
3. Run `openspec archive <id> --yes` so the CLI moves the change and applies spec updates without prompts (use `--skip-specs` only for tooling-only work).
|
||||
4. Review the command output to confirm the target specs were updated and the change landed in `changes/archive/`.
|
||||
5. Validate with `openspec validate --strict` and inspect with `openspec show <id>` if anything looks off.
|
||||
|
||||
**Reference**
|
||||
- Use `openspec list` to confirm change IDs before archiving.
|
||||
- Inspect refreshed specs with `openspec list --specs` and address any validation issues before handing off.
|
||||
22
.claude/commands/openspec/proposal.md
Normal file
22
.claude/commands/openspec/proposal.md
Normal file
@@ -0,0 +1,22 @@
|
||||
Create an OpenSpec change proposal for: $ARGUMENTS
|
||||
|
||||
**Guardrails**
|
||||
- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
|
||||
- Keep changes tightly scoped to the requested outcome.
|
||||
- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications.
|
||||
- Identify any vague or ambiguous details and ask the necessary follow-up questions before editing files.
|
||||
- Do not write any code during the proposal stage. Only create design documents (proposal.md, tasks.md, design.md, and spec deltas). Implementation happens in the apply stage after approval.
|
||||
|
||||
**Steps**
|
||||
1. Review `openspec/project.md`, run `openspec list` and `openspec list --specs`, and inspect related code or docs (e.g., via `rg`/`ls`) to ground the proposal in current behaviour; note any gaps that require clarification.
|
||||
2. Choose a unique verb-led `change-id` and scaffold `proposal.md`, `tasks.md`, and `design.md` (when needed) under `openspec/changes/<id>/`.
|
||||
3. Map the change into concrete capabilities or requirements, breaking multi-scope efforts into distinct spec deltas with clear relationships and sequencing.
|
||||
4. Capture architectural reasoning in `design.md` when the solution spans multiple systems, introduces new patterns, or demands trade-off discussion before committing to specs.
|
||||
5. Draft spec deltas in `changes/<id>/specs/<capability>/spec.md` (one folder per capability) using `## ADDED|MODIFIED|REMOVED Requirements` with at least one `#### Scenario:` per requirement and cross-reference related capabilities when relevant.
|
||||
6. Draft `tasks.md` as an ordered list of small, verifiable work items that deliver user-visible progress, include validation (tests, tooling), and highlight dependencies or parallelizable work.
|
||||
7. Validate with `openspec validate <id> --strict` and resolve every issue before sharing the proposal.
|
||||
|
||||
**Reference**
|
||||
- Use `openspec show <id> --json --deltas-only` or `openspec show <spec> --type spec` to inspect details when validation fails.
|
||||
- Search existing requirements with `rg -n "Requirement:|Scenario:" openspec/specs` before writing new ones.
|
||||
- Explore the codebase with `rg <keyword>`, `ls`, or direct file reads so proposals align with current implementation realities.
|
||||
43
.commitlintrc.json
Normal file
43
.commitlintrc.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"extends": ["@commitlint/config-conventional"],
|
||||
"rules": {
|
||||
"type-enum": [
|
||||
2,
|
||||
"always",
|
||||
[
|
||||
"feat",
|
||||
"fix",
|
||||
"docs",
|
||||
"style",
|
||||
"refactor",
|
||||
"perf",
|
||||
"test",
|
||||
"build",
|
||||
"ci",
|
||||
"chore",
|
||||
"revert"
|
||||
]
|
||||
],
|
||||
"scope-enum": [
|
||||
2,
|
||||
"always",
|
||||
[
|
||||
"theme",
|
||||
"templates",
|
||||
"assets",
|
||||
"css",
|
||||
"js",
|
||||
"php",
|
||||
"admin",
|
||||
"api",
|
||||
"schema",
|
||||
"seo",
|
||||
"config",
|
||||
"deps"
|
||||
]
|
||||
],
|
||||
"subject-case": [2, "always", "lower-case"],
|
||||
"subject-max-length": [2, "always", 72],
|
||||
"body-max-line-length": [2, "always", 72]
|
||||
}
|
||||
}
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -73,5 +73,4 @@ _testing-suite/
|
||||
# Claude Code tools
|
||||
.playwright-mcp/
|
||||
.serena/
|
||||
.claude/
|
||||
nul
|
||||
|
||||
1
.husky/commit-msg
Normal file
1
.husky/commit-msg
Normal file
@@ -0,0 +1 @@
|
||||
npx --no -- commitlint --edit $1
|
||||
18
AGENTS.md
Normal file
18
AGENTS.md
Normal file
@@ -0,0 +1,18 @@
|
||||
<!-- OPENSPEC:START -->
|
||||
# OpenSpec Instructions
|
||||
|
||||
These instructions are for AI assistants working in this project.
|
||||
|
||||
Always open `@/openspec/AGENTS.md` when the request:
|
||||
- Mentions planning or proposals (words like proposal, spec, change, plan)
|
||||
- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work
|
||||
- Sounds ambiguous and you need the authoritative spec before coding
|
||||
|
||||
Use `@/openspec/AGENTS.md` to learn:
|
||||
- How to create and apply change proposals
|
||||
- Spec format and conventions
|
||||
- Project structure and guidelines
|
||||
|
||||
Keep this managed block so 'openspec update' can refresh the instructions.
|
||||
|
||||
<!-- OPENSPEC:END -->
|
||||
@@ -95,6 +95,29 @@ final class AdsensePlacementFieldMapper implements FieldMapperInterface
|
||||
'adsense-placementVignetteReshowEnabled' => ['group' => 'vignette_ads', 'attribute' => 'vignette_reshow_enabled'],
|
||||
'adsense-placementVignetteReshowTime' => ['group' => 'vignette_ads', 'attribute' => 'vignette_reshow_time'],
|
||||
'adsense-placementVignetteMaxPerSession' => ['group' => 'vignette_ads', 'attribute' => 'vignette_max_per_session'],
|
||||
|
||||
// SEARCH RESULTS (ROI APU Search)
|
||||
'adsense-placementSearchAdsEnabled' => ['group' => 'search_results', 'attribute' => 'search_ads_enabled'],
|
||||
'adsense-placementSearchTopAdEnabled' => ['group' => 'search_results', 'attribute' => 'search_top_ad_enabled'],
|
||||
'adsense-placementSearchTopAdFormat' => ['group' => 'search_results', 'attribute' => 'search_top_ad_format'],
|
||||
'adsense-placementSearchBetweenEnabled' => ['group' => 'search_results', 'attribute' => 'search_between_enabled'],
|
||||
'adsense-placementSearchBetweenMax' => ['group' => 'search_results', 'attribute' => 'search_between_max'],
|
||||
'adsense-placementSearchBetweenFormat' => ['group' => 'search_results', 'attribute' => 'search_between_format'],
|
||||
'adsense-placementSearchBetweenPosition' => ['group' => 'search_results', 'attribute' => 'search_between_position'],
|
||||
'adsense-placementSearchBetweenEvery' => ['group' => 'search_results', 'attribute' => 'search_between_every'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'adsense-placementVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'adsense-placementVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'adsense-placementVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'adsense-placementVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'adsense-placementVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'adsense-placementExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'adsense-placementExcludeCategoriesAdv' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'adsense-placementExcludePostIdsAdv' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'adsense-placementExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace ROITheme\Admin\AdsensePlacement\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* FormBuilder para AdSense Placement y Google Analytics
|
||||
@@ -57,6 +58,7 @@ final class AdsensePlacementFormBuilder
|
||||
$html .= $this->buildRailAdsGroup($componentId);
|
||||
$html .= $this->buildAnchorAdsGroup($componentId);
|
||||
$html .= $this->buildVignetteAdsGroup($componentId);
|
||||
$html .= $this->buildSearchResultsGroup($componentId);
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= '</div>';
|
||||
@@ -95,6 +97,47 @@ final class AdsensePlacementFormBuilder
|
||||
$html .= '<small class="text-muted d-block" style="margin-top: -8px; margin-left: 40px;">No mostrar anuncios a usuarios con sesion iniciada en WordPress</small>';
|
||||
$html .= '</div>';
|
||||
|
||||
// =============================================
|
||||
// Visibilidad por tipo de pagina
|
||||
// Grupo especial: _page_visibility (Plan 99.11)
|
||||
// =============================================
|
||||
$html .= '<hr class="my-3">';
|
||||
$html .= '<p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-layout-text-window me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= '</p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($cid, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($cid, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($cid, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($cid, '_page_visibility', 'show_on_archives', true);
|
||||
$showOnSearch = $this->renderer->getFieldValue($cid, '_page_visibility', 'show_on_search', true);
|
||||
|
||||
$html .= '<div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox($cid . 'VisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox($cid . 'VisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox($cid . 'VisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox($cid . 'VisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox($cid . 'VisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($cid, $cid);
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
@@ -708,6 +751,101 @@ final class AdsensePlacementFormBuilder
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seccion para anuncios en resultados de busqueda (ROI APU Search)
|
||||
*/
|
||||
private function buildSearchResultsGroup(string $cid): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #fd7e14;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-search me-2" style="color: #fd7e14;"></i>';
|
||||
$html .= ' Resultados de Busqueda';
|
||||
$html .= ' <span class="badge bg-secondary ms-2">ROI APU Search</span>';
|
||||
$html .= ' </h5>';
|
||||
$html .= ' <p class="small text-muted mb-3">Insertar anuncios en los resultados del buscador de Analisis de Precios Unitarios.</p>';
|
||||
|
||||
// Master switch
|
||||
$searchAdsEnabled = $this->renderer->getFieldValue($cid, 'search_results', 'search_ads_enabled', false);
|
||||
$html .= $this->buildSwitch($cid . 'SearchAdsEnabled', 'Activar ads en busqueda', $searchAdsEnabled, 'bi-power');
|
||||
|
||||
// Anuncio superior
|
||||
$html .= '<div class="border rounded p-3 mb-3" style="background: #fff8f0;">';
|
||||
$html .= '<div class="d-flex align-items-center gap-2 mb-2">';
|
||||
$html .= ' <span class="badge" style="background: #fd7e14;">ANUNCIO SUPERIOR</span>';
|
||||
$html .= ' <small class="text-muted">Debajo del campo de busqueda</small>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="row g-2">';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$topEnabled = $this->renderer->getFieldValue($cid, 'search_results', 'search_top_ad_enabled', true);
|
||||
$html .= $this->buildSwitch($cid . 'SearchTopAdEnabled', 'Activar', $topEnabled);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$topFormat = $this->renderer->getFieldValue($cid, 'search_results', 'search_top_ad_format', 'auto');
|
||||
$html .= $this->buildSelect($cid . 'SearchTopAdFormat', 'Formato',
|
||||
(string)$topFormat,
|
||||
['auto' => 'Auto (responsive)', 'display' => 'Display (fijo)', 'in-article' => 'In-Article (fluid)']
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Anuncios entre resultados
|
||||
$html .= '<div class="border rounded p-3" style="background: #fff8f0;">';
|
||||
$html .= '<div class="d-flex align-items-center gap-2 mb-2">';
|
||||
$html .= ' <span class="badge" style="background: #fd7e14;">ENTRE RESULTADOS</span>';
|
||||
$html .= ' <small class="text-muted">Intercalados con los resultados</small>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="row g-2 mb-2">';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$betweenEnabled = $this->renderer->getFieldValue($cid, 'search_results', 'search_between_enabled', true);
|
||||
$html .= $this->buildSwitch($cid . 'SearchBetweenEnabled', 'Activar', $betweenEnabled);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$betweenMax = $this->renderer->getFieldValue($cid, 'search_results', 'search_between_max', '1');
|
||||
$html .= $this->buildSelect($cid . 'SearchBetweenMax', 'Maximo ads',
|
||||
(string)$betweenMax,
|
||||
['1' => '1 anuncio', '2' => '2 anuncios', '3' => '3 anuncios (max)']
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="row g-2 mb-2">';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$betweenFormat = $this->renderer->getFieldValue($cid, 'search_results', 'search_between_format', 'in-article');
|
||||
$html .= $this->buildSelect($cid . 'SearchBetweenFormat', 'Formato',
|
||||
(string)$betweenFormat,
|
||||
['in-article' => 'In-Article (fluid)', 'auto' => 'Auto (responsive)', 'autorelaxed' => 'Autorelaxed (feed)']
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$betweenPosition = $this->renderer->getFieldValue($cid, 'search_results', 'search_between_position', 'random');
|
||||
$html .= $this->buildSelect($cid . 'SearchBetweenPosition', 'Posicion',
|
||||
(string)$betweenPosition,
|
||||
['random' => 'Aleatorio', 'fixed' => 'Fijo (cada N)', 'first_half' => 'Primera mitad']
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="row g-2">';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$betweenEvery = $this->renderer->getFieldValue($cid, 'search_results', 'search_between_every', '5');
|
||||
$html .= $this->buildSelect($cid . 'SearchBetweenEvery', 'Cada N resultados (si es fijo)',
|
||||
(string)$betweenEvery,
|
||||
['3' => 'Cada 3', '4' => 'Cada 4', '5' => 'Cada 5', '6' => 'Cada 6', '7' => 'Cada 7', '8' => 'Cada 8', '10' => 'Cada 10']
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildExclusionsGroup(string $cid): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #6c757d;">';
|
||||
@@ -825,4 +963,23 @@ final class AdsensePlacementFormBuilder
|
||||
esc_attr($id), esc_html($label), esc_attr($id), $optionsHtml
|
||||
);
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, $value): string
|
||||
{
|
||||
$checked = checked($value, true, false);
|
||||
|
||||
return sprintf(
|
||||
'<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="%s" %s>
|
||||
<label class="form-check-label small" for="%s">
|
||||
<i class="bi %s me-1" style="color: #6c757d;"></i>%s
|
||||
</label>
|
||||
</div>',
|
||||
esc_attr($id),
|
||||
$checked,
|
||||
esc_attr($id),
|
||||
esc_attr($icon),
|
||||
esc_html($label)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\ArchiveHeader\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para Archive Header
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*/
|
||||
final class ArchiveHeaderFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'archive-header';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Visibility
|
||||
'archiveHeaderEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'archiveHeaderShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'archiveHeaderShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'archiveHeaderVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'archiveHeaderVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'archiveHeaderVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'archiveHeaderVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'archiveHeaderVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions)
|
||||
'archiveHeaderExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'archiveHeaderExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'archiveHeaderExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'archiveHeaderExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'archiveHeaderBlogTitle' => ['group' => 'content', 'attribute' => 'blog_title'],
|
||||
'archiveHeaderShowPostCount' => ['group' => 'content', 'attribute' => 'show_post_count'],
|
||||
'archiveHeaderShowDescription' => ['group' => 'content', 'attribute' => 'show_description'],
|
||||
'archiveHeaderCategoryPrefix' => ['group' => 'content', 'attribute' => 'category_prefix'],
|
||||
'archiveHeaderTagPrefix' => ['group' => 'content', 'attribute' => 'tag_prefix'],
|
||||
'archiveHeaderAuthorPrefix' => ['group' => 'content', 'attribute' => 'author_prefix'],
|
||||
'archiveHeaderDatePrefix' => ['group' => 'content', 'attribute' => 'date_prefix'],
|
||||
'archiveHeaderSearchPrefix' => ['group' => 'content', 'attribute' => 'search_prefix'],
|
||||
'archiveHeaderCountSingular' => ['group' => 'content', 'attribute' => 'posts_count_singular'],
|
||||
'archiveHeaderCountPlural' => ['group' => 'content', 'attribute' => 'posts_count_plural'],
|
||||
|
||||
// Typography
|
||||
'archiveHeaderHeadingLevel' => ['group' => 'typography', 'attribute' => 'heading_level'],
|
||||
'archiveHeaderTitleSize' => ['group' => 'typography', 'attribute' => 'title_size'],
|
||||
'archiveHeaderTitleWeight' => ['group' => 'typography', 'attribute' => 'title_weight'],
|
||||
'archiveHeaderDescriptionSize' => ['group' => 'typography', 'attribute' => 'description_size'],
|
||||
'archiveHeaderCountSize' => ['group' => 'typography', 'attribute' => 'count_size'],
|
||||
|
||||
// Colors
|
||||
'archiveHeaderTitleColor' => ['group' => 'colors', 'attribute' => 'title_color'],
|
||||
'archiveHeaderPrefixColor' => ['group' => 'colors', 'attribute' => 'prefix_color'],
|
||||
'archiveHeaderDescriptionColor' => ['group' => 'colors', 'attribute' => 'description_color'],
|
||||
'archiveHeaderCountBgColor' => ['group' => 'colors', 'attribute' => 'count_bg_color'],
|
||||
'archiveHeaderCountTextColor' => ['group' => 'colors', 'attribute' => 'count_text_color'],
|
||||
|
||||
// Spacing
|
||||
'archiveHeaderMarginTop' => ['group' => 'spacing', 'attribute' => 'margin_top'],
|
||||
'archiveHeaderMarginBottom' => ['group' => 'spacing', 'attribute' => 'margin_bottom'],
|
||||
'archiveHeaderPadding' => ['group' => 'spacing', 'attribute' => 'padding'],
|
||||
'archiveHeaderTitleMarginBottom' => ['group' => 'spacing', 'attribute' => 'title_margin_bottom'],
|
||||
'archiveHeaderCountPadding' => ['group' => 'spacing', 'attribute' => 'count_padding'],
|
||||
|
||||
// Behavior
|
||||
'archiveHeaderIsSticky' => ['group' => 'behavior', 'attribute' => 'is_sticky'],
|
||||
'archiveHeaderStickyOffset' => ['group' => 'behavior', 'attribute' => 'sticky_offset'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,492 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\ArchiveHeader\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* FormBuilder para Archive Header
|
||||
*
|
||||
* @package ROITheme\Admin\ArchiveHeader\Infrastructure\Ui
|
||||
*/
|
||||
final class ArchiveHeaderFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
$html .= '<div class="row g-3">';
|
||||
|
||||
// Columna izquierda
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildContentGroup($componentId);
|
||||
$html .= $this->buildBehaviorGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
// Columna derecha
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildTypographyGroup($componentId);
|
||||
$html .= $this->buildColorsGroup($componentId);
|
||||
$html .= $this->buildSpacingGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildHeader(string $componentId): string
|
||||
{
|
||||
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
|
||||
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||
$html .= ' <div>';
|
||||
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||
$html .= ' <i class="bi bi-layout-text-window me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuracion de Cabecera de Archivo';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Cabecera dinamica para paginas de listados (blog, categorias, tags, autor, fecha, busqueda)';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="archive-header">';
|
||||
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
|
||||
$html .= ' Restaurar valores por defecto';
|
||||
$html .= ' </button>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisibilityGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= $this->buildSwitch('archiveHeaderEnabled', 'Activar componente', 'bi-power', $enabled);
|
||||
|
||||
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= $this->buildSwitch('archiveHeaderShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop);
|
||||
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= $this->buildSwitch('archiveHeaderShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
// Page visibility checkboxes
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', false);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', false);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', true);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', true);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('archiveHeaderVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('archiveHeaderVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('archiveHeaderVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('archiveHeaderVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('archiveHeaderVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Exclusions
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'archiveHeader');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-card-text me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Contenido';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Blog Title
|
||||
$blogTitle = $this->renderer->getFieldValue($componentId, 'content', 'blog_title', 'Blog');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="archiveHeaderBlogTitle" class="form-label small mb-1 fw-semibold">Titulo del blog</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderBlogTitle" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($blogTitle) . '">';
|
||||
$html .= ' <small class="text-muted">Mostrado en la pagina principal del blog</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switches
|
||||
$showPostCount = $this->renderer->getFieldValue($componentId, 'content', 'show_post_count', true);
|
||||
$html .= $this->buildSwitch('archiveHeaderShowPostCount', 'Mostrar contador de posts', 'bi-hash', $showPostCount);
|
||||
|
||||
$showDescription = $this->renderer->getFieldValue($componentId, 'content', 'show_description', true);
|
||||
$html .= $this->buildSwitch('archiveHeaderShowDescription', 'Mostrar descripcion', 'bi-text-paragraph', $showDescription);
|
||||
|
||||
// Prefixes section
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-tag me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Prefijos de titulo';
|
||||
$html .= ' </p>';
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
|
||||
$categoryPrefix = $this->renderer->getFieldValue($componentId, 'content', 'category_prefix', 'Categoria:');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="archiveHeaderCategoryPrefix" class="form-label small mb-1">Categoria</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderCategoryPrefix" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($categoryPrefix) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$tagPrefix = $this->renderer->getFieldValue($componentId, 'content', 'tag_prefix', 'Etiqueta:');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="archiveHeaderTagPrefix" class="form-label small mb-1">Etiqueta</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderTagPrefix" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($tagPrefix) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$authorPrefix = $this->renderer->getFieldValue($componentId, 'content', 'author_prefix', 'Articulos de:');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="archiveHeaderAuthorPrefix" class="form-label small mb-1">Autor</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderAuthorPrefix" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($authorPrefix) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$datePrefix = $this->renderer->getFieldValue($componentId, 'content', 'date_prefix', 'Archivo:');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="archiveHeaderDatePrefix" class="form-label small mb-1">Fecha</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderDatePrefix" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($datePrefix) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$searchPrefix = $this->renderer->getFieldValue($componentId, 'content', 'search_prefix', 'Resultados para:');
|
||||
$html .= ' <div class="col-12">';
|
||||
$html .= ' <label for="archiveHeaderSearchPrefix" class="form-label small mb-1">Busqueda</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderSearchPrefix" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($searchPrefix) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Post count texts
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-123 me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Textos del contador';
|
||||
$html .= ' </p>';
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
|
||||
$countSingular = $this->renderer->getFieldValue($componentId, 'content', 'posts_count_singular', 'publicacion');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="archiveHeaderCountSingular" class="form-label small mb-1">Singular</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderCountSingular" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($countSingular) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$countPlural = $this->renderer->getFieldValue($componentId, 'content', 'posts_count_plural', 'publicaciones');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="archiveHeaderCountPlural" class="form-label small mb-1">Plural</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderCountPlural" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($countPlural) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildBehaviorGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-gear me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Comportamiento';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$isSticky = $this->renderer->getFieldValue($componentId, 'behavior', 'is_sticky', false);
|
||||
$html .= $this->buildSwitch('archiveHeaderIsSticky', 'Header fijo al hacer scroll', 'bi-pin-angle', $isSticky);
|
||||
|
||||
$stickyOffset = $this->renderer->getFieldValue($componentId, 'behavior', 'sticky_offset', '0');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="archiveHeaderStickyOffset" class="form-label small mb-1 fw-semibold">Offset sticky</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderStickyOffset" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($stickyOffset) . '">';
|
||||
$html .= ' <small class="text-muted">Distancia desde el top cuando es sticky (ej: 60px)</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTypographyGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-fonts me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Tipografia';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Heading level
|
||||
$headingLevel = $this->renderer->getFieldValue($componentId, 'typography', 'heading_level', 'h1');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="archiveHeaderHeadingLevel" class="form-label small mb-1 fw-semibold">Nivel de encabezado</label>';
|
||||
$html .= ' <select id="archiveHeaderHeadingLevel" class="form-select form-select-sm">';
|
||||
foreach (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] as $level) {
|
||||
$selected = $headingLevel === $level ? ' selected' : '';
|
||||
$html .= sprintf(' <option value="%s"%s>%s</option>', $level, $selected, strtoupper($level));
|
||||
}
|
||||
$html .= ' </select>';
|
||||
$html .= ' <small class="text-muted">Importante para SEO y accesibilidad</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$titleSize = $this->renderer->getFieldValue($componentId, 'typography', 'title_size', '2rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="archiveHeaderTitleSize" class="form-label small mb-1 fw-semibold">Tamano titulo</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderTitleSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$titleWeight = $this->renderer->getFieldValue($componentId, 'typography', 'title_weight', '700');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="archiveHeaderTitleWeight" class="form-label small mb-1 fw-semibold">Peso titulo</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderTitleWeight" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleWeight) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$descriptionSize = $this->renderer->getFieldValue($componentId, 'typography', 'description_size', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="archiveHeaderDescriptionSize" class="form-label small mb-1 fw-semibold">Tamano descripcion</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderDescriptionSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($descriptionSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$countSize = $this->renderer->getFieldValue($componentId, 'typography', 'count_size', '0.875rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="archiveHeaderCountSize" class="form-label small mb-1 fw-semibold">Tamano contador</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderCountSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($countSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Colores';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$titleColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_color', '#0E2337');
|
||||
$html .= $this->buildColorPicker('archiveHeaderTitleColor', 'Titulo', $titleColor);
|
||||
|
||||
$prefixColor = $this->renderer->getFieldValue($componentId, 'colors', 'prefix_color', '#6b7280');
|
||||
$html .= $this->buildColorPicker('archiveHeaderPrefixColor', 'Prefijo', $prefixColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$descriptionColor = $this->renderer->getFieldValue($componentId, 'colors', 'description_color', '#6b7280');
|
||||
$html .= $this->buildColorPicker('archiveHeaderDescriptionColor', 'Descripcion', $descriptionColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <p class="small fw-semibold mb-2">Contador de posts</p>';
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$countBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'count_bg_color', '#FF8600');
|
||||
$html .= $this->buildColorPicker('archiveHeaderCountBgColor', 'Fondo', $countBgColor);
|
||||
|
||||
$countTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'count_text_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('archiveHeaderCountTextColor', 'Texto', $countTextColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSpacingGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-arrows-move me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$marginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_top', '2rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="archiveHeaderMarginTop" class="form-label small mb-1 fw-semibold">Margen superior</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderMarginTop" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($marginTop) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$marginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_bottom', '2rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="archiveHeaderMarginBottom" class="form-label small mb-1 fw-semibold">Margen inferior</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($marginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$padding = $this->renderer->getFieldValue($componentId, 'spacing', 'padding', '1.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="archiveHeaderPadding" class="form-label small mb-1 fw-semibold">Padding</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderPadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($padding) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$titleMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'title_margin_bottom', '0.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="archiveHeaderTitleMarginBottom" class="form-label small mb-1 fw-semibold">Margen titulo</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderTitleMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleMarginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="mb-0">';
|
||||
$countPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'count_padding', '0.25rem 0.75rem');
|
||||
$html .= ' <label for="archiveHeaderCountPadding" class="form-label small mb-1 fw-semibold">Padding contador</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderCountPadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($countPadding) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSwitch(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' <strong>%s</strong>', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorPicker(string $id, string $label, string $value): string
|
||||
{
|
||||
$html = ' <div class="col-6">';
|
||||
$html .= sprintf(
|
||||
' <label class="form-label small fw-semibold">%s</label>',
|
||||
esc_html($label)
|
||||
);
|
||||
$html .= ' <div class="input-group input-group-sm">';
|
||||
$html .= sprintf(
|
||||
' <input type="color" class="form-control form-control-color" id="%s" value="%s">',
|
||||
esc_attr($id),
|
||||
esc_attr($value)
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <span class="input-group-text" id="%sValue">%s</span>',
|
||||
esc_attr($id),
|
||||
esc_html(strtoupper($value))
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,19 @@ final class ContactFormFieldMapper implements FieldMapperInterface
|
||||
'contactFormEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'contactFormShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'contactFormShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'contactFormShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'contactFormVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'contactFormVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'contactFormVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'contactFormVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'contactFormVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'contactFormExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'contactFormExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'contactFormExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'contactFormExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'contactFormSectionTitle' => ['group' => 'content', 'attribute' => 'section_title'],
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace ROITheme\Admin\ContactForm\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* FormBuilder para Contact Form
|
||||
@@ -93,19 +94,47 @@ final class ContactFormFormBuilder
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= $this->buildSwitch('contactFormShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all');
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <label for="contactFormShowOnPages" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="contactFormShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all"' . ($showOnPages === 'all' ? ' selected' : '') . '>Todos</option>';
|
||||
$html .= ' <option value="posts"' . ($showOnPages === 'posts' ? ' selected' : '') . '>Solo posts</option>';
|
||||
$html .= ' <option value="pages"' . ($showOnPages === 'pages' ? ' selected' : '') . '>Solo paginas</option>';
|
||||
$html .= ' </select>';
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('contactFormVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('contactFormVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('contactFormVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('contactFormVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('contactFormVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'contactForm');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
@@ -598,4 +627,26 @@ final class ContactFormFormBuilder
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,20 @@ final class CtaBoxSidebarFieldMapper implements FieldMapperInterface
|
||||
'ctaEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'ctaShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'ctaShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'ctaShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
'ctaHideForLoggedIn' => ['group' => 'visibility', 'attribute' => 'hide_for_logged_in'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'ctaVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'ctaVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'ctaVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'ctaVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'ctaVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'ctaExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'ctaExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'ctaExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'ctaExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'ctaTitle' => ['group' => 'content', 'attribute' => 'title'],
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace ROITheme\Admin\CtaBoxSidebar\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* FormBuilder para el CTA Box Sidebar
|
||||
@@ -94,18 +95,61 @@ final class CtaBoxSidebarFormBuilder
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', false);
|
||||
$html .= $this->buildSwitch('ctaShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
// show_on_pages
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
// Obtener valores de _page_visibility (grupo especial)
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', true);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
// Grid 3 columnas según Design System
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'cta');
|
||||
|
||||
// Switch: Ocultar para usuarios logueados (Plan 99.16)
|
||||
$hideForLoggedIn = $this->renderer->getFieldValue($componentId, 'visibility', 'hide_for_logged_in', false);
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <label for="ctaShowOnPages" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="ctaShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all"' . ($showOnPages === 'all' ? ' selected' : '') . '>Todos</option>';
|
||||
$html .= ' <option value="posts"' . ($showOnPages === 'posts' ? ' selected' : '') . '>Solo posts</option>';
|
||||
$html .= ' <option value="pages"' . ($showOnPages === 'pages' ? ' selected' : '') . '>Solo paginas</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="ctaHideForLoggedIn" ';
|
||||
$html .= checked($hideForLoggedIn, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="ctaHideForLoggedIn" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-person-lock me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Ocultar para usuarios logueados</strong>';
|
||||
$html .= ' <small class="text-muted d-block">No mostrar a usuarios con sesión iniciada</small>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
@@ -515,4 +559,29 @@ final class CtaBoxSidebarFormBuilder
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera un checkbox de visibilidad por tipo de pagina
|
||||
*
|
||||
* Sigue Design System: form-check-checkbox es obligatorio
|
||||
*/
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, bool $checked): string
|
||||
{
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,20 @@ final class CtaLetsTalkFieldMapper implements FieldMapperInterface
|
||||
'ctaLetsTalkEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'ctaLetsTalkShowDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'ctaLetsTalkShowMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'ctaLetsTalkShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
'ctaLetsTalkHideForLoggedIn' => ['group' => 'visibility', 'attribute' => 'hide_for_logged_in'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'ctaLetsTalkVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'ctaLetsTalkVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'ctaLetsTalkVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'ctaLetsTalkVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'ctaLetsTalkVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'letsTalkExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'letsTalkExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'letsTalkExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'letsTalkExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'ctaLetsTalkButtonText' => ['group' => 'content', 'attribute' => 'button_text'],
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace ROITheme\Admin\CtaLetsTalk\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* Class CtaLetsTalkFormBuilder
|
||||
@@ -120,16 +121,73 @@ final class CtaLetsTalkFormBuilder
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Select: Show on Pages
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all');
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaLetsTalkVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaLetsTalkVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaLetsTalkVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaLetsTalkVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaLetsTalkVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'letsTalk');
|
||||
|
||||
// Switch: CSS Crítico
|
||||
$isCritical = $this->renderer->getFieldValue($componentId, 'visibility', 'is_critical', true);
|
||||
$html .= ' <div class="mb-2 mt-3">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="ctaLetsTalkIsCritical" ';
|
||||
$html .= checked($isCritical, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="ctaLetsTalkIsCritical" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-lightning-charge me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>CSS Crítico</strong>';
|
||||
$html .= ' <small class="text-muted d-block">Inyectar CSS en <head> para optimizar LCP</small>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: Ocultar para usuarios logueados (Plan 99.16)
|
||||
$hideForLoggedIn = $this->renderer->getFieldValue($componentId, 'visibility', 'hide_for_logged_in', false);
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="ctaLetsTalkShowOnPages" class="form-label small mb-1 fw-semibold">Mostrar en</label>';
|
||||
$html .= ' <select id="ctaLetsTalkShowOnPages" name="visibility[show_on_pages]" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las páginas</option>';
|
||||
$html .= ' <option value="home" ' . selected($showOnPages, 'home', false) . '>Solo página de inicio</option>';
|
||||
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts individuales</option>';
|
||||
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo páginas</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="ctaLetsTalkHideForLoggedIn" ';
|
||||
$html .= checked($hideForLoggedIn, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="ctaLetsTalkHideForLoggedIn" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-person-lock me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Ocultar para usuarios logueados</strong>';
|
||||
$html .= ' <small class="text-muted d-block">No mostrar a usuarios con sesión iniciada</small>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
@@ -447,4 +505,26 @@ final class CtaLetsTalkFormBuilder
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,20 @@ final class CtaPostFieldMapper implements FieldMapperInterface
|
||||
'ctaPostEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'ctaPostShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'ctaPostShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'ctaPostShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
'ctaPostHideForLoggedIn' => ['group' => 'visibility', 'attribute' => 'hide_for_logged_in'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'ctaPostVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'ctaPostVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'ctaPostVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'ctaPostVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'ctaPostVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'ctaPostExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'ctaPostExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'ctaPostExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'ctaPostExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'ctaPostTitle' => ['group' => 'content', 'attribute' => 'title'],
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace ROITheme\Admin\CtaPost\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* FormBuilder para CTA Post
|
||||
@@ -85,17 +86,59 @@ final class CtaPostFormBuilder
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= $this->buildSwitch('ctaPostShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaPostVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaPostVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaPostVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaPostVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaPostVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'ctaPost');
|
||||
|
||||
// Switch: Ocultar para usuarios logueados (Plan 99.16)
|
||||
$hideForLoggedIn = $this->renderer->getFieldValue($componentId, 'visibility', 'hide_for_logged_in', false);
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <label for="ctaPostShowOnPages" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="ctaPostShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all"' . ($showOnPages === 'all' ? ' selected' : '') . '>Todos</option>';
|
||||
$html .= ' <option value="posts"' . ($showOnPages === 'posts' ? ' selected' : '') . '>Solo posts</option>';
|
||||
$html .= ' <option value="pages"' . ($showOnPages === 'pages' ? ' selected' : '') . '>Solo paginas</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="ctaPostHideForLoggedIn" ';
|
||||
$html .= checked($hideForLoggedIn, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="ctaPostHideForLoggedIn" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-person-lock me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Ocultar para usuarios logueados</strong>';
|
||||
$html .= ' <small class="text-muted d-block">No mostrar a usuarios con sesion iniciada</small>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
@@ -437,4 +480,26 @@ final class CtaPostFormBuilder
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,10 @@ use ROITheme\Shared\Domain\Exceptions\ValidationException;
|
||||
/**
|
||||
* Value Object para ID único de snippet CSS
|
||||
*
|
||||
* Soporta dos formatos:
|
||||
* Soporta tres formatos:
|
||||
* 1. Generado: css_[timestamp]_[random] (ej: "css_1701432000_a1b2c3")
|
||||
* 2. Legacy/Migración: kebab-case (ej: "cls-tables-apu", "generic-tables")
|
||||
* 2. Legacy con prefijo: css_[descriptive]_[number] (ej: "css_tablas_apu_1764624826")
|
||||
* 3. Legacy kebab-case: (ej: "cls-tables-apu", "generic-tables")
|
||||
*
|
||||
* Esto permite migrar snippets existentes sin romper IDs.
|
||||
*/
|
||||
@@ -18,6 +19,7 @@ final class SnippetId
|
||||
{
|
||||
private const PREFIX = 'css_';
|
||||
private const PATTERN_GENERATED = '/^css_[0-9]+_[a-z0-9]{6}$/';
|
||||
private const PATTERN_LEGACY_PREFIX = '/^css_[a-z0-9_]+$/';
|
||||
private const PATTERN_LEGACY = '/^[a-z0-9]+(-[a-z0-9]+)*$/';
|
||||
|
||||
private function __construct(
|
||||
@@ -47,7 +49,8 @@ final class SnippetId
|
||||
|
||||
// Validar formato generado (css_*)
|
||||
if (str_starts_with($id, self::PREFIX)) {
|
||||
if (!preg_match(self::PATTERN_GENERATED, $id)) {
|
||||
// Acepta formato nuevo (css_timestamp_random) o legacy (css_descriptivo_numero)
|
||||
if (!preg_match(self::PATTERN_GENERATED, $id) && !preg_match(self::PATTERN_LEGACY_PREFIX, $id)) {
|
||||
throw new ValidationException(
|
||||
sprintf('Formato de ID generado inválido: %s. Esperado: css_[timestamp]_[random]', $id)
|
||||
);
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CustomCSSManager\Infrastructure\Bootstrap;
|
||||
|
||||
use ROITheme\Admin\CustomCSSManager\Infrastructure\Persistence\WordPressSnippetRepository;
|
||||
use ROITheme\Admin\CustomCSSManager\Application\UseCases\SaveSnippetUseCase;
|
||||
use ROITheme\Admin\CustomCSSManager\Application\UseCases\DeleteSnippetUseCase;
|
||||
use ROITheme\Admin\CustomCSSManager\Application\DTOs\SaveSnippetRequest;
|
||||
use ROITheme\Admin\CustomCSSManager\Domain\ValueObjects\SnippetId;
|
||||
use ROITheme\Shared\Domain\Exceptions\ValidationException;
|
||||
|
||||
/**
|
||||
* Bootstrap para CustomCSSManager
|
||||
*
|
||||
* Registra el handler de formulario POST en admin_init
|
||||
* ANTES de que se envíen headers HTTP
|
||||
*/
|
||||
final class CustomCSSManagerBootstrap
|
||||
{
|
||||
private const NONCE_ACTION = 'roi_custom_css_manager';
|
||||
|
||||
public static function init(): void
|
||||
{
|
||||
add_action('admin_init', [self::class, 'handleFormSubmission']);
|
||||
}
|
||||
|
||||
public static function handleFormSubmission(): void
|
||||
{
|
||||
if (!isset($_POST['roi_css_action'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Verificar que estamos en la página correcta
|
||||
$page = $_GET['page'] ?? '';
|
||||
$component = $_GET['component'] ?? '';
|
||||
if ($page !== 'roi-theme-admin' || $component !== 'custom-css-manager') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Verificar nonce
|
||||
if (!wp_verify_nonce($_POST['_wpnonce'] ?? '', self::NONCE_ACTION)) {
|
||||
wp_die('Nonce verification failed');
|
||||
}
|
||||
|
||||
// Verificar permisos
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_die('Insufficient permissions');
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$repository = new WordPressSnippetRepository($wpdb);
|
||||
$saveUseCase = new SaveSnippetUseCase($repository);
|
||||
$deleteUseCase = new DeleteSnippetUseCase($repository);
|
||||
|
||||
$action = sanitize_text_field($_POST['roi_css_action']);
|
||||
|
||||
try {
|
||||
match ($action) {
|
||||
'save' => self::processSave($_POST, $saveUseCase),
|
||||
'delete' => self::processDelete($_POST, $deleteUseCase),
|
||||
default => null,
|
||||
};
|
||||
|
||||
// Redirect con mensaje de éxito
|
||||
$redirect_url = admin_url('admin.php?page=roi-theme-admin&component=custom-css-manager&roi_message=success');
|
||||
wp_redirect($redirect_url);
|
||||
exit;
|
||||
|
||||
} catch (ValidationException $e) {
|
||||
$redirect_url = admin_url('admin.php?page=roi-theme-admin&component=custom-css-manager&roi_message=error&roi_error=' . urlencode($e->getMessage()));
|
||||
wp_redirect($redirect_url);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
private static function processSave(array $data, SaveSnippetUseCase $useCase): void
|
||||
{
|
||||
$id = sanitize_text_field($data['snippet_id'] ?? '');
|
||||
if (empty($id)) {
|
||||
$id = SnippetId::generate()->value();
|
||||
}
|
||||
|
||||
$request = SaveSnippetRequest::fromArray([
|
||||
'id' => $id,
|
||||
'name' => sanitize_text_field($data['snippet_name'] ?? ''),
|
||||
'description' => sanitize_textarea_field($data['snippet_description'] ?? ''),
|
||||
'css' => wp_strip_all_tags($data['snippet_css'] ?? ''),
|
||||
'type' => sanitize_text_field($data['snippet_type'] ?? 'deferred'),
|
||||
'pages' => array_map('sanitize_text_field', $data['snippet_pages'] ?? ['all']),
|
||||
'enabled' => isset($data['snippet_enabled']),
|
||||
'order' => absint($data['snippet_order'] ?? 100),
|
||||
]);
|
||||
|
||||
$useCase->execute($request);
|
||||
}
|
||||
|
||||
private static function processDelete(array $data, DeleteSnippetUseCase $useCase): void
|
||||
{
|
||||
$id = sanitize_text_field($data['snippet_id'] ?? '');
|
||||
if (empty($id)) {
|
||||
throw new ValidationException('ID de snippet requerido para eliminar');
|
||||
}
|
||||
$useCase->execute($id);
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,7 @@ namespace ROITheme\Admin\CustomCSSManager\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\CustomCSSManager\Infrastructure\Persistence\WordPressSnippetRepository;
|
||||
use ROITheme\Admin\CustomCSSManager\Application\UseCases\SaveSnippetUseCase;
|
||||
use ROITheme\Admin\CustomCSSManager\Application\UseCases\DeleteSnippetUseCase;
|
||||
use ROITheme\Admin\CustomCSSManager\Application\UseCases\GetAllSnippetsUseCase;
|
||||
use ROITheme\Admin\CustomCSSManager\Application\DTOs\SaveSnippetRequest;
|
||||
use ROITheme\Admin\CustomCSSManager\Domain\ValueObjects\SnippetId;
|
||||
use ROITheme\Shared\Domain\Exceptions\ValidationException;
|
||||
|
||||
/**
|
||||
* FormBuilder para gestión de CSS snippets en Admin Panel
|
||||
@@ -19,6 +14,9 @@ use ROITheme\Shared\Domain\Exceptions\ValidationException;
|
||||
* - Constructor recibe AdminDashboardRenderer
|
||||
* - Método buildForm() genera el HTML del formulario
|
||||
*
|
||||
* NOTA: El handler de formulario POST está en CustomCSSManagerBootstrap
|
||||
* para que se ejecute en admin_init ANTES de que se envíen headers HTTP.
|
||||
*
|
||||
* Design System: Gradiente navy #0E2337 → #1e3a5f, accent #FF8600
|
||||
*/
|
||||
final class CustomCSSManagerFormBuilder
|
||||
@@ -28,120 +26,15 @@ final class CustomCSSManagerFormBuilder
|
||||
|
||||
private WordPressSnippetRepository $repository;
|
||||
private GetAllSnippetsUseCase $getAllUseCase;
|
||||
private SaveSnippetUseCase $saveUseCase;
|
||||
private DeleteSnippetUseCase $deleteUseCase;
|
||||
|
||||
public function __construct(
|
||||
private readonly AdminDashboardRenderer $renderer
|
||||
) {
|
||||
// Crear repositorio y Use Cases internamente
|
||||
// Crear repositorio y Use Case para listar snippets
|
||||
global $wpdb;
|
||||
$this->repository = new WordPressSnippetRepository($wpdb);
|
||||
$this->getAllUseCase = new GetAllSnippetsUseCase($this->repository);
|
||||
$this->saveUseCase = new SaveSnippetUseCase($this->repository);
|
||||
$this->deleteUseCase = new DeleteSnippetUseCase($this->repository);
|
||||
|
||||
// Registrar handler de formulario POST
|
||||
$this->registerFormHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra handler para procesar formularios POST
|
||||
*/
|
||||
private function registerFormHandler(): void
|
||||
{
|
||||
// Solo registrar una vez
|
||||
static $registered = false;
|
||||
if ($registered) {
|
||||
return;
|
||||
}
|
||||
$registered = true;
|
||||
|
||||
add_action('admin_init', function() {
|
||||
$this->handleFormSubmission();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Procesa envío de formulario
|
||||
*/
|
||||
public function handleFormSubmission(): void
|
||||
{
|
||||
if (!isset($_POST['roi_css_action'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Verificar nonce
|
||||
if (!wp_verify_nonce($_POST['_wpnonce'] ?? '', self::NONCE_ACTION)) {
|
||||
wp_die('Nonce verification failed');
|
||||
}
|
||||
|
||||
// Verificar permisos
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_die('Insufficient permissions');
|
||||
}
|
||||
|
||||
$action = sanitize_text_field($_POST['roi_css_action']);
|
||||
|
||||
try {
|
||||
match ($action) {
|
||||
'save' => $this->processSave($_POST),
|
||||
'delete' => $this->processDelete($_POST),
|
||||
default => null,
|
||||
};
|
||||
|
||||
// Redirect con mensaje de éxito
|
||||
wp_redirect(add_query_arg('roi_message', 'success', wp_get_referer()));
|
||||
exit;
|
||||
|
||||
} catch (ValidationException $e) {
|
||||
// Redirect con mensaje de error
|
||||
wp_redirect(add_query_arg([
|
||||
'roi_message' => 'error',
|
||||
'roi_error' => urlencode($e->getMessage())
|
||||
], wp_get_referer()));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Procesa guardado de snippet
|
||||
*/
|
||||
private function processSave(array $data): void
|
||||
{
|
||||
$id = sanitize_text_field($data['snippet_id'] ?? '');
|
||||
|
||||
// Generar ID si es nuevo
|
||||
if (empty($id)) {
|
||||
$id = SnippetId::generate()->value();
|
||||
}
|
||||
|
||||
$request = SaveSnippetRequest::fromArray([
|
||||
'id' => $id,
|
||||
'name' => sanitize_text_field($data['snippet_name'] ?? ''),
|
||||
'description' => sanitize_textarea_field($data['snippet_description'] ?? ''),
|
||||
'css' => wp_strip_all_tags($data['snippet_css'] ?? ''),
|
||||
'type' => sanitize_text_field($data['snippet_type'] ?? 'deferred'),
|
||||
'pages' => array_map('sanitize_text_field', $data['snippet_pages'] ?? ['all']),
|
||||
'enabled' => isset($data['snippet_enabled']),
|
||||
'order' => absint($data['snippet_order'] ?? 100),
|
||||
]);
|
||||
|
||||
$this->saveUseCase->execute($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Procesa eliminación de snippet
|
||||
*/
|
||||
private function processDelete(array $data): void
|
||||
{
|
||||
$id = sanitize_text_field($data['snippet_id'] ?? '');
|
||||
|
||||
if (empty($id)) {
|
||||
throw new ValidationException('ID de snippet requerido para eliminar');
|
||||
}
|
||||
|
||||
$this->deleteUseCase->execute($id);
|
||||
// NOTA: El handler POST está en CustomCSSManagerBootstrap (admin_init)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,13 +53,9 @@ final class CustomCSSManagerFormBuilder
|
||||
// Header
|
||||
$html .= $this->buildHeader($componentId, count($snippets));
|
||||
|
||||
// Mensajes flash
|
||||
// Toast para mensajes (usa el sistema existente de admin-dashboard.js)
|
||||
if ($message) {
|
||||
$html .= sprintf(
|
||||
'<div class="alert alert-%s m-3">%s</div>',
|
||||
esc_attr($message['type']),
|
||||
esc_html($message['text'])
|
||||
);
|
||||
$html .= $this->buildToastTrigger($message);
|
||||
}
|
||||
|
||||
// Lista de snippets existentes
|
||||
@@ -367,7 +256,7 @@ final class CustomCSSManagerFormBuilder
|
||||
// Botones
|
||||
$html .= ' <div class="col-12">';
|
||||
$html .= ' <button type="submit" class="btn text-white" style="background-color: #FF8600;">';
|
||||
$html .= ' <i class="bi bi-save me-1"></i> Guardar Snippet';
|
||||
$html .= ' <i class="bi bi-check-circle me-1"></i> Guardar Cambios';
|
||||
$html .= ' </button>';
|
||||
$html .= ' <button type="button" class="btn btn-secondary" onclick="resetCssForm()">';
|
||||
$html .= ' <i class="bi bi-x-circle me-1"></i> Cancelar';
|
||||
@@ -449,14 +338,84 @@ final class CustomCSSManagerFormBuilder
|
||||
$message = $_GET['roi_message'] ?? null;
|
||||
|
||||
if ($message === 'success') {
|
||||
return ['type' => 'success', 'text' => 'Snippet guardado correctamente'];
|
||||
return ['type' => 'success', 'text' => 'Cambios guardados correctamente'];
|
||||
}
|
||||
|
||||
if ($message === 'error') {
|
||||
$error = urldecode($_GET['roi_error'] ?? 'Error desconocido');
|
||||
return ['type' => 'danger', 'text' => $error];
|
||||
return ['type' => 'error', 'text' => $error];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera script para mostrar Toast
|
||||
*/
|
||||
private function buildToastTrigger(array $message): string
|
||||
{
|
||||
$type = esc_js($message['type']);
|
||||
$text = esc_js($message['text']);
|
||||
|
||||
// Mapear tipo a configuración de Bootstrap
|
||||
$typeMap = [
|
||||
'success' => ['bg' => 'success', 'icon' => 'bi-check-circle-fill'],
|
||||
'error' => ['bg' => 'danger', 'icon' => 'bi-x-circle-fill'],
|
||||
];
|
||||
$config = $typeMap[$type] ?? $typeMap['success'];
|
||||
$bg = $config['bg'];
|
||||
$icon = $config['icon'];
|
||||
|
||||
return <<<HTML
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Crear container de toasts si no existe
|
||||
let toastContainer = document.getElementById('roiToastContainer');
|
||||
if (!toastContainer) {
|
||||
toastContainer = document.createElement('div');
|
||||
toastContainer.id = 'roiToastContainer';
|
||||
toastContainer.className = 'toast-container position-fixed start-50 translate-middle-x';
|
||||
toastContainer.style.top = '60px';
|
||||
toastContainer.style.zIndex = '999999';
|
||||
document.body.appendChild(toastContainer);
|
||||
}
|
||||
|
||||
// Crear toast
|
||||
const toastId = 'toast-' + Date.now();
|
||||
const toastHTML = `
|
||||
<div id="\${toastId}" class="toast align-items-center text-white bg-{$bg} border-0" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
<i class="bi {$icon} me-2"></i>
|
||||
<strong>{$text}</strong>
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
toastContainer.insertAdjacentHTML('beforeend', toastHTML);
|
||||
|
||||
// Mostrar toast
|
||||
const toastElement = document.getElementById(toastId);
|
||||
const toast = new bootstrap.Toast(toastElement, {
|
||||
autohide: true,
|
||||
delay: 5000
|
||||
});
|
||||
toast.show();
|
||||
|
||||
// Eliminar del DOM después de ocultarse
|
||||
toastElement.addEventListener('hidden.bs.toast', function() {
|
||||
toastElement.remove();
|
||||
});
|
||||
|
||||
// Limpiar parámetros de URL sin recargar
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete('roi_message');
|
||||
url.searchParams.delete('roi_error');
|
||||
window.history.replaceState({}, '', url.toString());
|
||||
});
|
||||
</script>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,19 @@ final class FeaturedImageFieldMapper implements FieldMapperInterface
|
||||
'featuredImageEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'featuredImageShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'featuredImageShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'featuredImageShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'featuredImageVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'featuredImageVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'featuredImageVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'featuredImageVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'featuredImageVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'featuredImageExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'featuredImageExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'featuredImageExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'featuredImageExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'featuredImageSize' => ['group' => 'content', 'attribute' => 'image_size'],
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace ROITheme\Admin\FeaturedImage\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
final class FeaturedImageFormBuilder
|
||||
{
|
||||
@@ -100,25 +101,75 @@ final class FeaturedImageFormBuilder
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <label for="featuredImageShowOnPages" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="featuredImageShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las paginas</option>';
|
||||
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts individuales</option>';
|
||||
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo paginas</option>';
|
||||
$html .= ' </select>';
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', false);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'featuredImage');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
|
||||
@@ -27,6 +27,19 @@ final class FooterFieldMapper implements FieldMapperInterface
|
||||
'footerShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'footerShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'footerVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'footerVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'footerVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'footerVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'footerVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'footerExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'footerExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'footerExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'footerExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Widget 1
|
||||
'footerWidget1Visible' => ['group' => 'widget_1', 'attribute' => 'widget_1_visible'],
|
||||
'footerWidget1Title' => ['group' => 'widget_1', 'attribute' => 'widget_1_title'],
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace ROITheme\Admin\Footer\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* FormBuilder para Footer
|
||||
@@ -90,6 +91,47 @@ final class FooterFormBuilder
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= $this->buildSwitch('footerShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', true);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', true);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('footerVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('footerVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('footerVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('footerVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('footerVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'footer');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
@@ -410,4 +452,19 @@ final class FooterFormBuilder
|
||||
}
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, $value): string
|
||||
{
|
||||
$checked = $value === true || $value === '1' || $value === 1 ? 'checked' : '';
|
||||
|
||||
$html = '<div class="form-check">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="' . esc_attr($id) . '" ' . $checked . '>';
|
||||
$html .= ' <label class="form-check-label small" for="' . esc_attr($id) . '">';
|
||||
$html .= ' <i class="bi ' . esc_attr($icon) . ' me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' ' . esc_html($label);
|
||||
$html .= ' </label>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,9 +26,21 @@ final class HeroFieldMapper implements FieldMapperInterface
|
||||
'heroEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'heroShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'heroShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'heroShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
'heroIsCritical' => ['group' => 'visibility', 'attribute' => 'is_critical'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'heroVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'heroVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'heroVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'heroVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'heroVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'heroExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'heroExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'heroExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'heroExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'heroShowCategories' => ['group' => 'content', 'attribute' => 'show_categories'],
|
||||
'heroShowBadgeIcon' => ['group' => 'content', 'attribute' => 'show_badge_icon'],
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace ROITheme\Admin\Hero\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
final class HeroFormBuilder
|
||||
{
|
||||
@@ -102,20 +103,47 @@ final class HeroFormBuilder
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
|
||||
$html .= ' <div class="mb-2 mt-3">';
|
||||
$html .= ' <label for="heroShowOnPages" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="heroShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las páginas</option>';
|
||||
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts individuales</option>';
|
||||
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo páginas</option>';
|
||||
$html .= ' <option value="home" ' . selected($showOnPages, 'home', false) . '>Solo página de inicio</option>';
|
||||
$html .= ' </select>';
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', false);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('heroVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('heroVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('heroVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('heroVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('heroVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'hero');
|
||||
|
||||
// Switch: CSS Crítico
|
||||
$isCritical = $this->renderer->getFieldValue($componentId, 'visibility', 'is_critical', true);
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
@@ -427,4 +455,26 @@ final class HeroFormBuilder
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +107,15 @@ final class AdminAssetEnqueuer
|
||||
true
|
||||
);
|
||||
|
||||
// Script de toggle para exclusiones (Plan 99.11)
|
||||
wp_enqueue_script(
|
||||
'roi-exclusion-toggle',
|
||||
$this->themeUri . '/Admin/Shared/Infrastructure/Ui/Assets/Js/exclusion-toggle.js',
|
||||
['roi-admin-dashboard'],
|
||||
filemtime(get_template_directory() . '/Admin/Shared/Infrastructure/Ui/Assets/Js/exclusion-toggle.js'),
|
||||
true
|
||||
);
|
||||
|
||||
// Pasar variables al JavaScript
|
||||
wp_localize_script(
|
||||
'roi-admin-dashboard',
|
||||
|
||||
@@ -98,6 +98,16 @@ final class AdminDashboardRenderer implements DashboardRendererInterface
|
||||
'label' => 'Related Posts',
|
||||
'icon' => 'bi-grid-3x3-gap',
|
||||
],
|
||||
'archive-header' => [
|
||||
'id' => 'archive-header',
|
||||
'label' => 'Archive Header',
|
||||
'icon' => 'bi-layout-text-window',
|
||||
],
|
||||
'post-grid' => [
|
||||
'id' => 'post-grid',
|
||||
'label' => 'Post Grid',
|
||||
'icon' => 'bi-grid-3x3',
|
||||
],
|
||||
'contact-form' => [
|
||||
'id' => 'contact-form',
|
||||
'label' => 'Contact Form',
|
||||
|
||||
@@ -37,7 +37,7 @@ final class ComponentGroupRegistry
|
||||
'label' => __('Contenido Principal', 'roi-theme'),
|
||||
'icon' => 'bi-file-richtext',
|
||||
'description' => __('Secciones principales de páginas y posts', 'roi-theme'),
|
||||
'components' => ['hero', 'featured-image', 'table-of-contents', 'related-post']
|
||||
'components' => ['hero', 'featured-image', 'table-of-contents', 'related-post', 'archive-header', 'post-grid']
|
||||
],
|
||||
'ctas-conversion' => [
|
||||
'label' => __('CTAs & Conversión', 'roi-theme'),
|
||||
|
||||
@@ -60,6 +60,12 @@ $group = $groupId && isset($groups[$groupId]) ? $groups[$groupId] : null;
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// Componentes con sistema de guardado propio (CRUD de entidades)
|
||||
$componentsWithOwnSaveSystem = ['custom-css-manager'];
|
||||
|
||||
if (!in_array($activeComponent, $componentsWithOwnSaveSystem, true)):
|
||||
?>
|
||||
<!-- Botones Globales Save/Cancel -->
|
||||
<div class="d-flex justify-content-end gap-2 p-3 rounded border mt-4" style="background-color: #f8f9fa; border-color: #e9ecef !important;">
|
||||
<button type="button" class="btn btn-outline-secondary" id="cancelChanges">
|
||||
@@ -71,4 +77,5 @@ $group = $groupId && isset($groups[$groupId]) ? $groups[$groupId] : null;
|
||||
<?php echo esc_html__('Guardar Cambios', 'roi-theme'); ?>
|
||||
</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
@@ -26,10 +26,22 @@ final class NavbarFieldMapper implements FieldMapperInterface
|
||||
'navbarEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'navbarShowMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'navbarShowDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'navbarShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
'navbarSticky' => ['group' => 'visibility', 'attribute' => 'sticky_enabled'],
|
||||
'navbarIsCritical' => ['group' => 'visibility', 'attribute' => 'is_critical'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'navbarVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'navbarVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'navbarVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'navbarVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'navbarVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'navbarExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'navbarExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'navbarExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'navbarExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Layout
|
||||
'navbarContainerType' => ['group' => 'layout', 'attribute' => 'container_type'],
|
||||
'navbarPaddingVertical' => ['group' => 'layout', 'attribute' => 'padding_vertical'],
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace ROITheme\Admin\Navbar\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
final class NavbarFormBuilder
|
||||
{
|
||||
@@ -105,18 +106,47 @@ final class NavbarFormBuilder
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Select: Show on Pages
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="navbarShowOnPages" class="form-label small mb-1 fw-semibold">Mostrar en</label>';
|
||||
$html .= ' <select id="navbarShowOnPages" name="visibility[show_on_pages]" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las páginas</option>';
|
||||
$html .= ' <option value="home" ' . selected($showOnPages, 'home', false) . '>Solo página de inicio</option>';
|
||||
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts individuales</option>';
|
||||
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo páginas</option>';
|
||||
$html .= ' </select>';
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', true);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', true);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('navbarVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('navbarVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('navbarVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('navbarVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('navbarVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'navbar');
|
||||
|
||||
// Switch: Sticky
|
||||
$sticky = $this->renderer->getFieldValue($componentId, 'visibility', 'sticky_enabled', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
@@ -527,4 +557,26 @@ final class NavbarFormBuilder
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\PostGrid\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para Post Grid
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*/
|
||||
final class PostGridFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'post-grid';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Visibility
|
||||
'postGridEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'postGridShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'postGridShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'postGridVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'postGridVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'postGridVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'postGridVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'postGridVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions)
|
||||
'postGridExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'postGridExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'postGridExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'postGridExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'postGridShowThumbnail' => ['group' => 'content', 'attribute' => 'show_thumbnail'],
|
||||
'postGridShowExcerpt' => ['group' => 'content', 'attribute' => 'show_excerpt'],
|
||||
'postGridShowMeta' => ['group' => 'content', 'attribute' => 'show_meta'],
|
||||
'postGridShowCategories' => ['group' => 'content', 'attribute' => 'show_categories'],
|
||||
'postGridExcerptLength' => ['group' => 'content', 'attribute' => 'excerpt_length'],
|
||||
'postGridReadMoreText' => ['group' => 'content', 'attribute' => 'read_more_text'],
|
||||
'postGridNoPostsMessage' => ['group' => 'content', 'attribute' => 'no_posts_message'],
|
||||
|
||||
// Layout
|
||||
'postGridColumnsDesktop' => ['group' => 'layout', 'attribute' => 'columns_desktop'],
|
||||
'postGridColumnsTablet' => ['group' => 'layout', 'attribute' => 'columns_tablet'],
|
||||
'postGridColumnsMobile' => ['group' => 'layout', 'attribute' => 'columns_mobile'],
|
||||
'postGridImagePosition' => ['group' => 'layout', 'attribute' => 'image_position'],
|
||||
|
||||
// Media
|
||||
'postGridFallbackImage' => ['group' => 'media', 'attribute' => 'fallback_image'],
|
||||
'postGridFallbackImageAlt' => ['group' => 'media', 'attribute' => 'fallback_image_alt'],
|
||||
|
||||
// Typography
|
||||
'postGridHeadingLevel' => ['group' => 'typography', 'attribute' => 'heading_level'],
|
||||
'postGridCardTitleSize' => ['group' => 'typography', 'attribute' => 'card_title_size'],
|
||||
'postGridCardTitleWeight' => ['group' => 'typography', 'attribute' => 'card_title_weight'],
|
||||
'postGridExcerptSize' => ['group' => 'typography', 'attribute' => 'excerpt_size'],
|
||||
'postGridMetaSize' => ['group' => 'typography', 'attribute' => 'meta_size'],
|
||||
|
||||
// Colors
|
||||
'postGridCardBgColor' => ['group' => 'colors', 'attribute' => 'card_bg_color'],
|
||||
'postGridCardTitleColor' => ['group' => 'colors', 'attribute' => 'card_title_color'],
|
||||
'postGridCardHoverBgColor' => ['group' => 'colors', 'attribute' => 'card_hover_bg_color'],
|
||||
'postGridCardBorderColor' => ['group' => 'colors', 'attribute' => 'card_border_color'],
|
||||
'postGridCardHoverBorderColor' => ['group' => 'colors', 'attribute' => 'card_hover_border_color'],
|
||||
'postGridExcerptColor' => ['group' => 'colors', 'attribute' => 'excerpt_color'],
|
||||
'postGridMetaColor' => ['group' => 'colors', 'attribute' => 'meta_color'],
|
||||
'postGridCategoryBgColor' => ['group' => 'colors', 'attribute' => 'category_bg_color'],
|
||||
'postGridCategoryTextColor' => ['group' => 'colors', 'attribute' => 'category_text_color'],
|
||||
'postGridPaginationColor' => ['group' => 'colors', 'attribute' => 'pagination_color'],
|
||||
'postGridPaginationActiveBg' => ['group' => 'colors', 'attribute' => 'pagination_active_bg'],
|
||||
'postGridPaginationActiveColor' => ['group' => 'colors', 'attribute' => 'pagination_active_color'],
|
||||
|
||||
// Spacing
|
||||
'postGridGapHorizontal' => ['group' => 'spacing', 'attribute' => 'gap_horizontal'],
|
||||
'postGridGapVertical' => ['group' => 'spacing', 'attribute' => 'gap_vertical'],
|
||||
'postGridCardPadding' => ['group' => 'spacing', 'attribute' => 'card_padding'],
|
||||
'postGridSectionMarginTop' => ['group' => 'spacing', 'attribute' => 'section_margin_top'],
|
||||
'postGridSectionMarginBottom' => ['group' => 'spacing', 'attribute' => 'section_margin_bottom'],
|
||||
|
||||
// Visual Effects
|
||||
'postGridCardBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'card_border_radius'],
|
||||
'postGridCardShadow' => ['group' => 'visual_effects', 'attribute' => 'card_shadow'],
|
||||
'postGridCardHoverShadow' => ['group' => 'visual_effects', 'attribute' => 'card_hover_shadow'],
|
||||
'postGridCardTransition' => ['group' => 'visual_effects', 'attribute' => 'card_transition'],
|
||||
'postGridImageBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'image_border_radius'],
|
||||
];
|
||||
}
|
||||
}
|
||||
781
Admin/PostGrid/Infrastructure/Ui/PostGridFormBuilder.php
Normal file
781
Admin/PostGrid/Infrastructure/Ui/PostGridFormBuilder.php
Normal file
@@ -0,0 +1,781 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\PostGrid\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* PostGridFormBuilder - Genera formulario admin para Post Grid
|
||||
*
|
||||
* Sigue el mismo patron visual que RelatedPostFormBuilder:
|
||||
* - Header con gradiente navy
|
||||
* - Layout de 2 columnas
|
||||
* - Cards con borde izquierdo
|
||||
* - Inputs compactos (form-control-sm)
|
||||
*
|
||||
* @package ROITheme\Admin\PostGrid\Infrastructure\Ui
|
||||
*/
|
||||
final class PostGridFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
$html .= $this->buildHeader();
|
||||
|
||||
$html .= '<div class="row g-3">';
|
||||
|
||||
// Columna izquierda
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildShortcodeGuide();
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildContentGroup($componentId);
|
||||
$html .= $this->buildMediaGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
// Columna derecha
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildLayoutGroup($componentId);
|
||||
$html .= $this->buildTypographyGroup($componentId);
|
||||
$html .= $this->buildColorsGroup($componentId);
|
||||
$html .= $this->buildSpacingGroup($componentId);
|
||||
$html .= $this->buildEffectsGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildHeader(): string
|
||||
{
|
||||
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
|
||||
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||
$html .= ' <div>';
|
||||
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||
$html .= ' <i class="bi bi-grid-3x3-gap me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuracion de Post Grid';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Grid de posts para listados, archivos y resultados de busqueda';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="post-grid">';
|
||||
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
|
||||
$html .= ' Restaurar valores por defecto';
|
||||
$html .= ' </button>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisibilityGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= $this->buildSwitch('postGridEnabled', 'Activar componente', 'bi-power', $enabled);
|
||||
|
||||
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= $this->buildSwitch('postGridShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop);
|
||||
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= $this->buildSwitch('postGridShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', false);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', false);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', true);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', true);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('postGridVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('postGridVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('postGridVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('postGridVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('postGridVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Reglas de exclusion
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'postGrid');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-card-text me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Contenido';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Switches de contenido
|
||||
$showThumbnail = $this->renderer->getFieldValue($componentId, 'content', 'show_thumbnail', true);
|
||||
$html .= $this->buildSwitch('postGridShowThumbnail', 'Mostrar imagen destacada', 'bi-image', $showThumbnail);
|
||||
|
||||
$showExcerpt = $this->renderer->getFieldValue($componentId, 'content', 'show_excerpt', true);
|
||||
$html .= $this->buildSwitch('postGridShowExcerpt', 'Mostrar extracto', 'bi-text-paragraph', $showExcerpt);
|
||||
|
||||
$showMeta = $this->renderer->getFieldValue($componentId, 'content', 'show_meta', true);
|
||||
$html .= $this->buildSwitch('postGridShowMeta', 'Mostrar metadatos', 'bi-info-circle', $showMeta);
|
||||
|
||||
$showCategories = $this->renderer->getFieldValue($componentId, 'content', 'show_categories', true);
|
||||
$html .= $this->buildSwitch('postGridShowCategories', 'Mostrar categorias', 'bi-folder', $showCategories);
|
||||
|
||||
$html .= ' <hr class="my-3">';
|
||||
|
||||
// Excerpt length
|
||||
$excerptLength = $this->renderer->getFieldValue($componentId, 'content', 'excerpt_length', '20');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="postGridExcerptLength" class="form-label small mb-1 fw-semibold">Longitud del extracto</label>';
|
||||
$html .= ' <select id="postGridExcerptLength" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="10"' . ($excerptLength === '10' ? ' selected' : '') . '>10 palabras</option>';
|
||||
$html .= ' <option value="15"' . ($excerptLength === '15' ? ' selected' : '') . '>15 palabras</option>';
|
||||
$html .= ' <option value="20"' . ($excerptLength === '20' ? ' selected' : '') . '>20 palabras</option>';
|
||||
$html .= ' <option value="25"' . ($excerptLength === '25' ? ' selected' : '') . '>25 palabras</option>';
|
||||
$html .= ' <option value="30"' . ($excerptLength === '30' ? ' selected' : '') . '>30 palabras</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Read more text
|
||||
$readMoreText = $this->renderer->getFieldValue($componentId, 'content', 'read_more_text', 'Leer mas');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="postGridReadMoreText" class="form-label small mb-1 fw-semibold">Texto de leer mas</label>';
|
||||
$html .= ' <input type="text" id="postGridReadMoreText" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($readMoreText) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// No posts message
|
||||
$noPostsMessage = $this->renderer->getFieldValue($componentId, 'content', 'no_posts_message', 'No se encontraron publicaciones');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="postGridNoPostsMessage" class="form-label small mb-1 fw-semibold">Mensaje sin posts</label>';
|
||||
$html .= ' <input type="text" id="postGridNoPostsMessage" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($noPostsMessage) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildLayoutGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-grid me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Disposicion';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Columns desktop
|
||||
$colsDesktop = $this->renderer->getFieldValue($componentId, 'layout', 'columns_desktop', '3');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="postGridColumnsDesktop" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-display me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Columnas escritorio';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="postGridColumnsDesktop" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="2"' . ($colsDesktop === '2' ? ' selected' : '') . '>2 columnas</option>';
|
||||
$html .= ' <option value="3"' . ($colsDesktop === '3' ? ' selected' : '') . '>3 columnas</option>';
|
||||
$html .= ' <option value="4"' . ($colsDesktop === '4' ? ' selected' : '') . '>4 columnas</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Columns tablet
|
||||
$colsTablet = $this->renderer->getFieldValue($componentId, 'layout', 'columns_tablet', '2');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="postGridColumnsTablet" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-tablet me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Columnas tablet';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="postGridColumnsTablet" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="1"' . ($colsTablet === '1' ? ' selected' : '') . '>1 columna</option>';
|
||||
$html .= ' <option value="2"' . ($colsTablet === '2' ? ' selected' : '') . '>2 columnas</option>';
|
||||
$html .= ' <option value="3"' . ($colsTablet === '3' ? ' selected' : '') . '>3 columnas</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Columns mobile
|
||||
$colsMobile = $this->renderer->getFieldValue($componentId, 'layout', 'columns_mobile', '1');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="postGridColumnsMobile" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-phone me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Columnas movil';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="postGridColumnsMobile" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="1"' . ($colsMobile === '1' ? ' selected' : '') . '>1 columna</option>';
|
||||
$html .= ' <option value="2"' . ($colsMobile === '2' ? ' selected' : '') . '>2 columnas</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Image position
|
||||
$imagePosition = $this->renderer->getFieldValue($componentId, 'layout', 'image_position', 'top');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="postGridImagePosition" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-aspect-ratio me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Posicion de imagen';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="postGridImagePosition" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="top"' . ($imagePosition === 'top' ? ' selected' : '') . '>Arriba</option>';
|
||||
$html .= ' <option value="left"' . ($imagePosition === 'left' ? ' selected' : '') . '>Izquierda</option>';
|
||||
$html .= ' <option value="none"' . ($imagePosition === 'none' ? ' selected' : '') . '>Sin imagen</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildMediaGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-image me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Medios';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Fallback image
|
||||
$fallbackImage = $this->renderer->getFieldValue($componentId, 'media', 'fallback_image', '');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="postGridFallbackImage" class="form-label small mb-1 fw-semibold">URL imagen por defecto</label>';
|
||||
$html .= ' <input type="url" id="postGridFallbackImage" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_url($fallbackImage) . '" placeholder="https://...">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Fallback image alt
|
||||
$fallbackImageAlt = $this->renderer->getFieldValue($componentId, 'media', 'fallback_image_alt', 'Imagen por defecto');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="postGridFallbackImageAlt" class="form-label small mb-1 fw-semibold">Texto alternativo</label>';
|
||||
$html .= ' <input type="text" id="postGridFallbackImageAlt" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($fallbackImageAlt) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTypographyGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-fonts me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Tipografia';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Heading level
|
||||
$headingLevel = $this->renderer->getFieldValue($componentId, 'typography', 'heading_level', 'h3');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="postGridHeadingLevel" class="form-label small mb-1 fw-semibold">Nivel de encabezado</label>';
|
||||
$html .= ' <select id="postGridHeadingLevel" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="h2"' . ($headingLevel === 'h2' ? ' selected' : '') . '>H2</option>';
|
||||
$html .= ' <option value="h3"' . ($headingLevel === 'h3' ? ' selected' : '') . '>H3</option>';
|
||||
$html .= ' <option value="h4"' . ($headingLevel === 'h4' ? ' selected' : '') . '>H4</option>';
|
||||
$html .= ' <option value="h5"' . ($headingLevel === 'h5' ? ' selected' : '') . '>H5</option>';
|
||||
$html .= ' <option value="h6"' . ($headingLevel === 'h6' ? ' selected' : '') . '>H6</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$cardTitleSize = $this->renderer->getFieldValue($componentId, 'typography', 'card_title_size', '1.1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="postGridCardTitleSize" class="form-label small mb-1 fw-semibold">Tamano titulo</label>';
|
||||
$html .= ' <input type="text" id="postGridCardTitleSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($cardTitleSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$cardTitleWeight = $this->renderer->getFieldValue($componentId, 'typography', 'card_title_weight', '600');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="postGridCardTitleWeight" class="form-label small mb-1 fw-semibold">Peso titulo</label>';
|
||||
$html .= ' <input type="text" id="postGridCardTitleWeight" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($cardTitleWeight) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$excerptSize = $this->renderer->getFieldValue($componentId, 'typography', 'excerpt_size', '0.9rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="postGridExcerptSize" class="form-label small mb-1 fw-semibold">Tamano extracto</label>';
|
||||
$html .= ' <input type="text" id="postGridExcerptSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($excerptSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$metaSize = $this->renderer->getFieldValue($componentId, 'typography', 'meta_size', '0.8rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="postGridMetaSize" class="form-label small mb-1 fw-semibold">Tamano metadatos</label>';
|
||||
$html .= ' <input type="text" id="postGridMetaSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($metaSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Colores';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Cards
|
||||
$html .= ' <p class="small fw-semibold mb-2">Cards</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$cardBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'card_bg_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('postGridCardBgColor', 'Fondo', $cardBgColor);
|
||||
|
||||
$cardTitleColor = $this->renderer->getFieldValue($componentId, 'colors', 'card_title_color', '#0E2337');
|
||||
$html .= $this->buildColorPicker('postGridCardTitleColor', 'Titulo', $cardTitleColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$cardHoverBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'card_hover_bg_color', '#f9fafb');
|
||||
$html .= $this->buildColorPicker('postGridCardHoverBgColor', 'Fondo hover', $cardHoverBgColor);
|
||||
|
||||
$cardBorderColor = $this->renderer->getFieldValue($componentId, 'colors', 'card_border_color', '#e5e7eb');
|
||||
$html .= $this->buildColorPicker('postGridCardBorderColor', 'Borde', $cardBorderColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$cardHoverBorderColor = $this->renderer->getFieldValue($componentId, 'colors', 'card_hover_border_color', '#FF8600');
|
||||
$html .= $this->buildColorPicker('postGridCardHoverBorderColor', 'Borde hover', $cardHoverBorderColor);
|
||||
|
||||
$excerptColor = $this->renderer->getFieldValue($componentId, 'colors', 'excerpt_color', '#6b7280');
|
||||
$html .= $this->buildColorPicker('postGridExcerptColor', 'Extracto', $excerptColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$metaColor = $this->renderer->getFieldValue($componentId, 'colors', 'meta_color', '#9ca3af');
|
||||
$html .= $this->buildColorPicker('postGridMetaColor', 'Metadatos', $metaColor);
|
||||
|
||||
$categoryBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'category_bg_color', '#FFF5EB');
|
||||
$html .= $this->buildColorPicker('postGridCategoryBgColor', 'Fondo cat.', $categoryBgColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$categoryTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'category_text_color', '#FF8600');
|
||||
$html .= $this->buildColorPicker('postGridCategoryTextColor', 'Texto cat.', $categoryTextColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Paginacion
|
||||
$html .= ' <p class="small fw-semibold mb-2">Paginacion</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$paginationColor = $this->renderer->getFieldValue($componentId, 'colors', 'pagination_color', '#0E2337');
|
||||
$html .= $this->buildColorPicker('postGridPaginationColor', 'Color', $paginationColor);
|
||||
|
||||
$paginationActiveBg = $this->renderer->getFieldValue($componentId, 'colors', 'pagination_active_bg', '#FF8600');
|
||||
$html .= $this->buildColorPicker('postGridPaginationActiveBg', 'Activo fondo', $paginationActiveBg);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$paginationActiveColor = $this->renderer->getFieldValue($componentId, 'colors', 'pagination_active_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('postGridPaginationActiveColor', 'Activo texto', $paginationActiveColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSpacingGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-arrows-move me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Separación entre cards
|
||||
$html .= ' <p class="small text-muted mb-2">Separacion entre cards:</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// Gap horizontal (entre columnas)
|
||||
$gapHorizontal = $this->renderer->getFieldValue($componentId, 'spacing', 'gap_horizontal', '24px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="postGridGapHorizontal" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-arrows-expand me-1" style="color: #FF8600;"></i>Horizontal';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="postGridGapHorizontal" class="form-select form-select-sm">';
|
||||
$gapOptions = ['0px', '8px', '12px', '16px', '20px', '24px', '32px', '40px', '48px'];
|
||||
foreach ($gapOptions as $opt) {
|
||||
$selected = ($gapHorizontal === $opt) ? ' selected' : '';
|
||||
$html .= ' <option value="' . $opt . '"' . $selected . '>' . $opt . '</option>';
|
||||
}
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Gap vertical (entre filas)
|
||||
$gapVertical = $this->renderer->getFieldValue($componentId, 'spacing', 'gap_vertical', '24px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="postGridGapVertical" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-arrows-collapse me-1" style="color: #FF8600;"></i>Vertical';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="postGridGapVertical" class="form-select form-select-sm">';
|
||||
foreach ($gapOptions as $opt) {
|
||||
$selected = ($gapVertical === $opt) ? ' selected' : '';
|
||||
$html .= ' <option value="' . $opt . '"' . $selected . '>' . $opt . '</option>';
|
||||
}
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Padding interno de cada card
|
||||
$html .= ' <p class="small text-muted mb-2">Padding interno de card:</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$cardPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'card_padding', '20px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="postGridCardPadding" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-box me-1" style="color: #FF8600;"></i>Padding';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="postGridCardPadding" class="form-select form-select-sm">';
|
||||
$paddingOptions = ['0px', '8px', '12px', '16px', '20px', '24px', '32px'];
|
||||
foreach ($paddingOptions as $opt) {
|
||||
$selected = ($cardPadding === $opt) ? ' selected' : '';
|
||||
$html .= ' <option value="' . $opt . '"' . $selected . '>' . $opt . '</option>';
|
||||
}
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-6"></div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Margenes de la seccion
|
||||
$html .= ' <p class="small text-muted mb-2">Margenes de la seccion:</p>';
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$sectionMarginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'section_margin_top', '0px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="postGridSectionMarginTop" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-arrow-up me-1" style="color: #FF8600;"></i>Arriba';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="postGridSectionMarginTop" class="form-select form-select-sm">';
|
||||
$marginOptions = ['0px', '8px', '16px', '24px', '32px', '48px', '64px'];
|
||||
foreach ($marginOptions as $opt) {
|
||||
$selected = ($sectionMarginTop === $opt) ? ' selected' : '';
|
||||
$html .= ' <option value="' . $opt . '"' . $selected . '>' . $opt . '</option>';
|
||||
}
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$sectionMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'section_margin_bottom', '32px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="postGridSectionMarginBottom" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-arrow-down me-1" style="color: #FF8600;"></i>Abajo';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="postGridSectionMarginBottom" class="form-select form-select-sm">';
|
||||
foreach ($marginOptions as $opt) {
|
||||
$selected = ($sectionMarginBottom === $opt) ? ' selected' : '';
|
||||
$html .= ' <option value="' . $opt . '"' . $selected . '>' . $opt . '</option>';
|
||||
}
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildEffectsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-magic me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Efectos Visuales';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$cardBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_border_radius', '0.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="postGridCardBorderRadius" class="form-label small mb-1 fw-semibold">Radio borde</label>';
|
||||
$html .= ' <input type="text" id="postGridCardBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($cardBorderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$imageBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'image_border_radius', '0.375rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="postGridImageBorderRadius" class="form-label small mb-1 fw-semibold">Radio imagen</label>';
|
||||
$html .= ' <input type="text" id="postGridImageBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($imageBorderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="mb-3">';
|
||||
$cardTransition = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_transition', 'all 0.3s ease');
|
||||
$html .= ' <label for="postGridCardTransition" class="form-label small mb-1 fw-semibold">Transicion</label>';
|
||||
$html .= ' <input type="text" id="postGridCardTransition" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($cardTransition) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="mb-3">';
|
||||
$cardShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_shadow', '0 1px 3px rgba(0,0,0,0.1)');
|
||||
$html .= ' <label for="postGridCardShadow" class="form-label small mb-1 fw-semibold">Sombra normal</label>';
|
||||
$html .= ' <input type="text" id="postGridCardShadow" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($cardShadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="mb-0">';
|
||||
$cardHoverShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_hover_shadow', '0 4px 12px rgba(0,0,0,0.15)');
|
||||
$html .= ' <label for="postGridCardHoverShadow" class="form-label small mb-1 fw-semibold">Sombra hover</label>';
|
||||
$html .= ' <input type="text" id="postGridCardHoverShadow" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($cardHoverShadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSwitch(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' <strong>%s</strong>', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorPicker(string $id, string $label, string $value): string
|
||||
{
|
||||
$html = ' <div class="col-6">';
|
||||
$html .= sprintf(
|
||||
' <label class="form-label small fw-semibold">%s</label>',
|
||||
esc_html($label)
|
||||
);
|
||||
$html .= ' <div class="input-group input-group-sm">';
|
||||
$html .= sprintf(
|
||||
' <input type="color" class="form-control form-control-color" id="%s" value="%s">',
|
||||
esc_attr($id),
|
||||
esc_attr($value)
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <span class="input-group-text" id="%sValue">%s</span>',
|
||||
esc_attr($id),
|
||||
esc_html(strtoupper($value))
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildShortcodeGuide(): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-code-square me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Shortcode [roi_post_grid]';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <p class="small text-muted mb-3">';
|
||||
$html .= ' Usa este shortcode para insertar grids de posts en cualquier pagina o entrada. ';
|
||||
$html .= ' Los estilos se heredan de la configuracion de este componente.';
|
||||
$html .= ' </p>';
|
||||
|
||||
// Uso basico
|
||||
$html .= ' <p class="small fw-semibold mb-1">';
|
||||
$html .= ' <i class="bi bi-1-circle me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Uso basico (9 posts, 3 columnas)';
|
||||
$html .= ' </p>';
|
||||
$html .= ' <div class="bg-dark text-light rounded p-2 mb-3" style="font-family: monospace; font-size: 0.8rem;">';
|
||||
$html .= ' <code class="text-warning">[roi_post_grid]</code>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Por categoria
|
||||
$html .= ' <p class="small fw-semibold mb-1">';
|
||||
$html .= ' <i class="bi bi-2-circle me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Filtrar por categoria';
|
||||
$html .= ' </p>';
|
||||
$html .= ' <div class="bg-dark text-light rounded p-2 mb-3" style="font-family: monospace; font-size: 0.8rem;">';
|
||||
$html .= ' <code class="text-warning">[roi_post_grid category="precios-unitarios"]</code>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Personalizar cantidad y columnas
|
||||
$html .= ' <p class="small fw-semibold mb-1">';
|
||||
$html .= ' <i class="bi bi-3-circle me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' 6 posts en 2 columnas';
|
||||
$html .= ' </p>';
|
||||
$html .= ' <div class="bg-dark text-light rounded p-2 mb-3" style="font-family: monospace; font-size: 0.8rem;">';
|
||||
$html .= ' <code class="text-warning">[roi_post_grid posts_per_page="6" columns="2"]</code>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Con paginacion
|
||||
$html .= ' <p class="small fw-semibold mb-1">';
|
||||
$html .= ' <i class="bi bi-4-circle me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Con paginacion';
|
||||
$html .= ' </p>';
|
||||
$html .= ' <div class="bg-dark text-light rounded p-2 mb-3" style="font-family: monospace; font-size: 0.8rem;">';
|
||||
$html .= ' <code class="text-warning">[roi_post_grid posts_per_page="12" show_pagination="true"]</code>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Filtrar por tag
|
||||
$html .= ' <p class="small fw-semibold mb-1">';
|
||||
$html .= ' <i class="bi bi-5-circle me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Filtrar por etiqueta';
|
||||
$html .= ' </p>';
|
||||
$html .= ' <div class="bg-dark text-light rounded p-2 mb-3" style="font-family: monospace; font-size: 0.8rem;">';
|
||||
$html .= ' <code class="text-warning">[roi_post_grid tag="tutorial"]</code>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Ejemplo completo
|
||||
$html .= ' <p class="small fw-semibold mb-1">';
|
||||
$html .= ' <i class="bi bi-6-circle me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Ejemplo completo';
|
||||
$html .= ' </p>';
|
||||
$html .= ' <div class="bg-dark text-light rounded p-2 mb-3" style="font-family: monospace; font-size: 0.75rem;">';
|
||||
$html .= ' <code class="text-warning">[roi_post_grid category="cursos" posts_per_page="6" columns="3" show_meta="false" show_categories="true"]</code>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Tabla de atributos
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-list-check me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Atributos disponibles';
|
||||
$html .= ' </p>';
|
||||
$html .= ' <div class="table-responsive">';
|
||||
$html .= ' <table class="table table-sm table-bordered small mb-0">';
|
||||
$html .= ' <thead class="table-light">';
|
||||
$html .= ' <tr><th>Atributo</th><th>Default</th><th>Descripcion</th></tr>';
|
||||
$html .= ' </thead>';
|
||||
$html .= ' <tbody>';
|
||||
$html .= ' <tr><td><code>posts_per_page</code></td><td>9</td><td>Cantidad de posts</td></tr>';
|
||||
$html .= ' <tr><td><code>columns</code></td><td>3</td><td>Columnas (1-4)</td></tr>';
|
||||
$html .= ' <tr><td><code>category</code></td><td>-</td><td>Slug de categoria</td></tr>';
|
||||
$html .= ' <tr><td><code>exclude_category</code></td><td>-</td><td>Excluir categoria</td></tr>';
|
||||
$html .= ' <tr><td><code>tag</code></td><td>-</td><td>Slug de etiqueta</td></tr>';
|
||||
$html .= ' <tr><td><code>author</code></td><td>-</td><td>ID o username</td></tr>';
|
||||
$html .= ' <tr><td><code>orderby</code></td><td>date</td><td>date, title, rand</td></tr>';
|
||||
$html .= ' <tr><td><code>order</code></td><td>DESC</td><td>DESC o ASC</td></tr>';
|
||||
$html .= ' <tr><td><code>show_pagination</code></td><td>false</td><td>Mostrar paginacion</td></tr>';
|
||||
$html .= ' <tr><td><code>show_thumbnail</code></td><td>true</td><td>Mostrar imagen</td></tr>';
|
||||
$html .= ' <tr><td><code>show_excerpt</code></td><td>true</td><td>Mostrar extracto</td></tr>';
|
||||
$html .= ' <tr><td><code>show_meta</code></td><td>true</td><td>Fecha y autor</td></tr>';
|
||||
$html .= ' <tr><td><code>show_categories</code></td><td>true</td><td>Badges categoria</td></tr>';
|
||||
$html .= ' <tr><td><code>excerpt_length</code></td><td>20</td><td>Palabras extracto</td></tr>';
|
||||
$html .= ' <tr><td><code>exclude_posts</code></td><td>-</td><td>IDs separados por coma</td></tr>';
|
||||
$html .= ' <tr><td><code>offset</code></td><td>0</td><td>Saltar N posts</td></tr>';
|
||||
$html .= ' <tr><td><code>id</code></td><td>-</td><td>ID unico (multiples grids)</td></tr>';
|
||||
$html .= ' <tr><td><code>class</code></td><td>-</td><td>Clase CSS adicional</td></tr>';
|
||||
$html .= ' </tbody>';
|
||||
$html .= ' </table>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,19 @@ final class RelatedPostFieldMapper implements FieldMapperInterface
|
||||
'relatedPostEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'relatedPostShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'relatedPostShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'relatedPostShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'relatedPostVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'relatedPostVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'relatedPostVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'relatedPostVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'relatedPostVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'relatedPostExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'relatedPostExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'relatedPostExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'relatedPostExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'relatedPostSectionTitle' => ['group' => 'content', 'attribute' => 'section_title'],
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace ROITheme\Admin\RelatedPost\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* FormBuilder para Related Posts
|
||||
@@ -86,19 +87,47 @@ final class RelatedPostFormBuilder
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= $this->buildSwitch('relatedPostShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <label for="relatedPostShowOnPages" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="relatedPostShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all"' . ($showOnPages === 'all' ? ' selected' : '') . '>Todos</option>';
|
||||
$html .= ' <option value="posts"' . ($showOnPages === 'posts' ? ' selected' : '') . '>Solo posts</option>';
|
||||
$html .= ' <option value="pages"' . ($showOnPages === 'pages' ? ' selected' : '') . '>Solo paginas</option>';
|
||||
$html .= ' </select>';
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'relatedPost');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
@@ -498,4 +527,26 @@ final class RelatedPostFormBuilder
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace ROITheme\Admin\Shared\Infrastructure\Api\WordPress;
|
||||
|
||||
use ROITheme\Shared\Application\UseCases\SaveComponentSettings\SaveComponentSettingsUseCase;
|
||||
use ROITheme\Admin\Shared\Infrastructure\FieldMapping\FieldMapperRegistry;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Services\ExclusionFieldProcessor;
|
||||
|
||||
/**
|
||||
* Handler para peticiones AJAX del panel de administracion
|
||||
@@ -73,10 +74,16 @@ final class AdminAjaxHandler
|
||||
|
||||
/**
|
||||
* Mapea settings de field IDs a grupos/atributos
|
||||
*
|
||||
* Soporta tipos especiales para campos de exclusion:
|
||||
* - json_array: Convierte "a, b, c" a ["a", "b", "c"]
|
||||
* - json_array_int: Convierte "1, 2, 3" a [1, 2, 3]
|
||||
* - json_array_lines: Convierte lineas a array
|
||||
*/
|
||||
private function mapSettings(array $settings, array $fieldMapping): array
|
||||
{
|
||||
$mappedSettings = [];
|
||||
$fieldProcessor = new ExclusionFieldProcessor();
|
||||
|
||||
foreach ($settings as $fieldId => $value) {
|
||||
if (!isset($fieldMapping[$fieldId])) {
|
||||
@@ -86,11 +93,17 @@ final class AdminAjaxHandler
|
||||
$mapping = $fieldMapping[$fieldId];
|
||||
$groupName = $mapping['group'];
|
||||
$attributeName = $mapping['attribute'];
|
||||
$type = $mapping['type'] ?? null;
|
||||
|
||||
if (!isset($mappedSettings[$groupName])) {
|
||||
$mappedSettings[$groupName] = [];
|
||||
}
|
||||
|
||||
// Procesar valor segun tipo
|
||||
if ($type !== null && is_string($value)) {
|
||||
$value = $fieldProcessor->process($value, $type);
|
||||
}
|
||||
|
||||
$mappedSettings[$groupName][$attributeName] = $value;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@ final class FieldMapperProvider
|
||||
'Footer',
|
||||
'ThemeSettings',
|
||||
'AdsensePlacement',
|
||||
'ArchiveHeader',
|
||||
'PostGrid',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Shared\Infrastructure\Services;
|
||||
|
||||
/**
|
||||
* Servicio para procesar campos de exclusion antes de guardar en BD
|
||||
*
|
||||
* Convierte formatos de UI a JSON para almacenamiento.
|
||||
*
|
||||
* v1.1: Extraido de AdminAjaxHandler (SRP)
|
||||
*
|
||||
* @package ROITheme\Admin\Shared\Infrastructure\Services
|
||||
*/
|
||||
final class ExclusionFieldProcessor
|
||||
{
|
||||
/**
|
||||
* Procesa un valor de campo de exclusion segun su tipo
|
||||
*
|
||||
* @param string $value Valor del campo (desde UI)
|
||||
* @param string $type Tipo de campo: json_array, json_array_int, json_array_lines
|
||||
* @return string JSON string para almacenar en BD
|
||||
*/
|
||||
public function process(string $value, string $type): string
|
||||
{
|
||||
return match ($type) {
|
||||
'json_array' => $this->processJsonArray($value),
|
||||
'json_array_int' => $this->processJsonArrayInt($value),
|
||||
'json_array_lines' => $this->processJsonArrayLines($value),
|
||||
default => $value,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* "a, b, c" -> ["a", "b", "c"]
|
||||
*/
|
||||
private function processJsonArray(string $value): string
|
||||
{
|
||||
$items = array_map('trim', explode(',', $value));
|
||||
$items = array_filter($items, fn($item) => $item !== '');
|
||||
return json_encode(array_values($items), JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
/**
|
||||
* "1, 2, 3" -> [1, 2, 3]
|
||||
*/
|
||||
private function processJsonArrayInt(string $value): string
|
||||
{
|
||||
$items = array_map('trim', explode(',', $value));
|
||||
$items = array_filter($items, 'is_numeric');
|
||||
$items = array_map('intval', $items);
|
||||
return json_encode(array_values($items));
|
||||
}
|
||||
|
||||
/**
|
||||
* Lineas separadas -> array
|
||||
*/
|
||||
private function processJsonArrayLines(string $value): string
|
||||
{
|
||||
$items = preg_split('/\r\n|\r|\n/', $value);
|
||||
$items = array_map('trim', $items);
|
||||
$items = array_filter($items, fn($item) => $item !== '');
|
||||
return json_encode(array_values($items), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
}
|
||||
31
Admin/Shared/Infrastructure/Ui/Assets/Js/exclusion-toggle.js
Normal file
31
Admin/Shared/Infrastructure/Ui/Assets/Js/exclusion-toggle.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Toggle para mostrar/ocultar reglas de exclusion en FormBuilders
|
||||
*
|
||||
* Escucha cambios en checkboxes con ID que termine en "ExclusionsEnabled"
|
||||
* y muestra/oculta el contenedor de reglas correspondiente.
|
||||
*
|
||||
* @package ROITheme\Admin
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
function initExclusionToggles() {
|
||||
document.querySelectorAll('[id$="ExclusionsEnabled"]').forEach(function(checkbox) {
|
||||
// Handler para cambios
|
||||
checkbox.addEventListener('change', function() {
|
||||
const prefix = this.id.replace('ExclusionsEnabled', '');
|
||||
const rulesContainer = document.getElementById(prefix + 'ExclusionRules');
|
||||
if (rulesContainer) {
|
||||
rulesContainer.style.display = this.checked ? 'block' : 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Inicializar cuando DOM este listo
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initExclusionToggles);
|
||||
} else {
|
||||
initExclusionToggles();
|
||||
}
|
||||
})();
|
||||
260
Admin/Shared/Infrastructure/Ui/ExclusionFormPartial.php
Normal file
260
Admin/Shared/Infrastructure/Ui/ExclusionFormPartial.php
Normal file
@@ -0,0 +1,260 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Shared\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
|
||||
/**
|
||||
* Componente UI parcial reutilizable para reglas de exclusion
|
||||
*
|
||||
* Genera el HTML para la seccion de exclusiones en FormBuilders.
|
||||
* Debe ser incluido despues de la seccion de visibilidad por tipo de pagina.
|
||||
*
|
||||
* Uso en FormBuilder:
|
||||
* ```php
|
||||
* $exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
* $html .= $exclusionPartial->render($componentId, 'prefijo');
|
||||
* ```
|
||||
*
|
||||
* @package ROITheme\Admin\Shared\Infrastructure\Ui
|
||||
*/
|
||||
final class ExclusionFormPartial
|
||||
{
|
||||
private const GROUP_NAME = '_exclusions';
|
||||
|
||||
public function __construct(
|
||||
private readonly AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Renderiza la seccion de exclusiones
|
||||
*
|
||||
* @param string $componentId ID del componente (kebab-case)
|
||||
* @param string $prefix Prefijo para IDs de campos (ej: 'cta' genera 'ctaExclusionsEnabled')
|
||||
* @return string HTML de la seccion
|
||||
*/
|
||||
public function render(string $componentId, string $prefix): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
$html .= $this->buildExclusionHeader();
|
||||
$html .= $this->buildExclusionToggle($componentId, $prefix);
|
||||
$html .= $this->buildExclusionRules($componentId, $prefix);
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildExclusionHeader(): string
|
||||
{
|
||||
$html = '<hr class="my-3">';
|
||||
$html .= '<p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-funnel me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Reglas de exclusion avanzadas';
|
||||
$html .= '</p>';
|
||||
$html .= '<p class="small text-muted mb-2">';
|
||||
$html .= ' Excluir este componente de categorias, posts o URLs especificos.';
|
||||
$html .= '</p>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildExclusionToggle(string $componentId, string $prefix): string
|
||||
{
|
||||
$enabled = $this->renderer->getFieldValue(
|
||||
$componentId,
|
||||
self::GROUP_NAME,
|
||||
'exclusions_enabled',
|
||||
false
|
||||
);
|
||||
$checked = $this->toBool($enabled);
|
||||
|
||||
$id = $prefix . 'ExclusionsEnabled';
|
||||
|
||||
$html = '<div class="mb-3">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= ' <i class="bi bi-filter-circle me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Activar reglas de exclusion</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildExclusionRules(string $componentId, string $prefix): string
|
||||
{
|
||||
$enabled = $this->renderer->getFieldValue(
|
||||
$componentId,
|
||||
self::GROUP_NAME,
|
||||
'exclusions_enabled',
|
||||
false
|
||||
);
|
||||
$display = $this->toBool($enabled) ? 'block' : 'none';
|
||||
|
||||
$html = sprintf(
|
||||
'<div id="%sExclusionRules" style="display: %s;">',
|
||||
esc_attr($prefix),
|
||||
$display
|
||||
);
|
||||
|
||||
$html .= $this->buildCategoryField($componentId, $prefix);
|
||||
$html .= $this->buildPostIdsField($componentId, $prefix);
|
||||
$html .= $this->buildUrlPatternsField($componentId, $prefix);
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildCategoryField(string $componentId, string $prefix): string
|
||||
{
|
||||
$value = $this->renderer->getFieldValue(
|
||||
$componentId,
|
||||
self::GROUP_NAME,
|
||||
'exclude_categories',
|
||||
'[]'
|
||||
);
|
||||
$categories = $this->jsonToCommaList($value);
|
||||
|
||||
$id = $prefix . 'ExcludeCategories';
|
||||
|
||||
$html = '<div class="mb-3">';
|
||||
$html .= sprintf(
|
||||
' <label for="%s" class="form-label small mb-1 fw-semibold">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= ' <i class="bi bi-folder me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Excluir en categorias';
|
||||
$html .= ' </label>';
|
||||
$html .= sprintf(
|
||||
' <input type="text" id="%s" class="form-control form-control-sm" value="%s" placeholder="noticias, eventos, tutoriales">',
|
||||
esc_attr($id),
|
||||
esc_attr($categories)
|
||||
);
|
||||
$html .= ' <small class="text-muted">Slugs de categorias separados por comas</small>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPostIdsField(string $componentId, string $prefix): string
|
||||
{
|
||||
$value = $this->renderer->getFieldValue(
|
||||
$componentId,
|
||||
self::GROUP_NAME,
|
||||
'exclude_post_ids',
|
||||
'[]'
|
||||
);
|
||||
$postIds = $this->jsonToCommaList($value);
|
||||
|
||||
$id = $prefix . 'ExcludePostIds';
|
||||
|
||||
$html = '<div class="mb-3">';
|
||||
$html .= sprintf(
|
||||
' <label for="%s" class="form-label small mb-1 fw-semibold">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= ' <i class="bi bi-hash me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Excluir en posts/paginas';
|
||||
$html .= ' </label>';
|
||||
$html .= sprintf(
|
||||
' <input type="text" id="%s" class="form-control form-control-sm" value="%s" placeholder="123, 456, 789">',
|
||||
esc_attr($id),
|
||||
esc_attr($postIds)
|
||||
);
|
||||
$html .= ' <small class="text-muted">IDs de posts o paginas separados por comas</small>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildUrlPatternsField(string $componentId, string $prefix): string
|
||||
{
|
||||
$value = $this->renderer->getFieldValue(
|
||||
$componentId,
|
||||
self::GROUP_NAME,
|
||||
'exclude_url_patterns',
|
||||
'[]'
|
||||
);
|
||||
$patterns = $this->jsonToLineList($value);
|
||||
|
||||
$id = $prefix . 'ExcludeUrlPatterns';
|
||||
|
||||
$html = '<div class="mb-0">';
|
||||
$html .= sprintf(
|
||||
' <label for="%s" class="form-label small mb-1 fw-semibold">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= ' <i class="bi bi-link-45deg me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Excluir por patrones URL';
|
||||
$html .= ' </label>';
|
||||
$html .= sprintf(
|
||||
' <textarea id="%s" class="form-control form-control-sm" rows="3" placeholder="/privado/ /landing-especial/ /^\/categoria\/\d+$/">%s</textarea>',
|
||||
esc_attr($id),
|
||||
esc_textarea($patterns)
|
||||
);
|
||||
$html .= ' <small class="text-muted">Un patron por linea. Soporta texto simple o regex (ej: /^\/blog\/\d+$/)</small>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convierte JSON array o array a lista separada por comas
|
||||
*
|
||||
* @param string|array $value Valor desde BD (puede ser JSON string o array ya deserializado)
|
||||
*/
|
||||
private function jsonToCommaList(string|array $value): string
|
||||
{
|
||||
// Si ya es array, usarlo directamente
|
||||
if (is_array($value)) {
|
||||
return empty($value) ? '' : implode(', ', $value);
|
||||
}
|
||||
|
||||
// Si es string, intentar decodificar JSON
|
||||
$decoded = json_decode($value, true);
|
||||
|
||||
if (!is_array($decoded) || empty($decoded)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return implode(', ', $decoded);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convierte JSON array o array a lista separada por lineas
|
||||
*
|
||||
* @param string|array $value Valor desde BD (puede ser JSON string o array ya deserializado)
|
||||
*/
|
||||
private function jsonToLineList(string|array $value): string
|
||||
{
|
||||
// Si ya es array, usarlo directamente
|
||||
if (is_array($value)) {
|
||||
return empty($value) ? '' : implode("\n", $value);
|
||||
}
|
||||
|
||||
// Si es string, intentar decodificar JSON
|
||||
$decoded = json_decode($value, true);
|
||||
|
||||
if (!is_array($decoded) || empty($decoded)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return implode("\n", $decoded);
|
||||
}
|
||||
|
||||
private function toBool(mixed $value): bool
|
||||
{
|
||||
return $value === true || $value === '1' || $value === 1;
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,19 @@ final class SocialShareFieldMapper implements FieldMapperInterface
|
||||
'socialShareEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'socialShareShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'socialShareShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'socialShareShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'socialShareVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'socialShareVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'socialShareVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'socialShareVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'socialShareVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'socialShareExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'socialShareExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'socialShareExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'socialShareExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'socialShareShowLabel' => ['group' => 'content', 'attribute' => 'show_label'],
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace ROITheme\Admin\SocialShare\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* FormBuilder para Social Share
|
||||
@@ -94,20 +95,47 @@ final class SocialShareFormBuilder
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= $this->buildSwitch('socialShareShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
// show_on_pages
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <label for="socialShareShowOnPages" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="socialShareShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all"' . ($showOnPages === 'all' ? ' selected' : '') . '>Todos</option>';
|
||||
$html .= ' <option value="posts"' . ($showOnPages === 'posts' ? ' selected' : '') . '>Solo posts</option>';
|
||||
$html .= ' <option value="pages"' . ($showOnPages === 'pages' ? ' selected' : '') . '>Solo paginas</option>';
|
||||
$html .= ' </select>';
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('socialShareVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('socialShareVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('socialShareVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('socialShareVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('socialShareVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'socialShare');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
@@ -526,4 +554,26 @@ final class SocialShareFormBuilder
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,19 @@ final class TableOfContentsFieldMapper implements FieldMapperInterface
|
||||
'tocEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'tocShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'tocShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'tocShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'tocVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'tocVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'tocVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'tocVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'tocVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'tocExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'tocExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'tocExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'tocExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'tocTitle' => ['group' => 'content', 'attribute' => 'title'],
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace ROITheme\Admin\TableOfContents\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* FormBuilder para la Tabla de Contenido
|
||||
@@ -94,20 +95,47 @@ final class TableOfContentsFormBuilder
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', false);
|
||||
$html .= $this->buildSwitch('tocShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
// show_on_pages
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <label for="tocShowOnPages" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="tocShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las paginas</option>';
|
||||
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts</option>';
|
||||
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo paginas</option>';
|
||||
$html .= ' </select>';
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', false);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('tocVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('tocVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('tocVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('tocVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('tocVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'toc');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
@@ -585,4 +613,26 @@ final class TableOfContentsFormBuilder
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,21 @@ final class TopNotificationBarFieldMapper implements FieldMapperInterface
|
||||
'topBarEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'topBarShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'topBarShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'topBarShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
'topBarIsCritical' => ['group' => 'visibility', 'attribute' => 'is_critical'],
|
||||
'topBarHideForLoggedIn' => ['group' => 'visibility', 'attribute' => 'hide_for_logged_in'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'topBarVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'topBarVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'topBarVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'topBarVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'topBarVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'topBarExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'topBarExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'topBarExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'topBarExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'topBarIconClass' => ['group' => 'content', 'attribute' => 'icon_class'],
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace ROITheme\Admin\TopNotificationBar\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
final class TopNotificationBarFormBuilder
|
||||
{
|
||||
@@ -105,24 +106,50 @@ final class TopNotificationBarFormBuilder
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Select: Show on Pages
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all');
|
||||
$html .= ' <div class="mb-2 mt-3">';
|
||||
$html .= ' <label for="topBarShowOnPages" class="form-label small mb-1 fw-semibold" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="topBarShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las páginas</option>';
|
||||
$html .= ' <option value="home" ' . selected($showOnPages, 'home', false) . '>Solo página de inicio</option>';
|
||||
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts individuales</option>';
|
||||
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo páginas</option>';
|
||||
$html .= ' </select>';
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('topBarVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('topBarVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('topBarVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('topBarVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('topBarVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'topBar');
|
||||
|
||||
// Switch: CSS Crítico
|
||||
$isCritical = $this->renderer->getFieldValue($componentId, 'visibility', 'is_critical', true);
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <div class="mb-2 mt-3">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="topBarIsCritical" ';
|
||||
$html .= checked($isCritical, true, false) . '>';
|
||||
@@ -134,6 +161,20 @@ final class TopNotificationBarFormBuilder
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: Ocultar para usuarios logueados (Plan 99.16)
|
||||
$hideForLoggedIn = $this->renderer->getFieldValue($componentId, 'visibility', 'hide_for_logged_in', false);
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="topBarHideForLoggedIn" ';
|
||||
$html .= checked($hideForLoggedIn, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="topBarHideForLoggedIn" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-person-lock me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Ocultar para usuarios logueados</strong>';
|
||||
$html .= ' <small class="text-muted d-block">No mostrar a usuarios con sesión iniciada</small>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
@@ -319,4 +360,26 @@ final class TopNotificationBarFormBuilder
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* NO contiene CSS personalizado (ese va en critical-custom-temp.css - TIPO 3).
|
||||
*
|
||||
* Componentes Bootstrap incluidos:
|
||||
* - System Fonts (CERO flash - sin @font-face externos)
|
||||
* - Fonts (@font-face Poppins)
|
||||
* - Variables CSS (:root)
|
||||
* - Resets (box-sizing, body)
|
||||
* - Container system
|
||||
@@ -30,29 +30,45 @@
|
||||
*/
|
||||
|
||||
/* ==========================================================================
|
||||
SYSTEM FONTS - CERO Flash (sin fuentes externas)
|
||||
CRITICAL FONTS (Poppins - LCP optimization)
|
||||
|
||||
Usa fuentes nativas del sistema operativo:
|
||||
- macOS/iOS: -apple-system, BlinkMacSystemFont
|
||||
- Windows: Segoe UI
|
||||
- Android: Roboto
|
||||
- Linux: Ubuntu/Cantarell
|
||||
- Fallback: sans-serif
|
||||
|
||||
VENTAJAS:
|
||||
- 0 KB descarga (fuentes ya instaladas)
|
||||
- 0 flash/parpadeo (disponibles instantaneamente)
|
||||
- Mejor rendimiento LCP/FCP
|
||||
- Familiar para usuarios (fuentes nativas)
|
||||
font-display: swap + preload = fuente carga rapido y siempre se muestra
|
||||
size-adjust: 100.6% = fallback casi identico a Poppins (minimiza CLS)
|
||||
========================================================================== */
|
||||
@font-face {
|
||||
font-family: 'Poppins Fallback';
|
||||
src: local('Arial'), local('Helvetica Neue'), local('sans-serif');
|
||||
size-adjust: 106%;
|
||||
ascent-override: 105%;
|
||||
descent-override: 35%;
|
||||
line-gap-override: 10%;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('/wp-content/themes/roi-theme/Assets/Fonts/poppins-v24-latin-regular.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('/wp-content/themes/roi-theme/Assets/Fonts/poppins-v24-latin-600.woff2') format('woff2');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('/wp-content/themes/roi-theme/Assets/Fonts/poppins-v24-latin-700.woff2') format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
/* System Font Stack - CERO flash garantizado */
|
||||
--font-system: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
"Helvetica Neue", Arial, "Noto Sans", "Liberation Sans",
|
||||
sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
||||
--font-primary: var(--font-system);
|
||||
--bs-body-font-family: var(--font-system);
|
||||
/* Fonts */
|
||||
--font-primary: 'Poppins', 'Poppins Fallback', sans-serif;
|
||||
--bs-body-font-family: 'Poppins', 'Poppins Fallback', sans-serif;
|
||||
|
||||
/* Theme Colors (críticos para above-the-fold) */
|
||||
--color-navy-dark: #0E2337;
|
||||
@@ -372,7 +388,7 @@ button:focus:not(:focus-visible) {
|
||||
--bs-navbar-toggler-border-radius: var(--bs-border-radius, 0.375rem);
|
||||
--bs-navbar-toggler-focus-width: 0.25rem;
|
||||
--bs-navbar-toggler-transition: box-shadow 0.15s ease-in-out;
|
||||
position: relative;
|
||||
/* position: controlado por CriticalCSSService según sticky_enabled */
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
* Sistema de Tipografías - ROI Theme
|
||||
*
|
||||
* RESPONSABILIDAD: SOLO definición de fuentes y variables tipográficas
|
||||
* - Declaraciones @font-face (comentadas - usar Google Fonts)
|
||||
* - Variables CSS de tipografía (:root)
|
||||
* - Clases utilitarias de fuentes
|
||||
*
|
||||
* NOTA: Usando SYSTEM FONTS para CERO flash/parpadeo
|
||||
* Las fuentes del sistema están disponibles instantáneamente.
|
||||
*
|
||||
* NO debe contener:
|
||||
* - Estilos de body (van en style.css)
|
||||
* - Estilos de elementos HTML (van en style.css)
|
||||
@@ -18,20 +16,20 @@
|
||||
*/
|
||||
|
||||
/* ============================================
|
||||
SYSTEM FONTS - CERO Flash
|
||||
SYSTEM FONTS (Por defecto - Recomendado)
|
||||
============================================ */
|
||||
|
||||
:root {
|
||||
/* Stack de fuentes del sistema - disponibles instantáneamente */
|
||||
/* Stack de fuentes del sistema - Fallback */
|
||||
--font-system: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||
'Helvetica Neue', Arial, 'Noto Sans', 'Liberation Sans',
|
||||
sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
|
||||
Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
|
||||
|
||||
/* Fuente primaria - System fonts (CERO flash) */
|
||||
--font-primary: var(--font-system);
|
||||
/* Fuente primaria - Poppins con fallback ajustado (Fase 4.3 PageSpeed)
|
||||
'Poppins Fallback' tiene size-adjust para reducir CLS durante font swap */
|
||||
--font-primary: 'Poppins', 'Poppins Fallback', sans-serif;
|
||||
|
||||
/* Fuente para encabezados - System fonts */
|
||||
--font-headings: var(--font-system);
|
||||
/* Fuente para encabezados - Poppins con fallback ajustado */
|
||||
--font-headings: 'Poppins', 'Poppins Fallback', sans-serif;
|
||||
|
||||
/* Fuente para código (monospace) */
|
||||
--font-mono: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono',
|
||||
@@ -48,22 +46,70 @@
|
||||
*/
|
||||
|
||||
/* ============================================
|
||||
POPPINS - DESHABILITADO
|
||||
POPPINS (Self-hosted)
|
||||
============================================
|
||||
|
||||
Las @font-face de Poppins fueron eliminadas para
|
||||
garantizar CERO flash/parpadeo en la carga de página.
|
||||
Fuentes Poppins alojadas localmente para:
|
||||
- Eliminar dependencia de Google Fonts
|
||||
- Mejorar rendimiento (sin requests externos)
|
||||
- Cumplimiento GDPR (sin tracking de Google)
|
||||
|
||||
El sitio ahora usa fuentes del sistema (--font-system)
|
||||
que están disponibles instantáneamente en todos los
|
||||
dispositivos sin necesidad de descarga.
|
||||
Pesos incluidos: 400, 500, 600, 700
|
||||
Formato: WOFF2 (mejor compresión)
|
||||
|
||||
Para reactivar Poppins en el futuro, descomentar las
|
||||
declaraciones @font-face y actualizar las variables
|
||||
--font-primary y --font-headings.
|
||||
Fase 4.3 PageSpeed: Fallback con size-adjust para reducir CLS
|
||||
- size-adjust: 100.6% ajustado para coincidir mejor con Poppins
|
||||
- font-display: swap + preload = carga rapida sin salto visual
|
||||
- Preload en CriticalCSSInjector P:-2 acelera descarga de fuentes
|
||||
|
||||
NOTA: El valor 100.6% fue calibrado empiricamente.
|
||||
- 106% causaba un salto visual notable (navbar se "achicaba")
|
||||
- 100.6% minimiza el CLS manteniendo legibilidad del fallback
|
||||
|
||||
============================================ */
|
||||
|
||||
/* Fallback font con metricas ajustadas para Poppins */
|
||||
@font-face {
|
||||
font-family: 'Poppins Fallback';
|
||||
src: local('Arial'), local('Helvetica Neue'), local('Helvetica'), local('sans-serif');
|
||||
size-adjust: 106%;
|
||||
ascent-override: 105%;
|
||||
descent-override: 35%;
|
||||
line-gap-override: 10%;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('../Fonts/poppins-v24-latin-regular.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('../Fonts/poppins-v24-latin-500.woff2') format('woff2');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('../Fonts/poppins-v24-latin-600.woff2') format('woff2');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('../Fonts/poppins-v24-latin-700.woff2') format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
UTILIDADES DE FUENTES
|
||||
============================================ */
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
BASE STYLES - Todas las tablas genéricas
|
||||
======================================== */
|
||||
|
||||
.post-content table:not(.analisis table) {
|
||||
.post-content table:not(.analisis table):not(.desglose table) {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 2rem auto;
|
||||
@@ -23,9 +23,9 @@
|
||||
}
|
||||
|
||||
/* Header styles - VERY OBVIOUS */
|
||||
.post-content table:not(.analisis table) thead tr:first-child th,
|
||||
.post-content table:not(.analisis table) tbody tr:first-child td,
|
||||
.post-content table:not(.analisis table) tr:first-child td {
|
||||
.post-content table:not(.analisis table):not(.desglose table) thead tr:first-child th,
|
||||
.post-content table:not(.analisis table):not(.desglose table) tbody tr:first-child td,
|
||||
.post-content table:not(.analisis table):not(.desglose table) tr:first-child td {
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
padding: 1.25rem 1rem;
|
||||
@@ -34,7 +34,7 @@
|
||||
}
|
||||
|
||||
/* Body cells */
|
||||
.post-content table:not(.analisis table) tbody tr:not(:first-child) td {
|
||||
.post-content table:not(.analisis table):not(.desglose table) tbody tr:not(:first-child) td {
|
||||
padding: 0.875rem 1rem;
|
||||
border: 1px solid var(--color-neutral-100);
|
||||
text-align: left;
|
||||
|
||||
@@ -88,3 +88,43 @@
|
||||
.transition-none {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
COMPONENT VISIBILITY FAILSAFE (Plan 99.15)
|
||||
|
||||
CSS failsafe: Oculta wrappers de componentes
|
||||
cuando body tiene clases roi-hide-*
|
||||
|
||||
Estas clases se agregan via BodyClassHooksRegistrar
|
||||
cuando los componentes están deshabilitados/excluidos.
|
||||
======================================== */
|
||||
|
||||
/* Navbar hidden */
|
||||
body.roi-hide-navbar .navbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Table of Contents hidden */
|
||||
body.roi-hide-toc .roi-toc-container {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* CTA Sidebar hidden */
|
||||
body.roi-hide-cta-sidebar .roi-cta-box {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Generic sidebar hidden */
|
||||
body.roi-hide-sidebar .sidebar-sticky {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* When ALL sidebar components are hidden, expand main column */
|
||||
body.roi-sidebar-empty .col-lg-9 {
|
||||
flex: 0 0 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
body.roi-sidebar-empty .col-lg-3 {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -182,10 +182,72 @@
|
||||
}, CONFIG.timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activa slots de AdSense insertados dinamicamente
|
||||
* Escucha el evento 'roi-adsense-activate' disparado por otros scripts
|
||||
*/
|
||||
function setupDynamicAdsListener() {
|
||||
window.addEventListener('roi-adsense-activate', function() {
|
||||
debugLog('Evento roi-adsense-activate recibido');
|
||||
|
||||
// Si AdSense aun no ha cargado, forzar carga ahora
|
||||
if (!adsenseLoaded) {
|
||||
debugLog('AdSense no cargado, forzando carga...');
|
||||
loadAdSense();
|
||||
return;
|
||||
}
|
||||
|
||||
// AdSense ya cargado - activar nuevos slots
|
||||
debugLog('Activando nuevos slots dinamicos...');
|
||||
activateDynamicSlots();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Activa slots de AdSense que fueron insertados despues de la carga inicial
|
||||
*/
|
||||
function activateDynamicSlots() {
|
||||
// Buscar scripts de push que aun no han sido ejecutados
|
||||
var pendingPushScripts = document.querySelectorAll('script[data-adsense-push][type="text/plain"]');
|
||||
|
||||
if (pendingPushScripts.length === 0) {
|
||||
debugLog('No hay slots pendientes por activar');
|
||||
return;
|
||||
}
|
||||
|
||||
debugLog('Activando ' + pendingPushScripts.length + ' slot(s) dinamico(s)');
|
||||
|
||||
// Asegurar que adsbygoogle existe
|
||||
window.adsbygoogle = window.adsbygoogle || [];
|
||||
|
||||
pendingPushScripts.forEach(function(oldScript) {
|
||||
try {
|
||||
// Crear nuevo script ejecutable
|
||||
var newScript = document.createElement('script');
|
||||
newScript.type = 'text/javascript';
|
||||
newScript.innerHTML = oldScript.innerHTML;
|
||||
|
||||
// Reemplazar el placeholder con el script real
|
||||
oldScript.parentNode.replaceChild(newScript, oldScript);
|
||||
} catch (e) {
|
||||
debugLog('Error activando slot: ' + e.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializa el cargador retrasado de AdSense
|
||||
*/
|
||||
function init() {
|
||||
// =========================================================================
|
||||
// NUEVO: Siempre configurar listener para ads dinamicos
|
||||
// IMPORTANTE: Esto debe ejecutarse ANTES del early return
|
||||
// porque los ads dinamicos pueden necesitar activarse aunque
|
||||
// el delay global este deshabilitado
|
||||
// =========================================================================
|
||||
setupDynamicAdsListener();
|
||||
debugLog('Listener para ads dinamicos configurado');
|
||||
|
||||
// Verificar si el retardo de AdSense está habilitado
|
||||
if (!window.roiAdsenseDelayed) {
|
||||
debugLog('Retardo de AdSense no habilitado');
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
/**
|
||||
* Auto-detectar y agregar clases a filas especiales de tablas APU
|
||||
*
|
||||
* Este script detecta automáticamente filas especiales en tablas .desglose y .analisis
|
||||
* y les agrega las clases CSS correspondientes para que se apliquen los estilos correctos.
|
||||
*
|
||||
* Detecta:
|
||||
* - Section headers: Material, Mano de Obra, Herramienta, Equipo
|
||||
* - Subtotal rows: Filas que empiezan con "Suma de"
|
||||
* - Total row: Costo Directo
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Agrega clases a filas especiales de tablas APU
|
||||
*/
|
||||
function applyApuTableClasses() {
|
||||
// Buscar todas las tablas con clase .desglose o .analisis
|
||||
const tables = document.querySelectorAll('.desglose table, .analisis table');
|
||||
|
||||
if (tables.length === 0) {
|
||||
return; // No hay tablas APU en esta página
|
||||
}
|
||||
|
||||
let classesAdded = 0;
|
||||
|
||||
tables.forEach(function(table) {
|
||||
const rows = table.querySelectorAll('tbody tr');
|
||||
|
||||
rows.forEach(function(row) {
|
||||
// Evitar procesar filas que ya tienen clase
|
||||
if (row.classList.contains('section-header') ||
|
||||
row.classList.contains('subtotal-row') ||
|
||||
row.classList.contains('total-row')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const secondCell = row.querySelector('td:nth-child(2)');
|
||||
if (!secondCell) {
|
||||
return; // Fila sin segunda celda
|
||||
}
|
||||
|
||||
const text = secondCell.textContent.trim();
|
||||
|
||||
// Detectar section headers
|
||||
if (text === 'Material' ||
|
||||
text === 'Mano de Obra' ||
|
||||
text === 'Herramienta' ||
|
||||
text === 'Equipo' ||
|
||||
text === 'MATERIAL' ||
|
||||
text === 'MANO DE OBRA' ||
|
||||
text === 'HERRAMIENTA' ||
|
||||
text === 'EQUIPO') {
|
||||
row.classList.add('section-header');
|
||||
classesAdded++;
|
||||
return;
|
||||
}
|
||||
|
||||
// Detectar subtotales (cualquier variación de "Suma de")
|
||||
if (text.toLowerCase().startsWith('suma de ') ||
|
||||
text.toLowerCase().startsWith('subtotal ')) {
|
||||
row.classList.add('subtotal-row');
|
||||
classesAdded++;
|
||||
return;
|
||||
}
|
||||
|
||||
// Detectar total final
|
||||
if (text === 'Costo Directo' ||
|
||||
text === 'COSTO DIRECTO' ||
|
||||
text === 'Total' ||
|
||||
text === 'TOTAL' ||
|
||||
text === 'Costo directo') {
|
||||
row.classList.add('total-row');
|
||||
classesAdded++;
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Log para debugging (solo en desarrollo)
|
||||
if (classesAdded > 0 && window.console) {
|
||||
console.log('[APU Tables] Clases agregadas automáticamente: ' + classesAdded);
|
||||
}
|
||||
}
|
||||
|
||||
// Ejecutar cuando el DOM esté listo
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', applyApuTableClasses);
|
||||
} else {
|
||||
// DOM ya está listo
|
||||
applyApuTableClasses();
|
||||
}
|
||||
|
||||
})();
|
||||
@@ -1,342 +0,0 @@
|
||||
/**
|
||||
* Header Navigation JavaScript
|
||||
*
|
||||
* This file handles:
|
||||
* - Mobile hamburger menu toggle
|
||||
* - Sticky header behavior
|
||||
* - Smooth scroll to anchors (optional)
|
||||
* - Accessibility features (keyboard navigation, ARIA attributes)
|
||||
* - Body scroll locking when mobile menu is open
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Initialize on DOM ready
|
||||
*/
|
||||
function init() {
|
||||
setupMobileMenu();
|
||||
setupStickyHeader();
|
||||
setupSmoothScroll();
|
||||
setupKeyboardNavigation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mobile Menu Functionality
|
||||
*/
|
||||
function setupMobileMenu() {
|
||||
const mobileMenuToggle = document.getElementById('mobile-menu-toggle');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
const mobileMenuOverlay = document.getElementById('mobile-menu-overlay');
|
||||
const mobileMenuClose = document.getElementById('mobile-menu-close');
|
||||
|
||||
if (!mobileMenuToggle || !mobileMenu || !mobileMenuOverlay) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Open mobile menu
|
||||
mobileMenuToggle.addEventListener('click', function() {
|
||||
openMobileMenu();
|
||||
});
|
||||
|
||||
// Close mobile menu via close button
|
||||
if (mobileMenuClose) {
|
||||
mobileMenuClose.addEventListener('click', function() {
|
||||
closeMobileMenu();
|
||||
});
|
||||
}
|
||||
|
||||
// Close mobile menu via overlay click
|
||||
mobileMenuOverlay.addEventListener('click', function() {
|
||||
closeMobileMenu();
|
||||
});
|
||||
|
||||
// Close mobile menu on Escape key
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape' && mobileMenu.classList.contains('active')) {
|
||||
closeMobileMenu();
|
||||
mobileMenuToggle.focus();
|
||||
}
|
||||
});
|
||||
|
||||
// Close mobile menu when clicking a menu link
|
||||
const mobileMenuLinks = mobileMenu.querySelectorAll('a');
|
||||
mobileMenuLinks.forEach(function(link) {
|
||||
link.addEventListener('click', function() {
|
||||
closeMobileMenu();
|
||||
});
|
||||
});
|
||||
|
||||
// Handle window resize - close mobile menu if switching to desktop
|
||||
let resizeTimer;
|
||||
window.addEventListener('resize', function() {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(function() {
|
||||
if (window.innerWidth >= 768 && mobileMenu.classList.contains('active')) {
|
||||
closeMobileMenu();
|
||||
}
|
||||
}, 250);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Open mobile menu
|
||||
*/
|
||||
function openMobileMenu() {
|
||||
const mobileMenuToggle = document.getElementById('mobile-menu-toggle');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
const mobileMenuOverlay = document.getElementById('mobile-menu-overlay');
|
||||
|
||||
// Add active classes
|
||||
mobileMenu.classList.add('active');
|
||||
mobileMenuOverlay.classList.add('active');
|
||||
document.body.classList.add('mobile-menu-open');
|
||||
|
||||
// Update ARIA attributes
|
||||
mobileMenuToggle.setAttribute('aria-expanded', 'true');
|
||||
mobileMenu.setAttribute('aria-hidden', 'false');
|
||||
mobileMenuOverlay.setAttribute('aria-hidden', 'false');
|
||||
|
||||
// Focus trap - focus first menu item
|
||||
const firstMenuItem = mobileMenu.querySelector('a');
|
||||
if (firstMenuItem) {
|
||||
setTimeout(function() {
|
||||
firstMenuItem.focus();
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close mobile menu
|
||||
*/
|
||||
function closeMobileMenu() {
|
||||
const mobileMenuToggle = document.getElementById('mobile-menu-toggle');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
const mobileMenuOverlay = document.getElementById('mobile-menu-overlay');
|
||||
|
||||
// Remove active classes
|
||||
mobileMenu.classList.remove('active');
|
||||
mobileMenuOverlay.classList.remove('active');
|
||||
document.body.classList.remove('mobile-menu-open');
|
||||
|
||||
// Update ARIA attributes
|
||||
mobileMenuToggle.setAttribute('aria-expanded', 'false');
|
||||
mobileMenu.setAttribute('aria-hidden', 'true');
|
||||
mobileMenuOverlay.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sticky Header Behavior
|
||||
*/
|
||||
function setupStickyHeader() {
|
||||
const header = document.getElementById('masthead');
|
||||
|
||||
if (!header) {
|
||||
return;
|
||||
}
|
||||
|
||||
let lastScrollTop = 0;
|
||||
let scrollThreshold = 100;
|
||||
|
||||
window.addEventListener('scroll', function() {
|
||||
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
||||
|
||||
// Add/remove scrolled class based on scroll position
|
||||
if (scrollTop > scrollThreshold) {
|
||||
header.classList.add('scrolled');
|
||||
} else {
|
||||
header.classList.remove('scrolled');
|
||||
}
|
||||
|
||||
lastScrollTop = scrollTop;
|
||||
}, { passive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Smooth Scroll to Anchors (Optional)
|
||||
*/
|
||||
function setupSmoothScroll() {
|
||||
// Check if user prefers reduced motion
|
||||
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
|
||||
if (prefersReducedMotion) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all anchor links
|
||||
const anchorLinks = document.querySelectorAll('a[href^="#"]');
|
||||
|
||||
anchorLinks.forEach(function(link) {
|
||||
link.addEventListener('click', function(e) {
|
||||
const href = this.getAttribute('href');
|
||||
|
||||
// Skip if href is just "#"
|
||||
if (href === '#') {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = document.querySelector(href);
|
||||
|
||||
if (target) {
|
||||
e.preventDefault();
|
||||
|
||||
// Get header height for offset
|
||||
const header = document.getElementById('masthead');
|
||||
const headerHeight = header ? header.offsetHeight : 0;
|
||||
const targetPosition = target.getBoundingClientRect().top + window.pageYOffset - headerHeight - 20;
|
||||
|
||||
window.scrollTo({
|
||||
top: targetPosition,
|
||||
behavior: prefersReducedMotion ? 'auto' : 'smooth'
|
||||
});
|
||||
|
||||
// Update URL hash
|
||||
if (history.pushState) {
|
||||
history.pushState(null, null, href);
|
||||
}
|
||||
|
||||
// Focus target element for accessibility
|
||||
target.setAttribute('tabindex', '-1');
|
||||
target.focus();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Keyboard Navigation for Menus
|
||||
*/
|
||||
function setupKeyboardNavigation() {
|
||||
const menuItems = document.querySelectorAll('.primary-menu > li, .mobile-primary-menu > li');
|
||||
|
||||
menuItems.forEach(function(item) {
|
||||
const link = item.querySelector('a');
|
||||
const submenu = item.querySelector('.sub-menu');
|
||||
|
||||
if (!link || !submenu) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Open submenu on Enter/Space
|
||||
link.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
if (submenu) {
|
||||
e.preventDefault();
|
||||
toggleSubmenu(item, submenu);
|
||||
}
|
||||
}
|
||||
|
||||
// Close submenu on Escape
|
||||
if (e.key === 'Escape') {
|
||||
closeSubmenu(item, submenu);
|
||||
link.focus();
|
||||
}
|
||||
});
|
||||
|
||||
// Close submenu when focus leaves
|
||||
const submenuLinks = submenu.querySelectorAll('a');
|
||||
if (submenuLinks.length > 0) {
|
||||
const lastSubmenuLink = submenuLinks[submenuLinks.length - 1];
|
||||
|
||||
lastSubmenuLink.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Tab' && !e.shiftKey) {
|
||||
closeSubmenu(item, submenu);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle submenu visibility
|
||||
*/
|
||||
function toggleSubmenu(item, submenu) {
|
||||
const isExpanded = item.classList.contains('submenu-open');
|
||||
|
||||
if (isExpanded) {
|
||||
closeSubmenu(item, submenu);
|
||||
} else {
|
||||
openSubmenu(item, submenu);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open submenu
|
||||
*/
|
||||
function openSubmenu(item, submenu) {
|
||||
item.classList.add('submenu-open');
|
||||
submenu.setAttribute('aria-hidden', 'false');
|
||||
|
||||
const firstLink = submenu.querySelector('a');
|
||||
if (firstLink) {
|
||||
firstLink.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close submenu
|
||||
*/
|
||||
function closeSubmenu(item, submenu) {
|
||||
item.classList.remove('submenu-open');
|
||||
submenu.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
|
||||
/**
|
||||
* Trap focus within mobile menu when open
|
||||
*/
|
||||
function setupFocusTrap() {
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
|
||||
if (!mobileMenu) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (!mobileMenu.classList.contains('active')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'Tab') {
|
||||
const focusableElements = mobileMenu.querySelectorAll(
|
||||
'a, button, [tabindex]:not([tabindex="-1"])'
|
||||
);
|
||||
|
||||
const firstElement = focusableElements[0];
|
||||
const lastElement = focusableElements[focusableElements.length - 1];
|
||||
|
||||
if (e.shiftKey) {
|
||||
// Shift + Tab
|
||||
if (document.activeElement === firstElement) {
|
||||
e.preventDefault();
|
||||
lastElement.focus();
|
||||
}
|
||||
} else {
|
||||
// Tab
|
||||
if (document.activeElement === lastElement) {
|
||||
e.preventDefault();
|
||||
firstElement.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize focus trap
|
||||
*/
|
||||
setupFocusTrap();
|
||||
|
||||
/**
|
||||
* Initialize when DOM is ready
|
||||
*/
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
})();
|
||||
File diff suppressed because one or more lines are too long
@@ -25,6 +25,9 @@ if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||
use ROITheme\Shared\Infrastructure\Services\UserVisibilityHelper;
|
||||
|
||||
/**
|
||||
* Renderiza un slot de anuncio en una ubicacion
|
||||
*
|
||||
@@ -47,16 +50,21 @@ function roi_render_ad_slot(string $location): string
|
||||
return '';
|
||||
}
|
||||
|
||||
// Verificar si ocultar para usuarios logueados
|
||||
if (roi_should_hide_for_logged_in($settings)) {
|
||||
// Verificar visibilidad por usuario logueado (Plan 99.16)
|
||||
if (!UserVisibilityHelper::shouldShowForUser($settings['visibility'] ?? [])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Verificar exclusiones
|
||||
// Verificar exclusiones legacy (forms group)
|
||||
if (roi_is_ad_excluded($settings)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Verificar exclusiones modernas (Plan 99.11: _exclusions, _page_visibility)
|
||||
if (!PageVisibilityHelper::shouldShow('adsense-placement')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Obtener renderer desde DIContainer (DIP compliant)
|
||||
$renderer = $container->getAdsensePlacementRenderer();
|
||||
|
||||
@@ -72,17 +80,17 @@ function roi_render_ad_slot(string $location): string
|
||||
|
||||
/**
|
||||
* Verifica si se deben ocultar anuncios para usuarios logueados
|
||||
*
|
||||
* @deprecated Plan 99.16: Usar UserVisibilityHelper::shouldShowForUser() en su lugar.
|
||||
* Esta función se mantiene para compatibilidad hacia atrás.
|
||||
*
|
||||
* @param array $settings Configuración del componente
|
||||
* @return bool true si se debe ocultar, false si se debe mostrar
|
||||
*/
|
||||
function roi_should_hide_for_logged_in(array $settings): bool
|
||||
{
|
||||
// Si la opcion esta activada Y el usuario esta logueado, ocultar ads
|
||||
$hideForLoggedIn = $settings['visibility']['hide_for_logged_in'] ?? false;
|
||||
|
||||
if ($hideForLoggedIn && is_user_logged_in()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
// Delegar a UserVisibilityHelper (Plan 99.16)
|
||||
return !UserVisibilityHelper::shouldShowForUser($settings['visibility'] ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,16 +146,21 @@ function roi_render_rail_ads(): string
|
||||
return '';
|
||||
}
|
||||
|
||||
// Verificar si ocultar para usuarios logueados
|
||||
if (roi_should_hide_for_logged_in($settings)) {
|
||||
// Verificar visibilidad por usuario logueado (Plan 99.16)
|
||||
if (!UserVisibilityHelper::shouldShowForUser($settings['visibility'] ?? [])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Verificar exclusiones
|
||||
// Verificar exclusiones legacy (forms group)
|
||||
if (roi_is_ad_excluded($settings)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Verificar exclusiones modernas (Plan 99.11: _exclusions, _page_visibility)
|
||||
if (!PageVisibilityHelper::shouldShow('adsense-placement')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Obtener renderer desde DIContainer (DIP compliant)
|
||||
$renderer = $container->getAdsensePlacementRenderer();
|
||||
|
||||
@@ -188,8 +201,13 @@ function roi_enqueue_adsense_script(): void
|
||||
return;
|
||||
}
|
||||
|
||||
// Verificar si ocultar para usuarios logueados
|
||||
if (roi_should_hide_for_logged_in($settings)) {
|
||||
// Verificar visibilidad por usuario logueado (Plan 99.16)
|
||||
if (!UserVisibilityHelper::shouldShowForUser($settings['visibility'] ?? [])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Verificar exclusiones modernas (Plan 99.11: _exclusions, _page_visibility)
|
||||
if (!PageVisibilityHelper::shouldShow('adsense-placement')) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -241,16 +259,21 @@ function roi_inject_content_ads(string $content): string
|
||||
return $content;
|
||||
}
|
||||
|
||||
// Verificar si ocultar para usuarios logueados
|
||||
if (roi_should_hide_for_logged_in($settings)) {
|
||||
// Verificar visibilidad por usuario logueado (Plan 99.16)
|
||||
if (!UserVisibilityHelper::shouldShowForUser($settings['visibility'] ?? [])) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
// Verificar exclusiones
|
||||
// Verificar exclusiones legacy (forms group)
|
||||
if (roi_is_ad_excluded($settings)) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
// Verificar exclusiones modernas (Plan 99.11: _exclusions, _page_visibility)
|
||||
if (!PageVisibilityHelper::shouldShow('adsense-placement')) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
$renderer = $container->getAdsensePlacementRenderer();
|
||||
|
||||
// Inyectar anuncio al inicio (post-top)
|
||||
@@ -441,16 +464,21 @@ function roi_render_anchor_ads(): string
|
||||
return '';
|
||||
}
|
||||
|
||||
// Verificar si ocultar para usuarios logueados
|
||||
if (roi_should_hide_for_logged_in($settings)) {
|
||||
// Verificar visibilidad por usuario logueado (Plan 99.16)
|
||||
if (!UserVisibilityHelper::shouldShowForUser($settings['visibility'] ?? [])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Verificar exclusiones
|
||||
// Verificar exclusiones legacy (forms group)
|
||||
if (roi_is_ad_excluded($settings)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Verificar exclusiones modernas (Plan 99.11: _exclusions, _page_visibility)
|
||||
if (!PageVisibilityHelper::shouldShow('adsense-placement')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Obtener renderer desde DIContainer (DIP compliant)
|
||||
$renderer = $container->getAdsensePlacementRenderer();
|
||||
|
||||
@@ -485,16 +513,21 @@ function roi_render_vignette_ad(): string
|
||||
return '';
|
||||
}
|
||||
|
||||
// Verificar si ocultar para usuarios logueados
|
||||
if (roi_should_hide_for_logged_in($settings)) {
|
||||
// Verificar visibilidad por usuario logueado (Plan 99.16)
|
||||
if (!UserVisibilityHelper::shouldShowForUser($settings['visibility'] ?? [])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Verificar exclusiones
|
||||
// Verificar exclusiones legacy (forms group)
|
||||
if (roi_is_ad_excluded($settings)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Verificar exclusiones modernas (Plan 99.11: _exclusions, _page_visibility)
|
||||
if (!PageVisibilityHelper::shouldShow('adsense-placement')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Obtener renderer desde DIContainer (DIP compliant)
|
||||
$renderer = $container->getAdsensePlacementRenderer();
|
||||
|
||||
@@ -551,8 +584,13 @@ function roi_enqueue_anchor_vignette_scripts(): void
|
||||
return;
|
||||
}
|
||||
|
||||
// Verificar si ocultar para usuarios logueados
|
||||
if (roi_should_hide_for_logged_in($settings)) {
|
||||
// Verificar visibilidad por usuario logueado (Plan 99.16)
|
||||
if (!UserVisibilityHelper::shouldShowForUser($settings['visibility'] ?? [])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Verificar exclusiones modernas (Plan 99.11: _exclusions, _page_visibility)
|
||||
if (!PageVisibilityHelper::shouldShow('adsense-placement')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ define('ROI_DEFERRED_CSS', [
|
||||
'roi-utilities',
|
||||
'roi-accessibility',
|
||||
'roi-responsive',
|
||||
'bootstrap-icons',
|
||||
// NOTA: bootstrap-icons REMOVIDO de diferido - ahora crítico para evitar flash
|
||||
]);
|
||||
|
||||
/**
|
||||
@@ -125,19 +125,19 @@ function roi_enqueue_bootstrap() {
|
||||
'roi-bootstrap',
|
||||
get_template_directory_uri() . '/Assets/Vendor/Bootstrap/Css/bootstrap-subset.min.css',
|
||||
array('roi-fonts'),
|
||||
'5.3.2-subset',
|
||||
'5.3.2-subset-2', // v2: removed position:relative from .navbar
|
||||
'print' // DIFERIDO - critical CSS inline evita CLS
|
||||
);
|
||||
|
||||
// Bootstrap Icons CSS - SUBSET OPTIMIZADO (Fase 4.1 PageSpeed)
|
||||
// Original: 211 KB (2050 iconos) -> Subset: 13 KB (104 iconos) = 94% reduccion
|
||||
// DIFERIDO: Fase 4.3 - no crítico para renderizado inicial
|
||||
// CRITICO: Carga inmediata para evitar flash de iconos (4.4KB)
|
||||
wp_enqueue_style(
|
||||
'bootstrap-icons',
|
||||
get_template_directory_uri() . '/Assets/Vendor/bootstrap-icons-subset.min.css',
|
||||
array('roi-bootstrap'),
|
||||
ROI_VERSION,
|
||||
'print'
|
||||
'all' // CRITICO - no diferir para evitar parpadeo de iconos
|
||||
);
|
||||
|
||||
// Variables CSS del Template RDash - DIFERIDO
|
||||
|
||||
@@ -50,6 +50,13 @@ function roi_get_featured_image($post_id = null, $size = 'roi-featured-large', $
|
||||
return ''; // No placeholder - retornar vacío
|
||||
}
|
||||
|
||||
// Verificar que el archivo físico exista, no solo el attachment ID
|
||||
$thumbnailId = get_post_thumbnail_id($post_id);
|
||||
$filePath = get_attached_file($thumbnailId);
|
||||
if (empty($filePath) || !file_exists($filePath)) {
|
||||
return ''; // Archivo no existe en servidor
|
||||
}
|
||||
|
||||
// Obtener tipo de post
|
||||
$post_type = get_post_type($post_id);
|
||||
|
||||
@@ -145,6 +152,13 @@ function roi_get_post_thumbnail($post_id = null, $with_link = true) {
|
||||
return ''; // No placeholder - retornar vacío
|
||||
}
|
||||
|
||||
// Verificar que el archivo físico exista
|
||||
$thumbnailId = get_post_thumbnail_id($post_id);
|
||||
$filePath = get_attached_file($thumbnailId);
|
||||
if (empty($filePath) || !file_exists($filePath)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Obtener la imagen con clases Bootstrap
|
||||
$image = get_the_post_thumbnail($post_id, 'roi-featured-medium', array(
|
||||
'class' => 'img-fluid post-thumbnail',
|
||||
@@ -216,6 +230,13 @@ function roi_get_post_thumbnail_small($post_id = null, $with_link = true) {
|
||||
return ''; // No placeholder - retornar vacío
|
||||
}
|
||||
|
||||
// Verificar que el archivo físico exista
|
||||
$thumbnailId = get_post_thumbnail_id($post_id);
|
||||
$filePath = get_attached_file($thumbnailId);
|
||||
if (empty($filePath) || !file_exists($filePath)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Obtener la imagen
|
||||
$image = get_the_post_thumbnail($post_id, 'roi-thumbnail', array(
|
||||
'class' => 'img-fluid post-thumbnail-small',
|
||||
@@ -287,6 +308,13 @@ function roi_should_show_featured_image($post_id = null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verificar que el archivo físico exista
|
||||
$thumbnailId = get_post_thumbnail_id($post_id);
|
||||
$filePath = get_attached_file($thumbnailId);
|
||||
if (empty($filePath) || !file_exists($filePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Obtener tipo de post
|
||||
$post_type = get_post_type($post_id);
|
||||
|
||||
@@ -338,6 +366,13 @@ function roi_get_featured_image_url($post_id = null, $size = 'roi-featured-large
|
||||
return ''; // No placeholder - retornar vacío
|
||||
}
|
||||
|
||||
// Verificar que el archivo físico exista
|
||||
$thumbnailId = get_post_thumbnail_id($post_id);
|
||||
$filePath = get_attached_file($thumbnailId);
|
||||
if (empty($filePath) || !file_exists($filePath)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Obtener URL de la imagen
|
||||
$image_url = get_the_post_thumbnail_url($post_id, $size);
|
||||
|
||||
|
||||
@@ -552,30 +552,22 @@ add_filter( 'wp_lazy_loading_enabled', 'roi_enable_image_dimensions' );
|
||||
/**
|
||||
* Optimizar buffer de salida HTML
|
||||
*
|
||||
* Habilita compresión GZIP si está disponible y no está ya habilitada.
|
||||
* DESACTIVADO: Esta función causa conflicto con W3 Total Cache.
|
||||
* Cuando zlib.output_compression está activo, W3TC no puede cachear
|
||||
* las páginas porque recibe "Response is compressed".
|
||||
*
|
||||
* La compresión GZIP la maneja nginx a nivel de servidor.
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @deprecated 1.0.1 Conflicto con W3TC page cache - Issue #XX
|
||||
*/
|
||||
function roi_enable_gzip_compression() {
|
||||
// Solo en frontend
|
||||
if ( is_admin() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Verificar si GZIP ya está habilitado
|
||||
if ( ! ini_get( 'zlib.output_compression' ) && 'ob_gzhandler' !== ini_get( 'output_handler' ) ) {
|
||||
// Verificar si la extensión está disponible
|
||||
if ( function_exists( 'gzencode' ) && extension_loaded( 'zlib' ) ) {
|
||||
// Verificar headers
|
||||
if ( ! headers_sent() ) {
|
||||
// Habilitar compresión
|
||||
ini_set( 'zlib.output_compression', 'On' );
|
||||
ini_set( 'zlib.output_compression_level', '6' ); // Balance entre compresión y CPU
|
||||
}
|
||||
}
|
||||
}
|
||||
// DESACTIVADO - No hacer nada
|
||||
// La compresión la maneja nginx, no PHP
|
||||
return;
|
||||
}
|
||||
add_action( 'template_redirect', 'roi_enable_gzip_compression', 0 );
|
||||
// DESACTIVADO - Conflicto con W3 Total Cache page cache
|
||||
// add_action( 'template_redirect', 'roi_enable_gzip_compression', 0 );
|
||||
|
||||
/**
|
||||
* ============================================================================
|
||||
|
||||
@@ -1,294 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Related Posts Functionality
|
||||
*
|
||||
* Provides configurable related posts functionality with Bootstrap grid support.
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
// Exit if accessed directly
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get related posts based on categories
|
||||
*
|
||||
* @param int $post_id The post ID to get related posts for
|
||||
* @return WP_Query|false Query object with related posts or false if none found
|
||||
*/
|
||||
function roi_get_related_posts($post_id) {
|
||||
// Get post categories
|
||||
$categories = wp_get_post_categories($post_id);
|
||||
|
||||
if (empty($categories)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get number of posts to display (default: 3)
|
||||
$posts_per_page = get_option('roi_related_posts_count', 3);
|
||||
|
||||
// Query arguments
|
||||
$args = array(
|
||||
'post_type' => 'post',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => $posts_per_page,
|
||||
'post__not_in' => array($post_id),
|
||||
'category__in' => $categories,
|
||||
'orderby' => 'rand',
|
||||
'no_found_rows' => true,
|
||||
'update_post_meta_cache' => false,
|
||||
'update_post_term_cache' => false,
|
||||
);
|
||||
|
||||
// Allow filtering of query args
|
||||
$args = apply_filters('roi_related_posts_args', $args, $post_id);
|
||||
|
||||
// Get related posts
|
||||
$related_query = new WP_Query($args);
|
||||
|
||||
return $related_query->have_posts() ? $related_query : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display related posts section
|
||||
*
|
||||
* @param int|null $post_id Optional. Post ID. Default is current post.
|
||||
* @return void
|
||||
*/
|
||||
function roi_display_related_posts($post_id = null) {
|
||||
// Get post ID
|
||||
if (!$post_id) {
|
||||
$post_id = get_the_ID();
|
||||
}
|
||||
|
||||
// Check if related posts are enabled
|
||||
$enabled = get_option('roi_related_posts_enabled', true);
|
||||
if (!$enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get related posts
|
||||
$related_query = roi_get_related_posts($post_id);
|
||||
|
||||
if (!$related_query) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get configuration options
|
||||
$title = get_option('roi_related_posts_title', __('Related Posts', 'roi-theme'));
|
||||
$columns = get_option('roi_related_posts_columns', 3);
|
||||
$show_excerpt = get_option('roi_related_posts_show_excerpt', true);
|
||||
$show_date = get_option('roi_related_posts_show_date', true);
|
||||
$show_category = get_option('roi_related_posts_show_category', true);
|
||||
$excerpt_length = get_option('roi_related_posts_excerpt_length', 20);
|
||||
$background_colors = get_option('roi_related_posts_bg_colors', array(
|
||||
'#1a73e8', // Blue
|
||||
'#e91e63', // Pink
|
||||
'#4caf50', // Green
|
||||
'#ff9800', // Orange
|
||||
'#9c27b0', // Purple
|
||||
'#00bcd4', // Cyan
|
||||
));
|
||||
|
||||
// Calculate Bootstrap column class
|
||||
$col_class = roi_get_column_class($columns);
|
||||
|
||||
// Start output
|
||||
?>
|
||||
<section class="related-posts-section">
|
||||
<div class="related-posts-container">
|
||||
|
||||
<?php if ($title) : ?>
|
||||
<h2 class="related-posts-title"><?php echo esc_html($title); ?></h2>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row g-4">
|
||||
<?php
|
||||
$color_index = 0;
|
||||
while ($related_query->have_posts()) :
|
||||
$related_query->the_post();
|
||||
$has_thumbnail = has_post_thumbnail();
|
||||
|
||||
// Get background color for posts without image
|
||||
$bg_color = $background_colors[$color_index % count($background_colors)];
|
||||
$color_index++;
|
||||
?>
|
||||
|
||||
<div class="<?php echo esc_attr($col_class); ?>">
|
||||
<article class="related-post-card <?php echo $has_thumbnail ? 'has-thumbnail' : 'no-thumbnail'; ?>">
|
||||
|
||||
<a href="<?php the_permalink(); ?>" class="related-post-link">
|
||||
|
||||
<?php if ($has_thumbnail) : ?>
|
||||
<!-- Card with Image -->
|
||||
<div class="related-post-thumbnail">
|
||||
<?php
|
||||
the_post_thumbnail('roi-thumbnail', array(
|
||||
'alt' => the_title_attribute(array('echo' => false)),
|
||||
'loading' => 'lazy',
|
||||
));
|
||||
?>
|
||||
|
||||
<?php if ($show_category) : ?>
|
||||
<?php
|
||||
$categories = get_the_category();
|
||||
if (!empty($categories)) :
|
||||
$category = $categories[0];
|
||||
?>
|
||||
<span class="related-post-category">
|
||||
<?php echo esc_html($category->name); ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<!-- Card without Image - Color Background -->
|
||||
<div class="related-post-no-image" style="background-color: <?php echo esc_attr($bg_color); ?>;">
|
||||
<div class="related-post-no-image-content">
|
||||
<h3 class="related-post-no-image-title">
|
||||
<?php the_title(); ?>
|
||||
</h3>
|
||||
|
||||
<?php if ($show_category) : ?>
|
||||
<?php
|
||||
$categories = get_the_category();
|
||||
if (!empty($categories)) :
|
||||
$category = $categories[0];
|
||||
?>
|
||||
<span class="related-post-category no-image">
|
||||
<?php echo esc_html($category->name); ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="related-post-content">
|
||||
|
||||
<?php if ($has_thumbnail) : ?>
|
||||
<h3 class="related-post-title">
|
||||
<?php the_title(); ?>
|
||||
</h3>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($show_excerpt && $excerpt_length > 0) : ?>
|
||||
<div class="related-post-excerpt">
|
||||
<?php echo wp_trim_words(get_the_excerpt(), $excerpt_length, '...'); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($show_date) : ?>
|
||||
<div class="related-post-meta">
|
||||
<time class="related-post-date" datetime="<?php echo esc_attr(get_the_date('c')); ?>">
|
||||
<?php echo esc_html(get_the_date()); ?>
|
||||
</time>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
|
||||
</a>
|
||||
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<?php endwhile; ?>
|
||||
</div><!-- .row -->
|
||||
|
||||
</div><!-- .related-posts-container -->
|
||||
</section><!-- .related-posts-section -->
|
||||
|
||||
<?php
|
||||
// Reset post data
|
||||
wp_reset_postdata();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Bootstrap column class based on number of columns
|
||||
*
|
||||
* @param int $columns Number of columns (1-4)
|
||||
* @return string Bootstrap column classes
|
||||
*/
|
||||
function roi_get_column_class($columns) {
|
||||
$columns = absint($columns);
|
||||
|
||||
switch ($columns) {
|
||||
case 1:
|
||||
return 'col-12';
|
||||
case 2:
|
||||
return 'col-12 col-md-6';
|
||||
case 3:
|
||||
return 'col-12 col-sm-6 col-lg-4';
|
||||
case 4:
|
||||
return 'col-12 col-sm-6 col-lg-3';
|
||||
default:
|
||||
return 'col-12 col-sm-6 col-lg-4'; // Default to 3 columns
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook related posts display after post content
|
||||
*/
|
||||
function roi_hook_related_posts() {
|
||||
if (is_single() && !is_attachment()) {
|
||||
roi_display_related_posts();
|
||||
}
|
||||
}
|
||||
add_action('roi_after_post_content', 'roi_hook_related_posts');
|
||||
|
||||
/**
|
||||
* Enqueue related posts styles
|
||||
*/
|
||||
function roi_enqueue_related_posts_styles() {
|
||||
if (is_single() && !is_attachment()) {
|
||||
$enabled = get_option('roi_related_posts_enabled', true);
|
||||
|
||||
if ($enabled) {
|
||||
wp_enqueue_style(
|
||||
'roirelated-posts',
|
||||
get_template_directory_uri() . '/Assets/Css/related-posts.css',
|
||||
array('roibootstrap'),
|
||||
ROI_VERSION,
|
||||
'all'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
add_action('wp_enqueue_scripts', 'roi_enqueue_related_posts_styles');
|
||||
|
||||
/**
|
||||
* Register related posts settings
|
||||
* These can be configured via theme options or customizer
|
||||
*/
|
||||
function roi_related_posts_default_options() {
|
||||
// Set default options if they don't exist
|
||||
$defaults = array(
|
||||
'roi_related_posts_enabled' => true,
|
||||
'roi_related_posts_title' => __('Related Posts', 'roi-theme'),
|
||||
'roi_related_posts_count' => 3,
|
||||
'roi_related_posts_columns' => 3,
|
||||
'roi_related_posts_show_excerpt' => true,
|
||||
'roi_related_posts_excerpt_length' => 20,
|
||||
'roi_related_posts_show_date' => true,
|
||||
'roi_related_posts_show_category' => true,
|
||||
'roi_related_posts_bg_colors' => array(
|
||||
'#1a73e8', // Blue
|
||||
'#e91e63', // Pink
|
||||
'#4caf50', // Green
|
||||
'#ff9800', // Orange
|
||||
'#9c27b0', // Purple
|
||||
'#00bcd4', // Cyan
|
||||
),
|
||||
);
|
||||
|
||||
foreach ($defaults as $option => $value) {
|
||||
if (get_option($option) === false) {
|
||||
add_option($option, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
add_action('after_setup_theme', 'roi_related_posts_default_options');
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace ROITheme\Public\AdsensePlacement\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||
|
||||
/**
|
||||
* Renderer para slots de AdSense
|
||||
@@ -36,6 +37,11 @@ final class AdsensePlacementRenderer
|
||||
*/
|
||||
public function renderSlot(array $settings, string $location): string
|
||||
{
|
||||
// 0. Verificar visibilidad por tipo de página y exclusiones (Plan 99.10/99.11)
|
||||
if (!PageVisibilityHelper::shouldShow('adsense-placement')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 1. Validar is_enabled
|
||||
if (!($settings['visibility']['is_enabled'] ?? false)) {
|
||||
return '';
|
||||
|
||||
314
Public/ArchiveHeader/Infrastructure/Ui/ArchiveHeaderRenderer.php
Normal file
314
Public/ArchiveHeader/Infrastructure/Ui/ArchiveHeaderRenderer.php
Normal file
@@ -0,0 +1,314 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\ArchiveHeader\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||
|
||||
/**
|
||||
* ArchiveHeaderRenderer - Renderiza cabecera dinamica para paginas de archivo
|
||||
*
|
||||
* RESPONSABILIDAD: Generar HTML y CSS del componente Archive Header
|
||||
*
|
||||
* CARACTERISTICAS:
|
||||
* - Deteccion automatica del tipo de archivo (categoria, tag, autor, fecha, busqueda)
|
||||
* - Titulo y descripcion dinamicos
|
||||
* - Contador de posts opcional
|
||||
* - Estilos 100% desde BD via CSSGenerator
|
||||
*
|
||||
* @package ROITheme\Public\ArchiveHeader\Infrastructure\Ui
|
||||
*/
|
||||
final class ArchiveHeaderRenderer implements RendererInterface
|
||||
{
|
||||
private const COMPONENT_NAME = 'archive-header';
|
||||
|
||||
public function __construct(
|
||||
private CSSGeneratorInterface $cssGenerator
|
||||
) {}
|
||||
|
||||
public function render(Component $component): string
|
||||
{
|
||||
$data = $component->getData();
|
||||
|
||||
if (!$this->isEnabled($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$visibilityClass = $this->getVisibilityClass($data);
|
||||
if ($visibilityClass === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$css = $this->generateCSS($data);
|
||||
$html = $this->buildHTML($data, $visibilityClass);
|
||||
|
||||
return sprintf("<style>%s</style>\n%s", $css, $html);
|
||||
}
|
||||
|
||||
public function supports(string $componentType): bool
|
||||
{
|
||||
return $componentType === self::COMPONENT_NAME;
|
||||
}
|
||||
|
||||
private function isEnabled(array $data): bool
|
||||
{
|
||||
$value = $data['visibility']['is_enabled'] ?? false;
|
||||
return $value === true || $value === '1' || $value === 1;
|
||||
}
|
||||
|
||||
private function getVisibilityClass(array $data): ?string
|
||||
{
|
||||
$showDesktop = $data['visibility']['show_on_desktop'] ?? true;
|
||||
$showDesktop = $showDesktop === true || $showDesktop === '1' || $showDesktop === 1;
|
||||
$showMobile = $data['visibility']['show_on_mobile'] ?? true;
|
||||
$showMobile = $showMobile === true || $showMobile === '1' || $showMobile === 1;
|
||||
|
||||
if (!$showDesktop && !$showMobile) {
|
||||
return null;
|
||||
}
|
||||
if (!$showDesktop && $showMobile) {
|
||||
return 'd-lg-none';
|
||||
}
|
||||
if ($showDesktop && !$showMobile) {
|
||||
return 'd-none d-lg-block';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private function generateCSS(array $data): string
|
||||
{
|
||||
$colors = $data['colors'] ?? [];
|
||||
$spacing = $data['spacing'] ?? [];
|
||||
$typography = $data['typography'] ?? [];
|
||||
$behavior = $data['behavior'] ?? [];
|
||||
|
||||
$cssRules = [];
|
||||
|
||||
// Container
|
||||
$marginTop = $spacing['margin_top'] ?? '2rem';
|
||||
$marginBottom = $spacing['margin_bottom'] ?? '2rem';
|
||||
$padding = $spacing['padding'] ?? '1.5rem';
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.archive-header', [
|
||||
'margin-top' => $marginTop,
|
||||
'margin-bottom' => $marginBottom,
|
||||
'padding' => $padding,
|
||||
]);
|
||||
|
||||
// Sticky behavior
|
||||
$isSticky = $behavior['is_sticky'] ?? false;
|
||||
$isSticky = $isSticky === true || $isSticky === '1' || $isSticky === 1;
|
||||
|
||||
if ($isSticky) {
|
||||
$stickyOffset = $behavior['sticky_offset'] ?? '0';
|
||||
$cssRules[] = $this->cssGenerator->generate('.archive-header', [
|
||||
'position' => 'sticky',
|
||||
'top' => $stickyOffset,
|
||||
'z-index' => '100',
|
||||
'background' => '#ffffff',
|
||||
]);
|
||||
}
|
||||
|
||||
// Title
|
||||
$titleColor = $colors['title_color'] ?? '#0E2337';
|
||||
$titleSize = $typography['title_size'] ?? '2rem';
|
||||
$titleWeight = $typography['title_weight'] ?? '700';
|
||||
$titleMarginBottom = $spacing['title_margin_bottom'] ?? '0.5rem';
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.archive-header__title', [
|
||||
'color' => $titleColor,
|
||||
'font-size' => $titleSize,
|
||||
'font-weight' => $titleWeight,
|
||||
'margin-bottom' => $titleMarginBottom,
|
||||
'line-height' => '1.2',
|
||||
]);
|
||||
|
||||
// Prefix
|
||||
$prefixColor = $colors['prefix_color'] ?? '#6b7280';
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.archive-header__prefix', [
|
||||
'color' => $prefixColor,
|
||||
'font-weight' => '400',
|
||||
]);
|
||||
|
||||
// Description
|
||||
$descColor = $colors['description_color'] ?? '#6b7280';
|
||||
$descSize = $typography['description_size'] ?? '1rem';
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.archive-header__description', [
|
||||
'color' => $descColor,
|
||||
'font-size' => $descSize,
|
||||
'margin-top' => '0.5rem',
|
||||
'line-height' => '1.6',
|
||||
]);
|
||||
|
||||
// Post count badge
|
||||
$countBgColor = $colors['count_bg_color'] ?? '#FF8600';
|
||||
$countTextColor = $colors['count_text_color'] ?? '#ffffff';
|
||||
$countSize = $typography['count_size'] ?? '0.875rem';
|
||||
$countPadding = $spacing['count_padding'] ?? '0.25rem 0.75rem';
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.archive-header__count', [
|
||||
'background-color' => $countBgColor,
|
||||
'color' => $countTextColor,
|
||||
'font-size' => $countSize,
|
||||
'padding' => $countPadding,
|
||||
'border-radius' => '9999px',
|
||||
'font-weight' => '500',
|
||||
'display' => 'inline-block',
|
||||
'margin-left' => '0.75rem',
|
||||
]);
|
||||
|
||||
return implode("\n", $cssRules);
|
||||
}
|
||||
|
||||
private function buildHTML(array $data, string $visibilityClass): string
|
||||
{
|
||||
$content = $data['content'] ?? [];
|
||||
$typography = $data['typography'] ?? [];
|
||||
|
||||
$headingLevel = $typography['heading_level'] ?? 'h1';
|
||||
$showPostCount = $content['show_post_count'] ?? true;
|
||||
$showPostCount = $showPostCount === true || $showPostCount === '1' || $showPostCount === 1;
|
||||
$showDescription = $content['show_description'] ?? true;
|
||||
$showDescription = $showDescription === true || $showDescription === '1' || $showDescription === 1;
|
||||
|
||||
// Get context-specific title and description
|
||||
$titleData = $this->getContextualTitle($content);
|
||||
$title = $titleData['title'];
|
||||
$prefix = $titleData['prefix'];
|
||||
$description = $showDescription ? $titleData['description'] : '';
|
||||
|
||||
// Get post count
|
||||
$postCount = $this->getPostCount();
|
||||
$countSingular = $content['posts_count_singular'] ?? 'publicacion';
|
||||
$countPlural = $content['posts_count_plural'] ?? 'publicaciones';
|
||||
$countText = $postCount === 1 ? $countSingular : $countPlural;
|
||||
|
||||
$containerClass = 'archive-header';
|
||||
if (!empty($visibilityClass)) {
|
||||
$containerClass .= ' ' . $visibilityClass;
|
||||
}
|
||||
|
||||
$html = sprintf('<div class="%s">', esc_attr($containerClass));
|
||||
|
||||
// Title with optional prefix
|
||||
$html .= sprintf('<%s class="archive-header__title">', esc_attr($headingLevel));
|
||||
|
||||
if (!empty($prefix)) {
|
||||
$html .= sprintf(
|
||||
'<span class="archive-header__prefix">%s</span> ',
|
||||
esc_html($prefix)
|
||||
);
|
||||
}
|
||||
|
||||
$html .= esc_html($title);
|
||||
|
||||
// Post count badge
|
||||
if ($showPostCount && $postCount > 0) {
|
||||
$html .= sprintf(
|
||||
'<span class="archive-header__count">%d %s</span>',
|
||||
$postCount,
|
||||
esc_html($countText)
|
||||
);
|
||||
}
|
||||
|
||||
$html .= sprintf('</%s>', esc_attr($headingLevel));
|
||||
|
||||
// Description
|
||||
if (!empty($description)) {
|
||||
$html .= sprintf(
|
||||
'<p class="archive-header__description">%s</p>',
|
||||
esc_html($description)
|
||||
);
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get contextual title based on current page type
|
||||
*
|
||||
* @param array $content Content settings from schema
|
||||
* @return array{title: string, prefix: string, description: string}
|
||||
*/
|
||||
private function getContextualTitle(array $content): array
|
||||
{
|
||||
$title = '';
|
||||
$prefix = '';
|
||||
$description = '';
|
||||
|
||||
if (is_category()) {
|
||||
$prefix = $content['category_prefix'] ?? 'Categoria:';
|
||||
$title = single_cat_title('', false) ?: '';
|
||||
$description = category_description() ?: '';
|
||||
} elseif (is_tag()) {
|
||||
$prefix = $content['tag_prefix'] ?? 'Etiqueta:';
|
||||
$title = single_tag_title('', false) ?: '';
|
||||
$description = tag_description() ?: '';
|
||||
} elseif (is_author()) {
|
||||
$prefix = $content['author_prefix'] ?? 'Articulos de:';
|
||||
$title = get_the_author() ?: '';
|
||||
$description = get_the_author_meta('description') ?: '';
|
||||
} elseif (is_date()) {
|
||||
$prefix = $content['date_prefix'] ?? 'Archivo:';
|
||||
$title = $this->getDateArchiveTitle();
|
||||
$description = '';
|
||||
} elseif (is_search()) {
|
||||
$prefix = $content['search_prefix'] ?? 'Resultados para:';
|
||||
$title = get_search_query() ?: '';
|
||||
$description = '';
|
||||
} elseif (is_home()) {
|
||||
$prefix = '';
|
||||
$title = $content['blog_title'] ?? 'Blog';
|
||||
$description = '';
|
||||
} elseif (is_archive()) {
|
||||
$prefix = '';
|
||||
$title = get_the_archive_title() ?: 'Archivo';
|
||||
$description = get_the_archive_description() ?: '';
|
||||
} else {
|
||||
$prefix = '';
|
||||
$title = $content['blog_title'] ?? 'Blog';
|
||||
$description = '';
|
||||
}
|
||||
|
||||
return [
|
||||
'title' => $title,
|
||||
'prefix' => $prefix,
|
||||
'description' => strip_tags($description),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get formatted title for date archives
|
||||
*/
|
||||
private function getDateArchiveTitle(): string
|
||||
{
|
||||
if (is_day()) {
|
||||
return get_the_date();
|
||||
} elseif (is_month()) {
|
||||
return get_the_date('F Y');
|
||||
} elseif (is_year()) {
|
||||
return get_the_date('Y');
|
||||
}
|
||||
return get_the_archive_title() ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total post count for current query
|
||||
*/
|
||||
private function getPostCount(): int
|
||||
{
|
||||
global $wp_query;
|
||||
return $wp_query->found_posts ?? 0;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ namespace ROITheme\Public\ContactForm\Infrastructure\Ui;
|
||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||
|
||||
/**
|
||||
* ContactFormRenderer - Renderiza formulario de contacto con webhook
|
||||
@@ -22,6 +23,8 @@ use ROITheme\Shared\Domain\Entities\Component;
|
||||
*/
|
||||
final class ContactFormRenderer implements RendererInterface
|
||||
{
|
||||
private const COMPONENT_NAME = 'contact-form';
|
||||
|
||||
public function __construct(
|
||||
private CSSGeneratorInterface $cssGenerator
|
||||
) {}
|
||||
@@ -34,7 +37,7 @@ final class ContactFormRenderer implements RendererInterface
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!$this->shouldShowOnCurrentPage($data)) {
|
||||
if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -67,7 +70,7 @@ final class ContactFormRenderer implements RendererInterface
|
||||
|
||||
public function supports(string $componentType): bool
|
||||
{
|
||||
return $componentType === 'contact-form';
|
||||
return $componentType === self::COMPONENT_NAME;
|
||||
}
|
||||
|
||||
private function isEnabled(array $data): bool
|
||||
@@ -76,22 +79,6 @@ final class ContactFormRenderer implements RendererInterface
|
||||
return $value === true || $value === '1' || $value === 1;
|
||||
}
|
||||
|
||||
private function shouldShowOnCurrentPage(array $data): bool
|
||||
{
|
||||
$showOn = $data['visibility']['show_on_pages'] ?? 'all';
|
||||
|
||||
switch ($showOn) {
|
||||
case 'all':
|
||||
return true;
|
||||
case 'posts':
|
||||
return is_single();
|
||||
case 'pages':
|
||||
return is_page();
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private function getVisibilityClass(array $data): ?string
|
||||
{
|
||||
$showDesktop = $data['visibility']['show_on_desktop'] ?? true;
|
||||
|
||||
@@ -9,11 +9,10 @@ use ROITheme\Public\CriticalCSS\Domain\Contracts\CriticalCSSCacheInterface;
|
||||
* Inyecta CSS critico en wp_head
|
||||
*
|
||||
* Prioridades:
|
||||
* - P:-2 Font preload (antes de variables)
|
||||
* - P:-1 Variables CSS (antes de Bootstrap)
|
||||
* - P:2 Responsive critico (despues de Bootstrap critico)
|
||||
*
|
||||
* NOTA: Font preload deshabilitado - usando system fonts para CERO flash
|
||||
*
|
||||
* @package ROITheme\Public\CriticalCSS\Infrastructure\Services
|
||||
*/
|
||||
final class CriticalCSSInjector
|
||||
@@ -22,11 +21,23 @@ final class CriticalCSSInjector
|
||||
private readonly CriticalCSSCacheInterface $cache
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Fuentes criticas para preload (pesos usados en navbar above-the-fold)
|
||||
*/
|
||||
private const CRITICAL_FONTS = [
|
||||
'/Assets/Fonts/poppins-v24-latin-regular.woff2', // 400 - body text
|
||||
'/Assets/Fonts/poppins-v24-latin-600.woff2', // 600 - navbar brand
|
||||
'/Assets/Fonts/poppins-v24-latin-700.woff2', // 700 - headings
|
||||
];
|
||||
|
||||
/**
|
||||
* Registra hooks de WordPress
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
// Font preload: P:-2 (antes de todo, incluso variables)
|
||||
add_action('wp_head', [$this, 'preloadFonts'], -2);
|
||||
|
||||
// Variables CSS: P:-1 (antes de CriticalBootstrapService P:0)
|
||||
add_action('wp_head', [$this, 'injectVariables'], -1);
|
||||
|
||||
@@ -38,6 +49,25 @@ final class CriticalCSSInjector
|
||||
add_action('wp_enqueue_scripts', [$this, 'dequeueInlinedCSS'], 999);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inyecta preload links para fuentes criticas
|
||||
*
|
||||
* Resuelve el problema de "font swap" donde el fallback (106% size-adjust)
|
||||
* causa un salto visual cuando Poppins se carga.
|
||||
* Con preload, las fuentes llegan antes del primer paint.
|
||||
*/
|
||||
public function preloadFonts(): void
|
||||
{
|
||||
echo "<!-- TIPO 4: Font preload para evitar CLS -->\n";
|
||||
|
||||
foreach (self::CRITICAL_FONTS as $font) {
|
||||
printf(
|
||||
'<link rel="preload" href="%s" as="font" type="font/woff2" crossorigin>' . "\n",
|
||||
esc_url(get_template_directory_uri() . $font)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inyecta variables CSS criticas
|
||||
*/
|
||||
|
||||
@@ -6,6 +6,8 @@ namespace ROITheme\Public\CtaBoxSidebar\Infrastructure\Ui;
|
||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||
use ROITheme\Shared\Infrastructure\Services\UserVisibilityHelper;
|
||||
|
||||
/**
|
||||
* CtaBoxSidebarRenderer - Renderiza caja CTA en sidebar
|
||||
@@ -27,6 +29,12 @@ use ROITheme\Shared\Domain\Entities\Component;
|
||||
*/
|
||||
final class CtaBoxSidebarRenderer implements RendererInterface
|
||||
{
|
||||
/**
|
||||
* Nombre del componente para visibilidad
|
||||
* Evita strings hardcodeados y facilita mantenimiento
|
||||
*/
|
||||
private const COMPONENT_NAME = 'cta-box-sidebar';
|
||||
|
||||
public function __construct(
|
||||
private CSSGeneratorInterface $cssGenerator
|
||||
) {}
|
||||
@@ -39,7 +47,13 @@ final class CtaBoxSidebarRenderer implements RendererInterface
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!$this->shouldShowOnCurrentPage($data)) {
|
||||
// Evaluar visibilidad por tipo de página (usa Helper, NO cambia constructor)
|
||||
if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Validar visibilidad por usuario logueado
|
||||
if (!UserVisibilityHelper::shouldShowForUser($data['visibility'] ?? [])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -52,7 +66,7 @@ final class CtaBoxSidebarRenderer implements RendererInterface
|
||||
|
||||
public function supports(string $componentType): bool
|
||||
{
|
||||
return $componentType === 'cta-box-sidebar';
|
||||
return $componentType === self::COMPONENT_NAME;
|
||||
}
|
||||
|
||||
private function isEnabled(array $data): bool
|
||||
@@ -60,22 +74,6 @@ final class CtaBoxSidebarRenderer implements RendererInterface
|
||||
return ($data['visibility']['is_enabled'] ?? false) === true;
|
||||
}
|
||||
|
||||
private function shouldShowOnCurrentPage(array $data): bool
|
||||
{
|
||||
$showOn = $data['visibility']['show_on_pages'] ?? 'posts';
|
||||
|
||||
switch ($showOn) {
|
||||
case 'all':
|
||||
return true;
|
||||
case 'posts':
|
||||
return is_single();
|
||||
case 'pages':
|
||||
return is_page();
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private function generateCSS(array $data): string
|
||||
{
|
||||
$colors = $data['colors'] ?? [];
|
||||
|
||||
@@ -6,6 +6,8 @@ namespace ROITheme\Public\CtaLetsTalk\Infrastructure\Ui;
|
||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||
use ROITheme\Shared\Infrastructure\Services\UserVisibilityHelper;
|
||||
|
||||
/**
|
||||
* Class CtaLetsTalkRenderer
|
||||
@@ -34,6 +36,8 @@ use ROITheme\Shared\Domain\Entities\Component;
|
||||
*/
|
||||
final class CtaLetsTalkRenderer implements RendererInterface
|
||||
{
|
||||
private const COMPONENT_NAME = 'cta-lets-talk';
|
||||
|
||||
/**
|
||||
* @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS
|
||||
*/
|
||||
@@ -54,7 +58,12 @@ final class CtaLetsTalkRenderer implements RendererInterface
|
||||
}
|
||||
|
||||
// Validar visibilidad por página
|
||||
if (!$this->shouldShowOnCurrentPage($data)) {
|
||||
if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Validar visibilidad por usuario logueado
|
||||
if (!UserVisibilityHelper::shouldShowForUser($data['visibility'] ?? [])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -77,7 +86,7 @@ final class CtaLetsTalkRenderer implements RendererInterface
|
||||
*/
|
||||
public function supports(string $componentType): bool
|
||||
{
|
||||
return $componentType === 'cta-lets-talk';
|
||||
return $componentType === self::COMPONENT_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,25 +100,6 @@ final class CtaLetsTalkRenderer implements RendererInterface
|
||||
return ($data['visibility']['is_enabled'] ?? false) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si debe mostrarse en la página actual
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return bool
|
||||
*/
|
||||
private function shouldShowOnCurrentPage(array $data): bool
|
||||
{
|
||||
$showOn = $data['visibility']['show_on_pages'] ?? 'all';
|
||||
|
||||
return match ($showOn) {
|
||||
'all' => true,
|
||||
'home' => is_front_page(),
|
||||
'posts' => is_single(),
|
||||
'pages' => is_page(),
|
||||
default => true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcular clases de visibilidad responsive
|
||||
*
|
||||
|
||||
@@ -6,6 +6,8 @@ namespace ROITheme\Public\CtaPost\Infrastructure\Ui;
|
||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||
use ROITheme\Shared\Infrastructure\Services\UserVisibilityHelper;
|
||||
|
||||
/**
|
||||
* CtaPostRenderer - Renderiza CTA promocional debajo del contenido
|
||||
@@ -22,6 +24,8 @@ use ROITheme\Shared\Domain\Entities\Component;
|
||||
*/
|
||||
final class CtaPostRenderer implements RendererInterface
|
||||
{
|
||||
private const COMPONENT_NAME = 'cta-post';
|
||||
|
||||
public function __construct(
|
||||
private CSSGeneratorInterface $cssGenerator
|
||||
) {}
|
||||
@@ -34,7 +38,12 @@ final class CtaPostRenderer implements RendererInterface
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!$this->shouldShowOnCurrentPage($data)) {
|
||||
if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Validar visibilidad por usuario logueado
|
||||
if (!UserVisibilityHelper::shouldShowForUser($data['visibility'] ?? [])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -46,7 +55,7 @@ final class CtaPostRenderer implements RendererInterface
|
||||
|
||||
public function supports(string $componentType): bool
|
||||
{
|
||||
return $componentType === 'cta-post';
|
||||
return $componentType === self::COMPONENT_NAME;
|
||||
}
|
||||
|
||||
private function isEnabled(array $data): bool
|
||||
@@ -55,22 +64,6 @@ final class CtaPostRenderer implements RendererInterface
|
||||
return $value === true || $value === '1' || $value === 1;
|
||||
}
|
||||
|
||||
private function shouldShowOnCurrentPage(array $data): bool
|
||||
{
|
||||
$showOn = $data['visibility']['show_on_pages'] ?? 'posts';
|
||||
|
||||
switch ($showOn) {
|
||||
case 'all':
|
||||
return true;
|
||||
case 'posts':
|
||||
return is_single();
|
||||
case 'pages':
|
||||
return is_page();
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private function generateCSS(array $data): string
|
||||
{
|
||||
$colors = $data['colors'] ?? [];
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace ROITheme\Public\FeaturedImage\Infrastructure\Ui;
|
||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||
|
||||
/**
|
||||
* FeaturedImageRenderer - Renderiza la imagen destacada del post
|
||||
@@ -27,6 +28,8 @@ use ROITheme\Shared\Domain\Entities\Component;
|
||||
*/
|
||||
final class FeaturedImageRenderer implements RendererInterface
|
||||
{
|
||||
private const COMPONENT_NAME = 'featured-image';
|
||||
|
||||
public function __construct(
|
||||
private CSSGeneratorInterface $cssGenerator
|
||||
) {}
|
||||
@@ -39,7 +42,7 @@ final class FeaturedImageRenderer implements RendererInterface
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!$this->shouldShowOnCurrentPage($data)) {
|
||||
if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -63,7 +66,7 @@ final class FeaturedImageRenderer implements RendererInterface
|
||||
|
||||
public function supports(string $componentType): bool
|
||||
{
|
||||
return $componentType === 'featured-image';
|
||||
return $componentType === self::COMPONENT_NAME;
|
||||
}
|
||||
|
||||
private function isEnabled(array $data): bool
|
||||
@@ -71,25 +74,24 @@ final class FeaturedImageRenderer implements RendererInterface
|
||||
return ($data['visibility']['is_enabled'] ?? false) === true;
|
||||
}
|
||||
|
||||
private function shouldShowOnCurrentPage(array $data): bool
|
||||
{
|
||||
$showOn = $data['visibility']['show_on_pages'] ?? 'posts';
|
||||
|
||||
switch ($showOn) {
|
||||
case 'all':
|
||||
return true;
|
||||
case 'posts':
|
||||
return is_single();
|
||||
case 'pages':
|
||||
return is_page();
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private function hasPostThumbnail(): bool
|
||||
{
|
||||
return is_singular() && has_post_thumbnail();
|
||||
if (!is_singular() || !has_post_thumbnail()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verificar que el archivo físico exista, no solo el attachment ID
|
||||
$thumbnailId = get_post_thumbnail_id();
|
||||
if (!$thumbnailId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$filePath = get_attached_file($thumbnailId);
|
||||
if (empty($filePath) || !file_exists($filePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace ROITheme\Public\Footer\Infrastructure\Ui;
|
||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||
|
||||
/**
|
||||
* FooterRenderer - Renderiza el footer del sitio
|
||||
@@ -34,9 +35,14 @@ final class FooterRenderer implements RendererInterface
|
||||
|
||||
public function render(Component $component): string
|
||||
{
|
||||
// Verificar visibilidad por tipo de página y exclusiones (Plan 99.10/99.11)
|
||||
if (!PageVisibilityHelper::shouldShow('footer')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$data = $component->getData();
|
||||
|
||||
// Validar visibilidad
|
||||
// Validar visibilidad básica
|
||||
$visibility = $data['visibility'] ?? [];
|
||||
if (!($visibility['is_enabled'] ?? true)) {
|
||||
return '';
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace ROITheme\Public\Hero\Infrastructure\Ui;
|
||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||
|
||||
/**
|
||||
* Class HeroRenderer
|
||||
@@ -33,6 +34,8 @@ use ROITheme\Shared\Domain\Entities\Component;
|
||||
*/
|
||||
final class HeroRenderer implements RendererInterface
|
||||
{
|
||||
private const COMPONENT_NAME = 'hero';
|
||||
|
||||
/**
|
||||
* @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS
|
||||
*/
|
||||
@@ -48,7 +51,7 @@ final class HeroRenderer implements RendererInterface
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!$this->shouldShowOnCurrentPage($data)) {
|
||||
if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -68,7 +71,7 @@ final class HeroRenderer implements RendererInterface
|
||||
|
||||
public function supports(string $componentType): bool
|
||||
{
|
||||
return $componentType === 'hero';
|
||||
return $componentType === self::COMPONENT_NAME;
|
||||
}
|
||||
|
||||
private function isEnabled(array $data): bool
|
||||
@@ -76,24 +79,6 @@ final class HeroRenderer implements RendererInterface
|
||||
return ($data['visibility']['is_enabled'] ?? false) === true;
|
||||
}
|
||||
|
||||
private function shouldShowOnCurrentPage(array $data): bool
|
||||
{
|
||||
$showOn = $data['visibility']['show_on_pages'] ?? 'posts';
|
||||
|
||||
switch ($showOn) {
|
||||
case 'all':
|
||||
return true;
|
||||
case 'home':
|
||||
return is_front_page() || is_home();
|
||||
case 'posts':
|
||||
return is_single();
|
||||
case 'pages':
|
||||
return is_page();
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generar CSS usando CSSGeneratorService
|
||||
*
|
||||
@@ -148,6 +133,9 @@ final class HeroRenderer implements RendererInterface
|
||||
'padding' => "{$paddingVertical} 0",
|
||||
'margin-bottom' => $marginBottom,
|
||||
'min-height' => $minHeight,
|
||||
'display' => 'flex',
|
||||
'align-items' => 'center',
|
||||
'justify-content' => 'center',
|
||||
]);
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.hero-section__title', [
|
||||
|
||||
@@ -1,478 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\HeroSection\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
|
||||
/**
|
||||
* HeroSectionRenderer - Renderiza la sección hero con badges y título
|
||||
*
|
||||
* RESPONSABILIDAD: Generar HTML de la sección hero
|
||||
*
|
||||
* CARACTERÍSTICAS:
|
||||
* - Badges de categorías con múltiples fuentes de datos
|
||||
* - Título H1 con gradiente opcional
|
||||
* - Múltiples tipos de fondo (color, gradiente, imagen)
|
||||
* - Lógica condicional de visibilidad por tipo de página
|
||||
*
|
||||
* @package ROITheme\Public\HeroSection\Presentation
|
||||
*/
|
||||
final class HeroSectionRenderer implements RendererInterface
|
||||
{
|
||||
public function render(Component $component): string
|
||||
{
|
||||
$data = $component->getData();
|
||||
|
||||
if (!$this->isEnabled($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!$this->shouldShowOnCurrentPage($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$classes = $this->buildSectionClasses($data);
|
||||
$styles = $this->buildInlineStyles($data);
|
||||
|
||||
$html = sprintf(
|
||||
'<div class="%s"%s>',
|
||||
esc_attr($classes),
|
||||
$styles ? ' style="' . esc_attr($styles) . '"' : ''
|
||||
);
|
||||
|
||||
$html .= '<div class="container">';
|
||||
|
||||
// Categories badges
|
||||
if ($this->shouldShowCategories($data)) {
|
||||
$html .= $this->buildCategoriesBadges($data);
|
||||
}
|
||||
|
||||
// Title
|
||||
$html .= $this->buildTitle($data);
|
||||
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Custom styles
|
||||
$html .= $this->buildCustomStyles($data);
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function isEnabled(array $data): bool
|
||||
{
|
||||
return isset($data['visibility']['is_enabled']) &&
|
||||
$data['visibility']['is_enabled'] === true;
|
||||
}
|
||||
|
||||
private function shouldShowOnCurrentPage(array $data): bool
|
||||
{
|
||||
$showOn = $data['visibility']['show_on_pages'] ?? 'posts';
|
||||
|
||||
switch ($showOn) {
|
||||
case 'all':
|
||||
return true;
|
||||
|
||||
case 'home':
|
||||
return is_front_page();
|
||||
|
||||
case 'posts':
|
||||
return is_single() && get_post_type() === 'post';
|
||||
|
||||
case 'pages':
|
||||
return is_page();
|
||||
|
||||
case 'custom':
|
||||
$postTypes = $data['visibility']['custom_post_types'] ?? '';
|
||||
$allowedTypes = array_map('trim', explode(',', $postTypes));
|
||||
return in_array(get_post_type(), $allowedTypes, true);
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private function shouldShowCategories(array $data): bool
|
||||
{
|
||||
return isset($data['categories']['show_categories']) &&
|
||||
$data['categories']['show_categories'] === true;
|
||||
}
|
||||
|
||||
private function buildSectionClasses(array $data): string
|
||||
{
|
||||
$classes = ['container-fluid', 'hero-title'];
|
||||
|
||||
$paddingClass = $this->getPaddingClass($data['styles']['padding_vertical'] ?? 'normal');
|
||||
$classes[] = $paddingClass;
|
||||
|
||||
$marginClass = $this->getMarginClass($data['styles']['margin_bottom'] ?? 'normal');
|
||||
if ($marginClass) {
|
||||
$classes[] = $marginClass;
|
||||
}
|
||||
|
||||
return implode(' ', $classes);
|
||||
}
|
||||
|
||||
private function getPaddingClass(string $padding): string
|
||||
{
|
||||
$paddings = [
|
||||
'compact' => 'py-3',
|
||||
'normal' => 'py-5',
|
||||
'spacious' => 'py-6',
|
||||
'extra-spacious' => 'py-7'
|
||||
];
|
||||
|
||||
return $paddings[$padding] ?? 'py-5';
|
||||
}
|
||||
|
||||
private function getMarginClass(string $margin): string
|
||||
{
|
||||
$margins = [
|
||||
'none' => '',
|
||||
'small' => 'mb-2',
|
||||
'normal' => 'mb-4',
|
||||
'large' => 'mb-5'
|
||||
];
|
||||
|
||||
return $margins[$margin] ?? 'mb-4';
|
||||
}
|
||||
|
||||
private function buildInlineStyles(array $data): string
|
||||
{
|
||||
$styles = [];
|
||||
$backgroundType = $data['styles']['background_type'] ?? 'gradient';
|
||||
|
||||
switch ($backgroundType) {
|
||||
case 'color':
|
||||
$bgColor = $data['styles']['background_color'] ?? '#1e3a5f';
|
||||
$styles[] = "background-color: {$bgColor}";
|
||||
break;
|
||||
|
||||
case 'gradient':
|
||||
$startColor = $data['styles']['gradient_start_color'] ?? '#1e3a5f';
|
||||
$endColor = $data['styles']['gradient_end_color'] ?? '#2c5282';
|
||||
$angle = $data['styles']['gradient_angle'] ?? 135;
|
||||
$styles[] = "background: linear-gradient({$angle}deg, {$startColor}, {$endColor})";
|
||||
break;
|
||||
|
||||
case 'image':
|
||||
$imageUrl = $data['styles']['background_image_url'] ?? '';
|
||||
if (!empty($imageUrl)) {
|
||||
$styles[] = "background-image: url('" . esc_url($imageUrl) . "')";
|
||||
$styles[] = "background-size: cover";
|
||||
$styles[] = "background-position: center";
|
||||
$styles[] = "background-repeat: no-repeat";
|
||||
|
||||
if (isset($data['styles']['background_overlay']) && $data['styles']['background_overlay']) {
|
||||
$opacity = ($data['styles']['overlay_opacity'] ?? 60) / 100;
|
||||
$styles[] = "position: relative";
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Text color
|
||||
if (!empty($data['styles']['text_color'])) {
|
||||
$styles[] = 'color: ' . $data['styles']['text_color'];
|
||||
}
|
||||
|
||||
return implode('; ', $styles);
|
||||
}
|
||||
|
||||
private function buildCategoriesBadges(array $data): string
|
||||
{
|
||||
$categories = $this->getCategories($data);
|
||||
|
||||
if (empty($categories)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$maxCategories = $data['categories']['max_categories'] ?? 5;
|
||||
$categories = array_slice($categories, 0, $maxCategories);
|
||||
|
||||
$alignment = $data['categories']['categories_alignment'] ?? 'center';
|
||||
$alignmentClasses = [
|
||||
'left' => 'justify-content-start',
|
||||
'center' => 'justify-content-center',
|
||||
'right' => 'justify-content-end'
|
||||
];
|
||||
$alignmentClass = $alignmentClasses[$alignment] ?? 'justify-content-center';
|
||||
|
||||
$icon = $data['categories']['category_icon'] ?? 'bi-folder-fill';
|
||||
if (strpos($icon, 'bi-') !== 0) {
|
||||
$icon = 'bi-' . $icon;
|
||||
}
|
||||
|
||||
$html = sprintf('<div class="mb-3 d-flex %s">', esc_attr($alignmentClass));
|
||||
$html .= '<div class="d-flex gap-2 flex-wrap justify-content-center">';
|
||||
|
||||
foreach ($categories as $category) {
|
||||
$html .= sprintf(
|
||||
'<a href="%s" class="category-badge category-badge-hero"><i class="bi %s me-1"></i>%s</a>',
|
||||
esc_url($category['url']),
|
||||
esc_attr($icon),
|
||||
esc_html($category['name'])
|
||||
);
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function getCategories(array $data): array
|
||||
{
|
||||
$source = $data['categories']['categories_source'] ?? 'post_categories';
|
||||
|
||||
switch ($source) {
|
||||
case 'post_categories':
|
||||
return $this->getPostCategories();
|
||||
|
||||
case 'post_tags':
|
||||
return $this->getPostTags();
|
||||
|
||||
case 'custom_taxonomy':
|
||||
$taxonomy = $data['categories']['custom_taxonomy_name'] ?? '';
|
||||
return $this->getCustomTaxonomyTerms($taxonomy);
|
||||
|
||||
case 'custom_list':
|
||||
$list = $data['categories']['custom_categories_list'] ?? '';
|
||||
return $this->parseCustomCategoriesList($list);
|
||||
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private function getPostCategories(): array
|
||||
{
|
||||
$categories = get_the_category();
|
||||
if (empty($categories)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($categories as $category) {
|
||||
$result[] = [
|
||||
'name' => $category->name,
|
||||
'url' => get_category_link($category->term_id)
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function getPostTags(): array
|
||||
{
|
||||
$tags = get_the_tags();
|
||||
if (empty($tags)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($tags as $tag) {
|
||||
$result[] = [
|
||||
'name' => $tag->name,
|
||||
'url' => get_tag_link($tag->term_id)
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function getCustomTaxonomyTerms(string $taxonomy): array
|
||||
{
|
||||
if (empty($taxonomy)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$terms = get_the_terms(get_the_ID(), $taxonomy);
|
||||
if (empty($terms) || is_wp_error($terms)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($terms as $term) {
|
||||
$result[] = [
|
||||
'name' => $term->name,
|
||||
'url' => get_term_link($term)
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function parseCustomCategoriesList(string $list): array
|
||||
{
|
||||
if (empty($list)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$lines = explode("\n", $list);
|
||||
$result = [];
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if (empty($line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parts = explode('|', $line);
|
||||
if (count($parts) >= 2) {
|
||||
$result[] = [
|
||||
'name' => trim($parts[0]),
|
||||
'url' => trim($parts[1])
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function buildTitle(array $data): string
|
||||
{
|
||||
$titleText = $this->getTitleText($data);
|
||||
|
||||
if (empty($titleText)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$titleTag = $data['title']['title_tag'] ?? 'h1';
|
||||
$titleClasses = $data['title']['title_classes'] ?? 'display-5 fw-bold';
|
||||
$alignment = $data['title']['title_alignment'] ?? 'center';
|
||||
|
||||
$alignmentClasses = [
|
||||
'left' => 'text-start',
|
||||
'center' => 'text-center',
|
||||
'right' => 'text-end'
|
||||
];
|
||||
$alignmentClass = $alignmentClasses[$alignment] ?? 'text-center';
|
||||
|
||||
$classes = trim($titleClasses . ' ' . $alignmentClass);
|
||||
|
||||
$titleStyle = '';
|
||||
if (isset($data['title']['enable_gradient']) && $data['title']['enable_gradient']) {
|
||||
$titleStyle = $this->buildGradientStyle($data);
|
||||
$classes .= ' roi-gradient-text';
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<%s class="%s"%s>%s</%s>',
|
||||
esc_attr($titleTag),
|
||||
esc_attr($classes),
|
||||
$titleStyle ? ' style="' . esc_attr($titleStyle) . '"' : '',
|
||||
esc_html($titleText),
|
||||
esc_attr($titleTag)
|
||||
);
|
||||
}
|
||||
|
||||
private function getTitleText(array $data): string
|
||||
{
|
||||
$source = $data['title']['title_source'] ?? 'post_title';
|
||||
|
||||
switch ($source) {
|
||||
case 'post_title':
|
||||
return get_the_title();
|
||||
|
||||
case 'custom_field':
|
||||
$fieldName = $data['title']['custom_field_name'] ?? '';
|
||||
if (!empty($fieldName)) {
|
||||
$value = get_post_meta(get_the_ID(), $fieldName, true);
|
||||
return is_string($value) ? $value : '';
|
||||
}
|
||||
return '';
|
||||
|
||||
case 'custom_text':
|
||||
return $data['title']['custom_text'] ?? '';
|
||||
|
||||
default:
|
||||
return get_the_title();
|
||||
}
|
||||
}
|
||||
|
||||
private function buildGradientStyle(array $data): string
|
||||
{
|
||||
$startColor = $data['title']['gradient_color_start'] ?? '#1e3a5f';
|
||||
$endColor = $data['title']['gradient_color_end'] ?? '#FF8600';
|
||||
$direction = $data['title']['gradient_direction'] ?? 'to-right';
|
||||
|
||||
$directions = [
|
||||
'to-right' => 'to right',
|
||||
'to-left' => 'to left',
|
||||
'to-bottom' => 'to bottom',
|
||||
'to-top' => 'to top',
|
||||
'diagonal' => '135deg'
|
||||
];
|
||||
|
||||
$gradientDirection = $directions[$direction] ?? 'to right';
|
||||
|
||||
return "background: linear-gradient({$gradientDirection}, {$startColor}, {$endColor}); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;";
|
||||
}
|
||||
|
||||
private function buildCustomStyles(array $data): string
|
||||
{
|
||||
$badgeBg = $data['styles']['category_badge_background'] ?? 'rgba(255, 255, 255, 0.2)';
|
||||
$badgeTextColor = $data['styles']['category_badge_text_color'] ?? '#FFFFFF';
|
||||
$badgeBlur = isset($data['styles']['category_badge_blur']) && $data['styles']['category_badge_blur'];
|
||||
|
||||
$blurStyle = $badgeBlur ? 'backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);' : '';
|
||||
|
||||
$overlayStyle = '';
|
||||
if (($data['styles']['background_type'] ?? '') === 'image' &&
|
||||
isset($data['styles']['background_overlay']) &&
|
||||
$data['styles']['background_overlay']) {
|
||||
$opacity = ($data['styles']['overlay_opacity'] ?? 60) / 100;
|
||||
$overlayStyle = <<<CSS
|
||||
.hero-title::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, {$opacity});
|
||||
z-index: 0;
|
||||
}
|
||||
.hero-title > .container {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
CSS;
|
||||
}
|
||||
|
||||
return <<<STYLES
|
||||
<style>
|
||||
.category-badge-hero {
|
||||
background-color: {$badgeBg};
|
||||
color: {$badgeTextColor};
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 2rem;
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
transition: all 0.3s ease;
|
||||
{$blurStyle}
|
||||
}
|
||||
.category-badge-hero:hover {
|
||||
background-color: rgba(255, 134, 0, 0.3);
|
||||
color: {$badgeTextColor};
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.roi-gradient-text {
|
||||
display: inline-block;
|
||||
}
|
||||
{$overlayStyle}
|
||||
</style>
|
||||
STYLES;
|
||||
}
|
||||
|
||||
public function supports(string $componentType): bool
|
||||
{
|
||||
return $componentType === 'hero-section';
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ namespace ROITheme\Public\Navbar\Infrastructure\Ui;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||
use Walker_Nav_Menu;
|
||||
|
||||
/**
|
||||
@@ -28,6 +29,8 @@ use Walker_Nav_Menu;
|
||||
*/
|
||||
final class NavbarRenderer implements RendererInterface
|
||||
{
|
||||
private const COMPONENT_NAME = 'navbar';
|
||||
|
||||
/**
|
||||
* @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS
|
||||
*/
|
||||
@@ -43,6 +46,10 @@ final class NavbarRenderer implements RendererInterface
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$html = $this->buildMenu($data);
|
||||
|
||||
// Si is_critical=true, CSS ya fue inyectado en <head> por CriticalCSSService
|
||||
@@ -281,7 +288,7 @@ final class NavbarRenderer implements RendererInterface
|
||||
|
||||
public function supports(string $componentType): bool
|
||||
{
|
||||
return $componentType === 'navbar';
|
||||
return $componentType === self::COMPONENT_NAME;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
651
Public/PostGrid/Infrastructure/Ui/PostGridRenderer.php
Normal file
651
Public/PostGrid/Infrastructure/Ui/PostGridRenderer.php
Normal file
@@ -0,0 +1,651 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\PostGrid\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||
|
||||
/**
|
||||
* PostGridRenderer - Renderiza grid de posts del loop principal de WordPress
|
||||
*
|
||||
* RESPONSABILIDAD: Generar HTML y CSS del componente Post Grid
|
||||
*
|
||||
* DIFERENCIA CON RelatedPostRenderer:
|
||||
* - PostGrid usa global $wp_query (loop principal)
|
||||
* - RelatedPost crea su propio WP_Query
|
||||
*
|
||||
* CARACTERISTICAS:
|
||||
* - Grid responsive de cards con imagen, excerpt y meta
|
||||
* - Usa loop principal de WordPress (no crea queries propias)
|
||||
* - Paginacion nativa de WordPress
|
||||
* - Estilos 100% desde BD via CSSGenerator
|
||||
*
|
||||
* @package ROITheme\Public\PostGrid\Infrastructure\Ui
|
||||
*/
|
||||
final class PostGridRenderer implements RendererInterface
|
||||
{
|
||||
private const COMPONENT_NAME = 'post-grid';
|
||||
|
||||
public function __construct(
|
||||
private CSSGeneratorInterface $cssGenerator
|
||||
) {}
|
||||
|
||||
public function render(Component $component): string
|
||||
{
|
||||
$data = $component->getData();
|
||||
|
||||
if (!$this->isEnabled($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$visibilityClass = $this->getVisibilityClass($data);
|
||||
if ($visibilityClass === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
global $wp_query;
|
||||
|
||||
// Si no hay posts, mostrar mensaje
|
||||
if (!have_posts()) {
|
||||
$noPostsMessage = $data['content']['no_posts_message'] ?? 'No se encontraron publicaciones';
|
||||
return $this->renderNoPostsMessage($noPostsMessage, $visibilityClass, $data);
|
||||
}
|
||||
|
||||
$css = $this->generateCSS($data);
|
||||
$html = $this->buildHTML($data, $visibilityClass);
|
||||
|
||||
return sprintf("<style>%s</style>\n%s", $css, $html);
|
||||
}
|
||||
|
||||
public function supports(string $componentType): bool
|
||||
{
|
||||
return $componentType === self::COMPONENT_NAME;
|
||||
}
|
||||
|
||||
private function isEnabled(array $data): bool
|
||||
{
|
||||
$value = $data['visibility']['is_enabled'] ?? false;
|
||||
return $value === true || $value === '1' || $value === 1;
|
||||
}
|
||||
|
||||
private function getVisibilityClass(array $data): ?string
|
||||
{
|
||||
$showDesktop = $data['visibility']['show_on_desktop'] ?? true;
|
||||
$showDesktop = $showDesktop === true || $showDesktop === '1' || $showDesktop === 1;
|
||||
$showMobile = $data['visibility']['show_on_mobile'] ?? true;
|
||||
$showMobile = $showMobile === true || $showMobile === '1' || $showMobile === 1;
|
||||
|
||||
if (!$showDesktop && !$showMobile) {
|
||||
return null;
|
||||
}
|
||||
if (!$showDesktop && $showMobile) {
|
||||
return 'd-lg-none';
|
||||
}
|
||||
if ($showDesktop && !$showMobile) {
|
||||
return 'd-none d-lg-block';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private function renderNoPostsMessage(string $message, string $visibilityClass, array $data): string
|
||||
{
|
||||
$colors = $data['colors'] ?? [];
|
||||
$spacing = $data['spacing'] ?? [];
|
||||
|
||||
$bgColor = $colors['card_bg_color'] ?? '#ffffff';
|
||||
$textColor = $colors['excerpt_color'] ?? '#6b7280';
|
||||
$borderColor = $colors['card_border_color'] ?? '#e5e7eb';
|
||||
$padding = $spacing['card_padding'] ?? '1.25rem';
|
||||
|
||||
$css = $this->cssGenerator->generate('.post-grid-no-posts', [
|
||||
'background-color' => $bgColor,
|
||||
'color' => $textColor,
|
||||
'border' => "1px solid {$borderColor}",
|
||||
'border-radius' => '0.5rem',
|
||||
'padding' => '2rem',
|
||||
'text-align' => 'center',
|
||||
]);
|
||||
|
||||
$containerClass = 'post-grid-no-posts';
|
||||
if (!empty($visibilityClass)) {
|
||||
$containerClass .= ' ' . $visibilityClass;
|
||||
}
|
||||
|
||||
$html = sprintf(
|
||||
'<div class="%s"><p class="mb-0">%s</p></div>',
|
||||
esc_attr($containerClass),
|
||||
esc_html($message)
|
||||
);
|
||||
|
||||
return sprintf("<style>%s</style>\n%s", $css, $html);
|
||||
}
|
||||
|
||||
private function generateCSS(array $data): string
|
||||
{
|
||||
$colors = $data['colors'] ?? [];
|
||||
$spacing = $data['spacing'] ?? [];
|
||||
$effects = $data['visual_effects'] ?? [];
|
||||
$typography = $data['typography'] ?? [];
|
||||
$layout = $data['layout'] ?? [];
|
||||
|
||||
$cssRules = [];
|
||||
|
||||
// Colores
|
||||
$cardBgColor = $colors['card_bg_color'] ?? '#ffffff';
|
||||
$cardTitleColor = $colors['card_title_color'] ?? '#0E2337';
|
||||
$cardHoverBgColor = $colors['card_hover_bg_color'] ?? '#f9fafb';
|
||||
$cardBorderColor = $colors['card_border_color'] ?? '#e5e7eb';
|
||||
$cardHoverBorderColor = $colors['card_hover_border_color'] ?? '#FF8600';
|
||||
$excerptColor = $colors['excerpt_color'] ?? '#6b7280';
|
||||
$metaColor = $colors['meta_color'] ?? '#9ca3af';
|
||||
$categoryBgColor = $colors['category_bg_color'] ?? '#FFF5EB';
|
||||
$categoryTextColor = $colors['category_text_color'] ?? '#FF8600';
|
||||
$paginationColor = $colors['pagination_color'] ?? '#0E2337';
|
||||
$paginationActiveBg = $colors['pagination_active_bg'] ?? '#FF8600';
|
||||
$paginationActiveColor = $colors['pagination_active_color'] ?? '#ffffff';
|
||||
|
||||
// Spacing
|
||||
$gapHorizontal = $spacing['gap_horizontal'] ?? '24px';
|
||||
$gapVertical = $spacing['gap_vertical'] ?? '24px';
|
||||
$cardPadding = $spacing['card_padding'] ?? '20px';
|
||||
$sectionMarginTop = $spacing['section_margin_top'] ?? '0px';
|
||||
$sectionMarginBottom = $spacing['section_margin_bottom'] ?? '32px';
|
||||
|
||||
// Visual effects
|
||||
$cardBorderRadius = $effects['card_border_radius'] ?? '0.5rem';
|
||||
$cardShadow = $effects['card_shadow'] ?? '0 1px 3px rgba(0,0,0,0.1)';
|
||||
$cardHoverShadow = $effects['card_hover_shadow'] ?? '0 4px 12px rgba(0,0,0,0.15)';
|
||||
$cardTransition = $effects['card_transition'] ?? 'all 0.3s ease';
|
||||
$imageBorderRadius = $effects['image_border_radius'] ?? '0.375rem';
|
||||
|
||||
// Typography
|
||||
$cardTitleSize = $typography['card_title_size'] ?? '1.1rem';
|
||||
$cardTitleWeight = $typography['card_title_weight'] ?? '600';
|
||||
$excerptSize = $typography['excerpt_size'] ?? '0.9rem';
|
||||
$metaSize = $typography['meta_size'] ?? '0.8rem';
|
||||
|
||||
// Container
|
||||
$cssRules[] = $this->cssGenerator->generate('.post-grid', [
|
||||
'margin-top' => $sectionMarginTop,
|
||||
'margin-bottom' => $sectionMarginBottom,
|
||||
]);
|
||||
|
||||
// Row: usar display flex con gap, quitar margins/paddings de Bootstrap
|
||||
$cssRules[] = ".post-grid .row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
column-gap: {$gapHorizontal};
|
||||
row-gap: {$gapVertical};
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}";
|
||||
|
||||
// Columnas: quitar padding de Bootstrap y margin-bottom
|
||||
$cssRules[] = ".post-grid .post-card-col {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}";
|
||||
|
||||
// Card base - sin margin extra
|
||||
$cssRules[] = ".post-grid .card {
|
||||
background: {$cardBgColor};
|
||||
border: 1px solid {$cardBorderColor};
|
||||
border-radius: {$cardBorderRadius};
|
||||
box-shadow: {$cardShadow};
|
||||
transition: {$cardTransition};
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
}";
|
||||
|
||||
// Card hover
|
||||
$cssRules[] = ".post-grid .card:hover {
|
||||
background: {$cardHoverBgColor};
|
||||
border-color: {$cardHoverBorderColor};
|
||||
box-shadow: {$cardHoverShadow};
|
||||
transform: translateY(-2px);
|
||||
}";
|
||||
|
||||
// Card body
|
||||
$cssRules[] = $this->cssGenerator->generate('.post-grid .card-body', [
|
||||
'padding' => $cardPadding,
|
||||
]);
|
||||
|
||||
// Card image
|
||||
$cssRules[] = ".post-grid .card-img-top {
|
||||
border-radius: {$imageBorderRadius} {$imageBorderRadius} 0 0;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
}";
|
||||
|
||||
// Card title
|
||||
$cssRules[] = ".post-grid .card-title {
|
||||
color: {$cardTitleColor};
|
||||
font-size: {$cardTitleSize};
|
||||
font-weight: {$cardTitleWeight};
|
||||
line-height: 1.4;
|
||||
margin-bottom: 0.75rem;
|
||||
}";
|
||||
|
||||
// Card title hover
|
||||
$cssRules[] = ".post-grid a:hover .card-title {
|
||||
color: {$cardHoverBorderColor};
|
||||
}";
|
||||
|
||||
// Excerpt
|
||||
$cssRules[] = ".post-grid .card-text {
|
||||
color: {$excerptColor};
|
||||
font-size: {$excerptSize};
|
||||
line-height: 1.6;
|
||||
}";
|
||||
|
||||
// Meta
|
||||
$cssRules[] = ".post-grid .post-meta {
|
||||
color: {$metaColor};
|
||||
font-size: {$metaSize};
|
||||
}";
|
||||
|
||||
// Categories
|
||||
$cssRules[] = ".post-grid .post-category {
|
||||
background: {$categoryBgColor};
|
||||
color: {$categoryTextColor};
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 9999px;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
margin-right: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}";
|
||||
|
||||
$cssRules[] = ".post-grid .post-category:hover {
|
||||
background: {$categoryTextColor};
|
||||
color: #ffffff;
|
||||
}";
|
||||
|
||||
// Pagination
|
||||
$cssRules[] = ".post-grid .pagination {
|
||||
margin-top: 2rem;
|
||||
}";
|
||||
|
||||
$cssRules[] = ".post-grid .page-link {
|
||||
color: {$paginationColor};
|
||||
border: 1px solid {$cardBorderColor};
|
||||
padding: 0.5rem 1rem;
|
||||
margin: 0 0.25rem;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}";
|
||||
|
||||
$cssRules[] = ".post-grid .page-link:hover {
|
||||
background-color: rgba(255, 133, 0, 0.1);
|
||||
border-color: {$paginationActiveBg};
|
||||
color: {$paginationActiveBg};
|
||||
}";
|
||||
|
||||
$cssRules[] = ".post-grid .page-item.active .page-link,
|
||||
.post-grid .nav-links .current {
|
||||
background-color: {$paginationActiveBg};
|
||||
border-color: {$paginationActiveBg};
|
||||
color: {$paginationActiveColor};
|
||||
}";
|
||||
|
||||
// WordPress pagination classes
|
||||
$cssRules[] = ".post-grid .nav-links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 2rem;
|
||||
}";
|
||||
|
||||
$cssRules[] = ".post-grid .nav-links a,
|
||||
.post-grid .nav-links span {
|
||||
color: {$paginationColor};
|
||||
border: 1px solid {$cardBorderColor};
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
}";
|
||||
|
||||
$cssRules[] = ".post-grid .nav-links a:hover {
|
||||
background-color: rgba(255, 133, 0, 0.1);
|
||||
border-color: {$paginationActiveBg};
|
||||
color: {$paginationActiveBg};
|
||||
}";
|
||||
|
||||
// Layout responsive columns
|
||||
$colsDesktop = $layout['columns_desktop'] ?? '3';
|
||||
$colsTablet = $layout['columns_tablet'] ?? '2';
|
||||
$colsMobile = $layout['columns_mobile'] ?? '1';
|
||||
|
||||
// Mobile (1 col = no gap needed)
|
||||
$mobileWidth = $this->getColumnWidth($colsMobile, $gapHorizontal);
|
||||
$cssRules[] = "@media (max-width: 575.98px) {
|
||||
.post-grid .post-card-col {
|
||||
flex: 0 0 {$mobileWidth};
|
||||
max-width: {$mobileWidth};
|
||||
}
|
||||
}";
|
||||
|
||||
// Tablet
|
||||
$tabletWidth = $this->getColumnWidth($colsTablet, $gapHorizontal);
|
||||
$cssRules[] = "@media (min-width: 576px) and (max-width: 991.98px) {
|
||||
.post-grid .post-card-col {
|
||||
flex: 0 0 {$tabletWidth};
|
||||
max-width: {$tabletWidth};
|
||||
}
|
||||
}";
|
||||
|
||||
// Desktop
|
||||
$desktopWidth = $this->getColumnWidth($colsDesktop, $gapHorizontal);
|
||||
$cssRules[] = "@media (min-width: 992px) {
|
||||
.post-grid .post-card-col {
|
||||
flex: 0 0 {$desktopWidth};
|
||||
max-width: {$desktopWidth};
|
||||
}
|
||||
}";
|
||||
|
||||
return implode("\n", $cssRules);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcula el ancho de columna considerando el gap
|
||||
*
|
||||
* Con gap en flexbox, el ancho debe ser:
|
||||
* (100% - (n-1)*gap) / n
|
||||
*
|
||||
* @param string $cols Número de columnas
|
||||
* @param string $gap Valor del gap (ej: '1.5rem', '24px')
|
||||
* @return string Valor CSS con calc() si hay gap
|
||||
*/
|
||||
private function getColumnWidth(string $cols, string $gap): string
|
||||
{
|
||||
$colCount = (int)$cols;
|
||||
if ($colCount <= 0) {
|
||||
$colCount = 1;
|
||||
}
|
||||
|
||||
// Si es 1 columna, no hay gap entre columnas
|
||||
if ($colCount === 1) {
|
||||
return '100%';
|
||||
}
|
||||
|
||||
// Número de gaps = columnas - 1
|
||||
$gapCount = $colCount - 1;
|
||||
|
||||
// calc((100% - (n-1)*gap) / n)
|
||||
return sprintf('calc((100%% - %d * %s) / %d)', $gapCount, $gap, $colCount);
|
||||
}
|
||||
|
||||
private function buildHTML(array $data, string $visibilityClass): string
|
||||
{
|
||||
$content = $data['content'] ?? [];
|
||||
$typography = $data['typography'] ?? [];
|
||||
$media = $data['media'] ?? [];
|
||||
$layout = $data['layout'] ?? [];
|
||||
|
||||
$showThumbnail = $this->toBool($content['show_thumbnail'] ?? true);
|
||||
$showExcerpt = $this->toBool($content['show_excerpt'] ?? true);
|
||||
$showMeta = $this->toBool($content['show_meta'] ?? true);
|
||||
$showCategories = $this->toBool($content['show_categories'] ?? true);
|
||||
$excerptLength = (int)($content['excerpt_length'] ?? 20);
|
||||
$readMoreText = $content['read_more_text'] ?? 'Leer mas';
|
||||
$headingLevel = $typography['heading_level'] ?? 'h3';
|
||||
$fallbackImage = $media['fallback_image'] ?? '';
|
||||
$fallbackImageAlt = $media['fallback_image_alt'] ?? 'Imagen por defecto';
|
||||
$imagePosition = $layout['image_position'] ?? 'top';
|
||||
|
||||
$containerClass = 'post-grid';
|
||||
if (!empty($visibilityClass)) {
|
||||
$containerClass .= ' ' . $visibilityClass;
|
||||
}
|
||||
|
||||
$html = sprintf('<div class="%s">', esc_attr($containerClass));
|
||||
$html .= '<div class="row">';
|
||||
|
||||
while (have_posts()) {
|
||||
the_post();
|
||||
$html .= $this->buildCardHTML(
|
||||
$showThumbnail,
|
||||
$showExcerpt,
|
||||
$showMeta,
|
||||
$showCategories,
|
||||
$excerptLength,
|
||||
$readMoreText,
|
||||
$headingLevel,
|
||||
$fallbackImage,
|
||||
$fallbackImageAlt,
|
||||
$imagePosition
|
||||
);
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
// Paginacion nativa de WordPress
|
||||
$html .= '<div class="pagination-wrapper">';
|
||||
$html .= $this->buildPaginationHTML();
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
wp_reset_postdata();
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function toBool(mixed $value): bool
|
||||
{
|
||||
return $value === true || $value === '1' || $value === 1;
|
||||
}
|
||||
|
||||
private function buildCardHTML(
|
||||
bool $showThumbnail,
|
||||
bool $showExcerpt,
|
||||
bool $showMeta,
|
||||
bool $showCategories,
|
||||
int $excerptLength,
|
||||
string $readMoreText,
|
||||
string $headingLevel,
|
||||
string $fallbackImage,
|
||||
string $fallbackImageAlt,
|
||||
string $imagePosition
|
||||
): string {
|
||||
$permalink = get_permalink();
|
||||
$title = get_the_title();
|
||||
|
||||
$html = '<div class="post-card-col">';
|
||||
$html .= sprintf(
|
||||
'<a href="%s" class="text-decoration-none">',
|
||||
esc_url($permalink)
|
||||
);
|
||||
|
||||
$cardClass = 'card h-100';
|
||||
if ($imagePosition === 'left') {
|
||||
$cardClass .= ' flex-row';
|
||||
}
|
||||
|
||||
$html .= sprintf('<div class="%s">', esc_attr($cardClass));
|
||||
|
||||
// Imagen
|
||||
if ($showThumbnail && $imagePosition !== 'none') {
|
||||
$html .= $this->buildImageHTML($fallbackImage, $fallbackImageAlt, $imagePosition);
|
||||
}
|
||||
|
||||
$html .= '<div class="card-body">';
|
||||
|
||||
// Categorias
|
||||
if ($showCategories) {
|
||||
$html .= $this->buildCategoriesHTML();
|
||||
}
|
||||
|
||||
// Titulo
|
||||
$html .= sprintf(
|
||||
'<%s class="card-title">%s</%s>',
|
||||
esc_attr($headingLevel),
|
||||
esc_html($title),
|
||||
esc_attr($headingLevel)
|
||||
);
|
||||
|
||||
// Meta
|
||||
if ($showMeta) {
|
||||
$html .= $this->buildMetaHTML();
|
||||
}
|
||||
|
||||
// Excerpt
|
||||
if ($showExcerpt) {
|
||||
$html .= $this->buildExcerptHTML($excerptLength);
|
||||
}
|
||||
|
||||
$html .= '</div>'; // card-body
|
||||
$html .= '</div>'; // card
|
||||
$html .= '</a>';
|
||||
$html .= '</div>'; // col
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildImageHTML(string $fallbackImage, string $fallbackImageAlt, string $imagePosition): string
|
||||
{
|
||||
if (has_post_thumbnail()) {
|
||||
$imageClass = $imagePosition === 'left' ? 'card-img-left' : 'card-img-top';
|
||||
return get_the_post_thumbnail(
|
||||
null,
|
||||
'medium_large',
|
||||
['class' => $imageClass, 'loading' => 'lazy']
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($fallbackImage)) {
|
||||
$imageClass = $imagePosition === 'left' ? 'card-img-left' : 'card-img-top';
|
||||
return sprintf(
|
||||
'<img src="%s" alt="%s" class="%s" loading="lazy">',
|
||||
esc_url($fallbackImage),
|
||||
esc_attr($fallbackImageAlt),
|
||||
esc_attr($imageClass)
|
||||
);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private function buildCategoriesHTML(): string
|
||||
{
|
||||
$categories = get_the_category();
|
||||
if (empty($categories)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$html = '<div class="post-categories mb-2">';
|
||||
foreach (array_slice($categories, 0, 2) as $category) {
|
||||
$html .= sprintf(
|
||||
'<span class="post-category">%s</span>',
|
||||
esc_html($category->name)
|
||||
);
|
||||
}
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildMetaHTML(): string
|
||||
{
|
||||
$date = get_the_date();
|
||||
$author = get_the_author();
|
||||
|
||||
return sprintf(
|
||||
'<div class="post-meta mb-2"><small>%s | %s</small></div>',
|
||||
esc_html($date),
|
||||
esc_html($author)
|
||||
);
|
||||
}
|
||||
|
||||
private function buildExcerptHTML(int $length): string
|
||||
{
|
||||
$excerpt = get_the_excerpt();
|
||||
|
||||
if (empty($excerpt)) {
|
||||
$excerpt = wp_trim_words(get_the_content(), $length, '...');
|
||||
} else {
|
||||
$excerpt = wp_trim_words($excerpt, $length, '...');
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<p class="card-text">%s</p>',
|
||||
esc_html($excerpt)
|
||||
);
|
||||
}
|
||||
|
||||
private function buildPaginationHTML(): string
|
||||
{
|
||||
global $wp_query;
|
||||
|
||||
$totalPages = $wp_query->max_num_pages;
|
||||
if ($totalPages <= 1) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$currentPage = max(1, get_query_var('paged', 1));
|
||||
$html = '<nav aria-label="Paginacion"><ul class="pagination justify-content-center">';
|
||||
|
||||
// Boton Inicio (siempre visible)
|
||||
$html .= sprintf(
|
||||
'<li class="page-item"><a class="page-link" href="%s">Inicio</a></li>',
|
||||
esc_url(get_pagenum_link(1))
|
||||
);
|
||||
|
||||
// Numeros de pagina - mostrar 5 paginas
|
||||
$visiblePages = 5;
|
||||
$start = max(1, $currentPage - 2);
|
||||
$end = min($totalPages, $start + $visiblePages - 1);
|
||||
|
||||
// Ajustar inicio si estamos cerca del final
|
||||
if ($end - $start < $visiblePages - 1) {
|
||||
$start = max(1, $end - $visiblePages + 1);
|
||||
}
|
||||
|
||||
for ($i = $start; $i <= $end; $i++) {
|
||||
if ($i === $currentPage) {
|
||||
$html .= sprintf(
|
||||
'<li class="page-item active"><span class="page-link">%d</span></li>',
|
||||
$i
|
||||
);
|
||||
} else {
|
||||
$html .= sprintf(
|
||||
'<li class="page-item"><a class="page-link" href="%s">%d</a></li>',
|
||||
esc_url(get_pagenum_link($i)),
|
||||
$i
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Ver mas (siguiente pagina)
|
||||
if ($currentPage < $totalPages) {
|
||||
$html .= sprintf(
|
||||
'<li class="page-item"><a class="page-link" href="%s">Ver mas</a></li>',
|
||||
esc_url(get_pagenum_link($currentPage + 1))
|
||||
);
|
||||
}
|
||||
|
||||
// Boton Fin (siempre visible)
|
||||
$html .= sprintf(
|
||||
'<li class="page-item"><a class="page-link" href="%s">Fin</a></li>',
|
||||
esc_url(get_pagenum_link($totalPages))
|
||||
);
|
||||
|
||||
$html .= '</ul></nav>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ namespace ROITheme\Public\RelatedPost\Infrastructure\Ui;
|
||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||
|
||||
/**
|
||||
* RelatedPostRenderer - Renderiza seccion de posts relacionados
|
||||
@@ -22,6 +23,8 @@ use ROITheme\Shared\Domain\Entities\Component;
|
||||
*/
|
||||
final class RelatedPostRenderer implements RendererInterface
|
||||
{
|
||||
private const COMPONENT_NAME = 'related-post';
|
||||
|
||||
public function __construct(
|
||||
private CSSGeneratorInterface $cssGenerator
|
||||
) {}
|
||||
@@ -34,7 +37,7 @@ final class RelatedPostRenderer implements RendererInterface
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!$this->shouldShowOnCurrentPage($data)) {
|
||||
if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -51,7 +54,7 @@ final class RelatedPostRenderer implements RendererInterface
|
||||
|
||||
public function supports(string $componentType): bool
|
||||
{
|
||||
return $componentType === 'related-post';
|
||||
return $componentType === self::COMPONENT_NAME;
|
||||
}
|
||||
|
||||
private function isEnabled(array $data): bool
|
||||
@@ -60,22 +63,6 @@ final class RelatedPostRenderer implements RendererInterface
|
||||
return $value === true || $value === '1' || $value === 1;
|
||||
}
|
||||
|
||||
private function shouldShowOnCurrentPage(array $data): bool
|
||||
{
|
||||
$showOn = $data['visibility']['show_on_pages'] ?? 'posts';
|
||||
|
||||
switch ($showOn) {
|
||||
case 'all':
|
||||
return true;
|
||||
case 'posts':
|
||||
return is_single();
|
||||
case 'pages':
|
||||
return is_page();
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private function getVisibilityClass(array $data): ?string
|
||||
{
|
||||
$showDesktop = $data['visibility']['show_on_desktop'] ?? true;
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace ROITheme\Public\SocialShare\Infrastructure\Ui;
|
||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||
|
||||
/**
|
||||
* SocialShareRenderer - Renderiza botones de compartir en redes sociales
|
||||
@@ -27,6 +28,8 @@ use ROITheme\Shared\Domain\Entities\Component;
|
||||
*/
|
||||
final class SocialShareRenderer implements RendererInterface
|
||||
{
|
||||
private const COMPONENT_NAME = 'social-share';
|
||||
|
||||
private const NETWORKS = [
|
||||
'facebook' => [
|
||||
'field' => 'show_facebook',
|
||||
@@ -84,7 +87,7 @@ final class SocialShareRenderer implements RendererInterface
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!$this->shouldShowOnCurrentPage($data)) {
|
||||
if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -96,7 +99,7 @@ final class SocialShareRenderer implements RendererInterface
|
||||
|
||||
public function supports(string $componentType): bool
|
||||
{
|
||||
return $componentType === 'social-share';
|
||||
return $componentType === self::COMPONENT_NAME;
|
||||
}
|
||||
|
||||
private function isEnabled(array $data): bool
|
||||
@@ -105,22 +108,6 @@ final class SocialShareRenderer implements RendererInterface
|
||||
return $value === true || $value === '1' || $value === 1;
|
||||
}
|
||||
|
||||
private function shouldShowOnCurrentPage(array $data): bool
|
||||
{
|
||||
$showOn = $data['visibility']['show_on_pages'] ?? 'posts';
|
||||
|
||||
switch ($showOn) {
|
||||
case 'all':
|
||||
return true;
|
||||
case 'posts':
|
||||
return is_single();
|
||||
case 'pages':
|
||||
return is_page();
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private function generateCSS(array $data): string
|
||||
{
|
||||
$colors = $data['colors'] ?? [];
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace ROITheme\Public\TableOfContents\Infrastructure\Ui;
|
||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||
use DOMDocument;
|
||||
use DOMXPath;
|
||||
|
||||
@@ -30,6 +31,8 @@ use DOMXPath;
|
||||
*/
|
||||
final class TableOfContentsRenderer implements RendererInterface
|
||||
{
|
||||
private const COMPONENT_NAME = 'table-of-contents';
|
||||
|
||||
private array $headingCounter = [];
|
||||
|
||||
public function __construct(
|
||||
@@ -44,7 +47,7 @@ final class TableOfContentsRenderer implements RendererInterface
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!$this->shouldShowOnCurrentPage($data)) {
|
||||
if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -63,7 +66,7 @@ final class TableOfContentsRenderer implements RendererInterface
|
||||
|
||||
public function supports(string $componentType): bool
|
||||
{
|
||||
return $componentType === 'table-of-contents';
|
||||
return $componentType === self::COMPONENT_NAME;
|
||||
}
|
||||
|
||||
private function isEnabled(array $data): bool
|
||||
@@ -71,22 +74,6 @@ final class TableOfContentsRenderer implements RendererInterface
|
||||
return ($data['visibility']['is_enabled'] ?? false) === true;
|
||||
}
|
||||
|
||||
private function shouldShowOnCurrentPage(array $data): bool
|
||||
{
|
||||
$showOn = $data['visibility']['show_on_pages'] ?? 'posts';
|
||||
|
||||
switch ($showOn) {
|
||||
case 'all':
|
||||
return true;
|
||||
case 'posts':
|
||||
return is_single();
|
||||
case 'pages':
|
||||
return is_page();
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private function getVisibilityClasses(bool $desktop, bool $mobile): ?string
|
||||
{
|
||||
if (!$desktop && !$mobile) {
|
||||
@@ -124,8 +111,23 @@ final class TableOfContentsRenderer implements RendererInterface
|
||||
return [];
|
||||
}
|
||||
|
||||
// Intentar primero con contenido filtrado (respeta shortcodes, etc.)
|
||||
$content = apply_filters('the_content', $post->post_content);
|
||||
|
||||
// Verificar si el contenido filtrado tiene headings
|
||||
$hasFilteredHeadings = preg_match('/<h[2-6][^>]*>/i', $content);
|
||||
|
||||
// FIX: Si el contenido filtrado no tiene headings pero el raw si,
|
||||
// usar el contenido raw. Esto ocurre cuando plugins como Thrive
|
||||
// transforman el contenido para usuarios no logueados.
|
||||
if (!$hasFilteredHeadings) {
|
||||
$hasRawHeadings = preg_match('/<h[2-6][^>]*>/i', $post->post_content);
|
||||
if ($hasRawHeadings) {
|
||||
// Usar wpautop para dar formato basico al contenido raw
|
||||
$content = wpautop($post->post_content);
|
||||
}
|
||||
}
|
||||
|
||||
$dom = new DOMDocument();
|
||||
libxml_use_internal_errors(true);
|
||||
$dom->loadHTML('<?xml encoding="utf-8" ?>' . $content);
|
||||
|
||||
@@ -6,6 +6,8 @@ namespace ROITheme\Public\TopNotificationBar\Infrastructure\Ui;
|
||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||
use ROITheme\Shared\Infrastructure\Services\UserVisibilityHelper;
|
||||
|
||||
/**
|
||||
* Class TopNotificationBarRenderer
|
||||
@@ -34,6 +36,8 @@ use ROITheme\Shared\Domain\Entities\Component;
|
||||
*/
|
||||
final class TopNotificationBarRenderer implements RendererInterface
|
||||
{
|
||||
private const COMPONENT_NAME = 'top-notification-bar';
|
||||
|
||||
/**
|
||||
* @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS
|
||||
*/
|
||||
@@ -54,7 +58,12 @@ final class TopNotificationBarRenderer implements RendererInterface
|
||||
}
|
||||
|
||||
// Validar visibilidad por página
|
||||
if (!$this->shouldShowOnCurrentPage($data)) {
|
||||
if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Validar visibilidad por usuario logueado
|
||||
if (!UserVisibilityHelper::shouldShowForUser($data['visibility'] ?? [])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -78,7 +87,7 @@ final class TopNotificationBarRenderer implements RendererInterface
|
||||
*/
|
||||
public function supports(string $componentType): bool
|
||||
{
|
||||
return $componentType === 'top-notification-bar';
|
||||
return $componentType === self::COMPONENT_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,46 +101,6 @@ final class TopNotificationBarRenderer implements RendererInterface
|
||||
return ($data['visibility']['is_enabled'] ?? false) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si debe mostrarse en la página actual
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return bool
|
||||
*/
|
||||
private function shouldShowOnCurrentPage(array $data): bool
|
||||
{
|
||||
$showOn = $data['visibility']['show_on_pages'] ?? 'all';
|
||||
|
||||
return match ($showOn) {
|
||||
'all' => true,
|
||||
'home' => is_front_page(),
|
||||
'posts' => is_single(),
|
||||
'pages' => is_page(),
|
||||
'custom' => $this->isInCustomPages($data),
|
||||
default => true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si está en páginas personalizadas
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return bool
|
||||
*/
|
||||
private function isInCustomPages(array $data): bool
|
||||
{
|
||||
$pageIds = $data['visibility']['custom_page_ids'] ?? '';
|
||||
|
||||
if (empty($pageIds)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$allowedIds = array_map('trim', explode(',', $pageIds));
|
||||
$currentId = (string) get_the_ID();
|
||||
|
||||
return in_array($currentId, $allowedIds, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si el componente fue dismissed por el usuario
|
||||
*
|
||||
|
||||
@@ -110,3 +110,14 @@
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
FIX: Legacy wrapper with padding-top
|
||||
Removes duplicate aspect-ratio from parent
|
||||
containers that use the old padding-top trick
|
||||
(prevents double spacing above videos)
|
||||
======================================== */
|
||||
|
||||
div[style*="padding-top"]:has(> .youtube-facade) {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
@@ -437,6 +437,72 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"search_results": {
|
||||
"label": "Resultados de Busqueda (ROI APU Search)",
|
||||
"priority": 73,
|
||||
"fields": {
|
||||
"search_ads_enabled": {
|
||||
"type": "boolean",
|
||||
"label": "Activar ads en busqueda",
|
||||
"default": false,
|
||||
"editable": true,
|
||||
"description": "Insertar anuncios en resultados del buscador APU"
|
||||
},
|
||||
"search_top_ad_enabled": {
|
||||
"type": "boolean",
|
||||
"label": "Anuncio fijo arriba",
|
||||
"default": true,
|
||||
"editable": true,
|
||||
"description": "Mostrar anuncio debajo del campo de busqueda"
|
||||
},
|
||||
"search_top_ad_format": {
|
||||
"type": "select",
|
||||
"label": "Formato anuncio superior",
|
||||
"default": "auto",
|
||||
"editable": true,
|
||||
"options": ["auto", "display", "in-article"]
|
||||
},
|
||||
"search_between_enabled": {
|
||||
"type": "boolean",
|
||||
"label": "Anuncios entre resultados",
|
||||
"default": true,
|
||||
"editable": true
|
||||
},
|
||||
"search_between_max": {
|
||||
"type": "select",
|
||||
"label": "Maximo anuncios entre resultados",
|
||||
"default": "1",
|
||||
"editable": true,
|
||||
"options": ["1", "2", "3"],
|
||||
"description": "Maximo 3 por politicas AdSense"
|
||||
},
|
||||
"search_between_format": {
|
||||
"type": "select",
|
||||
"label": "Formato entre resultados",
|
||||
"default": "in-article",
|
||||
"editable": true,
|
||||
"options": ["auto", "in-article", "autorelaxed"]
|
||||
},
|
||||
"search_between_position": {
|
||||
"type": "select",
|
||||
"label": "Posicion de anuncios",
|
||||
"default": "random",
|
||||
"editable": true,
|
||||
"options": {
|
||||
"random": "Aleatorio",
|
||||
"fixed": "Fijo (cada N resultados)",
|
||||
"first_half": "Primera mitad"
|
||||
}
|
||||
},
|
||||
"search_between_every": {
|
||||
"type": "select",
|
||||
"label": "Cada N resultados (si es fijo)",
|
||||
"default": "5",
|
||||
"editable": true,
|
||||
"options": ["3", "4", "5", "6", "7", "8", "10"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
"label": "Ubicaciones Archivos/Globales",
|
||||
"priority": 80,
|
||||
|
||||
233
Schemas/archive-header.json
Normal file
233
Schemas/archive-header.json
Normal file
@@ -0,0 +1,233 @@
|
||||
{
|
||||
"component_name": "archive-header",
|
||||
"version": "1.0.0",
|
||||
"description": "Cabecera dinamica para paginas de archivo con titulo y descripcion contextual",
|
||||
"groups": {
|
||||
"visibility": {
|
||||
"label": "Visibilidad",
|
||||
"priority": 10,
|
||||
"fields": {
|
||||
"is_enabled": {
|
||||
"type": "boolean",
|
||||
"label": "Activar componente",
|
||||
"default": true,
|
||||
"editable": true,
|
||||
"required": true
|
||||
},
|
||||
"show_on_desktop": {
|
||||
"type": "boolean",
|
||||
"label": "Mostrar en escritorio",
|
||||
"default": true,
|
||||
"editable": true,
|
||||
"description": "Muestra el componente en pantallas >= 992px"
|
||||
},
|
||||
"show_on_mobile": {
|
||||
"type": "boolean",
|
||||
"label": "Mostrar en movil",
|
||||
"default": true,
|
||||
"editable": true,
|
||||
"description": "Muestra el componente en pantallas < 992px"
|
||||
}
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"label": "Contenido",
|
||||
"priority": 20,
|
||||
"fields": {
|
||||
"blog_title": {
|
||||
"type": "text",
|
||||
"label": "Titulo del blog",
|
||||
"default": "Blog",
|
||||
"editable": true,
|
||||
"description": "Titulo mostrado en la pagina principal del blog"
|
||||
},
|
||||
"show_post_count": {
|
||||
"type": "boolean",
|
||||
"label": "Mostrar contador de posts",
|
||||
"default": true,
|
||||
"editable": true,
|
||||
"description": "Muestra el numero de posts encontrados"
|
||||
},
|
||||
"show_description": {
|
||||
"type": "boolean",
|
||||
"label": "Mostrar descripcion",
|
||||
"default": true,
|
||||
"editable": true,
|
||||
"description": "Muestra la descripcion de categoria/tag si existe"
|
||||
},
|
||||
"category_prefix": {
|
||||
"type": "text",
|
||||
"label": "Prefijo categoria",
|
||||
"default": "Categoria:",
|
||||
"editable": true
|
||||
},
|
||||
"tag_prefix": {
|
||||
"type": "text",
|
||||
"label": "Prefijo etiqueta",
|
||||
"default": "Etiqueta:",
|
||||
"editable": true
|
||||
},
|
||||
"author_prefix": {
|
||||
"type": "text",
|
||||
"label": "Prefijo autor",
|
||||
"default": "Articulos de:",
|
||||
"editable": true
|
||||
},
|
||||
"date_prefix": {
|
||||
"type": "text",
|
||||
"label": "Prefijo fecha",
|
||||
"default": "Archivo:",
|
||||
"editable": true
|
||||
},
|
||||
"search_prefix": {
|
||||
"type": "text",
|
||||
"label": "Prefijo busqueda",
|
||||
"default": "Resultados para:",
|
||||
"editable": true
|
||||
},
|
||||
"posts_count_singular": {
|
||||
"type": "text",
|
||||
"label": "Texto singular posts",
|
||||
"default": "publicacion",
|
||||
"editable": true
|
||||
},
|
||||
"posts_count_plural": {
|
||||
"type": "text",
|
||||
"label": "Texto plural posts",
|
||||
"default": "publicaciones",
|
||||
"editable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"typography": {
|
||||
"label": "Tipografia",
|
||||
"priority": 30,
|
||||
"fields": {
|
||||
"heading_level": {
|
||||
"type": "select",
|
||||
"label": "Nivel de encabezado",
|
||||
"default": "h1",
|
||||
"editable": true,
|
||||
"options": ["h1", "h2", "h3", "h4", "h5", "h6"],
|
||||
"description": "Nivel semantico del titulo para SEO"
|
||||
},
|
||||
"title_size": {
|
||||
"type": "text",
|
||||
"label": "Tamano titulo",
|
||||
"default": "2rem",
|
||||
"editable": true
|
||||
},
|
||||
"title_weight": {
|
||||
"type": "text",
|
||||
"label": "Peso titulo",
|
||||
"default": "700",
|
||||
"editable": true
|
||||
},
|
||||
"description_size": {
|
||||
"type": "text",
|
||||
"label": "Tamano descripcion",
|
||||
"default": "1rem",
|
||||
"editable": true
|
||||
},
|
||||
"count_size": {
|
||||
"type": "text",
|
||||
"label": "Tamano contador",
|
||||
"default": "0.875rem",
|
||||
"editable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"colors": {
|
||||
"label": "Colores",
|
||||
"priority": 40,
|
||||
"fields": {
|
||||
"title_color": {
|
||||
"type": "color",
|
||||
"label": "Color titulo",
|
||||
"default": "#0E2337",
|
||||
"editable": true
|
||||
},
|
||||
"description_color": {
|
||||
"type": "color",
|
||||
"label": "Color descripcion",
|
||||
"default": "#6b7280",
|
||||
"editable": true
|
||||
},
|
||||
"count_bg_color": {
|
||||
"type": "color",
|
||||
"label": "Fondo contador",
|
||||
"default": "#FF8600",
|
||||
"editable": true
|
||||
},
|
||||
"count_text_color": {
|
||||
"type": "color",
|
||||
"label": "Texto contador",
|
||||
"default": "#ffffff",
|
||||
"editable": true
|
||||
},
|
||||
"prefix_color": {
|
||||
"type": "color",
|
||||
"label": "Color prefijo",
|
||||
"default": "#6b7280",
|
||||
"editable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"spacing": {
|
||||
"label": "Espaciado",
|
||||
"priority": 50,
|
||||
"fields": {
|
||||
"margin_top": {
|
||||
"type": "text",
|
||||
"label": "Margen superior",
|
||||
"default": "2rem",
|
||||
"editable": true
|
||||
},
|
||||
"margin_bottom": {
|
||||
"type": "text",
|
||||
"label": "Margen inferior",
|
||||
"default": "2rem",
|
||||
"editable": true
|
||||
},
|
||||
"padding": {
|
||||
"type": "text",
|
||||
"label": "Padding interno",
|
||||
"default": "1.5rem",
|
||||
"editable": true
|
||||
},
|
||||
"title_margin_bottom": {
|
||||
"type": "text",
|
||||
"label": "Margen inferior titulo",
|
||||
"default": "0.5rem",
|
||||
"editable": true
|
||||
},
|
||||
"count_padding": {
|
||||
"type": "text",
|
||||
"label": "Padding contador",
|
||||
"default": "0.25rem 0.75rem",
|
||||
"editable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"behavior": {
|
||||
"label": "Comportamiento",
|
||||
"priority": 70,
|
||||
"fields": {
|
||||
"is_sticky": {
|
||||
"type": "boolean",
|
||||
"label": "Header fijo",
|
||||
"default": false,
|
||||
"editable": true,
|
||||
"description": "Mantiene el header visible al hacer scroll"
|
||||
},
|
||||
"sticky_offset": {
|
||||
"type": "text",
|
||||
"label": "Offset sticky",
|
||||
"default": "0",
|
||||
"editable": true,
|
||||
"description": "Distancia desde el top cuando es sticky"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,14 +27,6 @@
|
||||
"default": true,
|
||||
"editable": true,
|
||||
"description": "Muestra el componente en pantallas < 992px"
|
||||
},
|
||||
"show_on_pages": {
|
||||
"type": "select",
|
||||
"label": "Mostrar en",
|
||||
"default": "all",
|
||||
"editable": true,
|
||||
"options": ["all", "posts", "pages"],
|
||||
"description": "Tipos de contenido donde se muestra"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -28,13 +28,12 @@
|
||||
"editable": true,
|
||||
"description": "Muestra el componente en pantallas < 992px"
|
||||
},
|
||||
"show_on_pages": {
|
||||
"type": "select",
|
||||
"label": "Mostrar en",
|
||||
"default": "posts",
|
||||
"hide_for_logged_in": {
|
||||
"type": "boolean",
|
||||
"label": "Ocultar para usuarios logueados",
|
||||
"default": false,
|
||||
"editable": true,
|
||||
"options": ["all", "posts", "pages"],
|
||||
"description": "Tipos de contenido donde se muestra"
|
||||
"description": "No mostrar el CTA a usuarios con sesión iniciada en WordPress"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -29,26 +29,19 @@
|
||||
"editable": true,
|
||||
"description": "Muestra el botón en pantallas móviles (<992px). Por defecto oculto para ahorrar espacio en navbar móvil"
|
||||
},
|
||||
"show_on_pages": {
|
||||
"type": "select",
|
||||
"label": "Mostrar en",
|
||||
"default": "all",
|
||||
"editable": true,
|
||||
"required": true,
|
||||
"options": {
|
||||
"all": "Todas las páginas",
|
||||
"home": "Solo página de inicio",
|
||||
"posts": "Solo posts individuales",
|
||||
"pages": "Solo páginas"
|
||||
},
|
||||
"description": "Define en qué páginas se mostrará el botón"
|
||||
},
|
||||
"is_critical": {
|
||||
"type": "boolean",
|
||||
"label": "CSS Crítico",
|
||||
"default": true,
|
||||
"editable": true,
|
||||
"description": "Inyectar CSS inline en <head> para optimizar LCP (componente above-the-fold)"
|
||||
},
|
||||
"hide_for_logged_in": {
|
||||
"type": "boolean",
|
||||
"label": "Ocultar para usuarios logueados",
|
||||
"default": false,
|
||||
"editable": true,
|
||||
"description": "No mostrar el botón a usuarios con sesión iniciada en WordPress"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -28,13 +28,12 @@
|
||||
"editable": true,
|
||||
"description": "Muestra el componente en pantallas < 992px"
|
||||
},
|
||||
"show_on_pages": {
|
||||
"type": "select",
|
||||
"label": "Mostrar en",
|
||||
"default": "posts",
|
||||
"hide_for_logged_in": {
|
||||
"type": "boolean",
|
||||
"label": "Ocultar para usuarios logueados",
|
||||
"default": false,
|
||||
"editable": true,
|
||||
"options": ["all", "posts", "pages"],
|
||||
"description": "Tipos de contenido donde se muestra"
|
||||
"description": "No mostrar el CTA a usuarios con sesión iniciada en WordPress"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -30,19 +30,6 @@
|
||||
"editable": true,
|
||||
"required": true,
|
||||
"description": "Muestra la imagen en dispositivos moviles (<768px)"
|
||||
},
|
||||
"show_on_pages": {
|
||||
"type": "select",
|
||||
"label": "Mostrar en",
|
||||
"default": "posts",
|
||||
"editable": true,
|
||||
"required": true,
|
||||
"options": {
|
||||
"all": "Todas las paginas",
|
||||
"posts": "Solo posts individuales",
|
||||
"pages": "Solo paginas"
|
||||
},
|
||||
"description": "Define en que tipo de contenido se muestra la imagen"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -31,20 +31,6 @@
|
||||
"required": true,
|
||||
"description": "Muestra el hero en dispositivos móviles (<768px)"
|
||||
},
|
||||
"show_on_pages": {
|
||||
"type": "select",
|
||||
"label": "Mostrar en",
|
||||
"default": "posts",
|
||||
"editable": true,
|
||||
"required": true,
|
||||
"options": {
|
||||
"all": "Todas las páginas",
|
||||
"posts": "Solo posts individuales",
|
||||
"pages": "Solo páginas",
|
||||
"home": "Solo página de inicio"
|
||||
},
|
||||
"description": "Define en qué tipo de contenido se mostrará el hero"
|
||||
},
|
||||
"is_critical": {
|
||||
"type": "boolean",
|
||||
"label": "CSS Crítico",
|
||||
|
||||
@@ -29,19 +29,6 @@
|
||||
"editable": true,
|
||||
"description": "Muestra el menú en dispositivos de escritorio (≥768px)"
|
||||
},
|
||||
"show_on_pages": {
|
||||
"type": "select",
|
||||
"label": "Mostrar en",
|
||||
"default": "all",
|
||||
"editable": true,
|
||||
"options": {
|
||||
"all": "Todas las páginas",
|
||||
"home": "Solo página de inicio",
|
||||
"posts": "Solo posts individuales",
|
||||
"pages": "Solo páginas"
|
||||
},
|
||||
"description": "Define en qué páginas se muestra el navbar"
|
||||
},
|
||||
"sticky_enabled": {
|
||||
"type": "boolean",
|
||||
"label": "Navbar fijo (sticky)",
|
||||
|
||||
271
Schemas/post-grid.json
Normal file
271
Schemas/post-grid.json
Normal file
@@ -0,0 +1,271 @@
|
||||
{
|
||||
"component_name": "post-grid",
|
||||
"version": "1.0.0",
|
||||
"description": "Grid de posts para templates de listados usando el loop principal de WordPress",
|
||||
"groups": {
|
||||
"visibility": {
|
||||
"label": "Visibilidad",
|
||||
"priority": 10,
|
||||
"fields": {
|
||||
"is_enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"label": "Habilitar componente"
|
||||
},
|
||||
"show_on_desktop": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"label": "Mostrar en desktop"
|
||||
},
|
||||
"show_on_mobile": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"label": "Mostrar en movil"
|
||||
}
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"label": "Contenido",
|
||||
"priority": 20,
|
||||
"fields": {
|
||||
"show_thumbnail": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"label": "Mostrar imagen destacada"
|
||||
},
|
||||
"show_excerpt": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"label": "Mostrar extracto"
|
||||
},
|
||||
"show_meta": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"label": "Mostrar metadatos (fecha, autor)"
|
||||
},
|
||||
"show_categories": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"label": "Mostrar categorias"
|
||||
},
|
||||
"excerpt_length": {
|
||||
"type": "select",
|
||||
"default": "20",
|
||||
"label": "Longitud del extracto (palabras)",
|
||||
"options": ["10", "15", "20", "25", "30"]
|
||||
},
|
||||
"read_more_text": {
|
||||
"type": "text",
|
||||
"default": "Leer mas",
|
||||
"label": "Texto de leer mas"
|
||||
},
|
||||
"no_posts_message": {
|
||||
"type": "text",
|
||||
"default": "No se encontraron publicaciones",
|
||||
"label": "Mensaje cuando no hay posts"
|
||||
}
|
||||
}
|
||||
},
|
||||
"typography": {
|
||||
"label": "Tipografia",
|
||||
"priority": 30,
|
||||
"fields": {
|
||||
"heading_level": {
|
||||
"type": "select",
|
||||
"default": "h3",
|
||||
"label": "Nivel de encabezado de tarjetas",
|
||||
"options": ["h2", "h3", "h4", "h5", "h6"]
|
||||
},
|
||||
"card_title_size": {
|
||||
"type": "text",
|
||||
"default": "1.1rem",
|
||||
"label": "Tamano titulo de tarjeta"
|
||||
},
|
||||
"card_title_weight": {
|
||||
"type": "text",
|
||||
"default": "600",
|
||||
"label": "Peso titulo de tarjeta"
|
||||
},
|
||||
"excerpt_size": {
|
||||
"type": "text",
|
||||
"default": "0.9rem",
|
||||
"label": "Tamano de extracto"
|
||||
},
|
||||
"meta_size": {
|
||||
"type": "text",
|
||||
"default": "0.8rem",
|
||||
"label": "Tamano de metadatos"
|
||||
}
|
||||
}
|
||||
},
|
||||
"colors": {
|
||||
"label": "Colores",
|
||||
"priority": 40,
|
||||
"fields": {
|
||||
"card_bg_color": {
|
||||
"type": "color",
|
||||
"default": "#ffffff",
|
||||
"label": "Fondo de tarjeta"
|
||||
},
|
||||
"card_title_color": {
|
||||
"type": "color",
|
||||
"default": "#0E2337",
|
||||
"label": "Color titulo de tarjeta"
|
||||
},
|
||||
"card_hover_bg_color": {
|
||||
"type": "color",
|
||||
"default": "#f9fafb",
|
||||
"label": "Fondo hover de tarjeta"
|
||||
},
|
||||
"card_border_color": {
|
||||
"type": "color",
|
||||
"default": "#e5e7eb",
|
||||
"label": "Color borde de tarjeta"
|
||||
},
|
||||
"card_hover_border_color": {
|
||||
"type": "color",
|
||||
"default": "#FF8600",
|
||||
"label": "Color borde hover"
|
||||
},
|
||||
"excerpt_color": {
|
||||
"type": "color",
|
||||
"default": "#6b7280",
|
||||
"label": "Color de extracto"
|
||||
},
|
||||
"meta_color": {
|
||||
"type": "color",
|
||||
"default": "#9ca3af",
|
||||
"label": "Color de metadatos"
|
||||
},
|
||||
"category_bg_color": {
|
||||
"type": "color",
|
||||
"default": "#FFF5EB",
|
||||
"label": "Fondo de categoria"
|
||||
},
|
||||
"category_text_color": {
|
||||
"type": "color",
|
||||
"default": "#FF8600",
|
||||
"label": "Color texto categoria"
|
||||
},
|
||||
"pagination_color": {
|
||||
"type": "color",
|
||||
"default": "#0E2337",
|
||||
"label": "Color de paginacion"
|
||||
},
|
||||
"pagination_active_bg": {
|
||||
"type": "color",
|
||||
"default": "#FF8600",
|
||||
"label": "Fondo paginacion activa"
|
||||
},
|
||||
"pagination_active_color": {
|
||||
"type": "color",
|
||||
"default": "#ffffff",
|
||||
"label": "Color texto paginacion activa"
|
||||
}
|
||||
}
|
||||
},
|
||||
"spacing": {
|
||||
"label": "Espaciado",
|
||||
"priority": 50,
|
||||
"fields": {
|
||||
"grid_gap": {
|
||||
"type": "text",
|
||||
"default": "1.5rem",
|
||||
"label": "Espacio entre tarjetas"
|
||||
},
|
||||
"card_padding": {
|
||||
"type": "text",
|
||||
"default": "1.25rem",
|
||||
"label": "Padding interno de tarjeta"
|
||||
},
|
||||
"section_margin_top": {
|
||||
"type": "text",
|
||||
"default": "0",
|
||||
"label": "Margen superior de seccion"
|
||||
},
|
||||
"section_margin_bottom": {
|
||||
"type": "text",
|
||||
"default": "2rem",
|
||||
"label": "Margen inferior de seccion"
|
||||
}
|
||||
}
|
||||
},
|
||||
"visual_effects": {
|
||||
"label": "Efectos Visuales",
|
||||
"priority": 60,
|
||||
"fields": {
|
||||
"card_border_radius": {
|
||||
"type": "text",
|
||||
"default": "0.5rem",
|
||||
"label": "Radio de borde de tarjeta"
|
||||
},
|
||||
"card_shadow": {
|
||||
"type": "text",
|
||||
"default": "0 1px 3px rgba(0,0,0,0.1)",
|
||||
"label": "Sombra de tarjeta"
|
||||
},
|
||||
"card_hover_shadow": {
|
||||
"type": "text",
|
||||
"default": "0 4px 12px rgba(0,0,0,0.15)",
|
||||
"label": "Sombra hover de tarjeta"
|
||||
},
|
||||
"card_transition": {
|
||||
"type": "text",
|
||||
"default": "all 0.3s ease",
|
||||
"label": "Transicion de tarjeta"
|
||||
},
|
||||
"image_border_radius": {
|
||||
"type": "text",
|
||||
"default": "0.375rem",
|
||||
"label": "Radio de borde de imagen"
|
||||
}
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
"label": "Disposicion",
|
||||
"priority": 80,
|
||||
"fields": {
|
||||
"columns_desktop": {
|
||||
"type": "select",
|
||||
"default": "3",
|
||||
"label": "Columnas en desktop",
|
||||
"options": ["2", "3", "4"]
|
||||
},
|
||||
"columns_tablet": {
|
||||
"type": "select",
|
||||
"default": "2",
|
||||
"label": "Columnas en tablet",
|
||||
"options": ["1", "2", "3"]
|
||||
},
|
||||
"columns_mobile": {
|
||||
"type": "select",
|
||||
"default": "1",
|
||||
"label": "Columnas en movil",
|
||||
"options": ["1", "2"]
|
||||
},
|
||||
"image_position": {
|
||||
"type": "select",
|
||||
"default": "top",
|
||||
"label": "Posicion de imagen",
|
||||
"options": ["top", "left", "none"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"media": {
|
||||
"label": "Medios",
|
||||
"priority": 90,
|
||||
"fields": {
|
||||
"fallback_image": {
|
||||
"type": "url",
|
||||
"default": "",
|
||||
"label": "Imagen por defecto (URL)"
|
||||
},
|
||||
"fallback_image_alt": {
|
||||
"type": "text",
|
||||
"default": "Imagen por defecto",
|
||||
"label": "Texto alternativo imagen por defecto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,14 +27,6 @@
|
||||
"default": true,
|
||||
"editable": true,
|
||||
"description": "Muestra el componente en pantallas < 992px"
|
||||
},
|
||||
"show_on_pages": {
|
||||
"type": "select",
|
||||
"label": "Mostrar en",
|
||||
"default": "posts",
|
||||
"editable": true,
|
||||
"options": ["all", "posts", "pages"],
|
||||
"description": "Tipos de contenido donde se muestra"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -27,14 +27,6 @@
|
||||
"default": true,
|
||||
"editable": true,
|
||||
"description": "Muestra el componente en pantallas < 992px"
|
||||
},
|
||||
"show_on_pages": {
|
||||
"type": "select",
|
||||
"label": "Mostrar en",
|
||||
"default": "posts",
|
||||
"editable": true,
|
||||
"options": ["all", "posts", "pages"],
|
||||
"description": "Tipos de contenido donde se muestra"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -28,14 +28,6 @@
|
||||
"editable": true,
|
||||
"description": "Muestra el componente en pantallas < 992px"
|
||||
},
|
||||
"show_on_pages": {
|
||||
"type": "select",
|
||||
"label": "Mostrar en",
|
||||
"default": "posts",
|
||||
"editable": true,
|
||||
"options": ["all", "posts", "pages"],
|
||||
"description": "Tipos de contenido donde se muestra"
|
||||
},
|
||||
"is_critical": {
|
||||
"type": "boolean",
|
||||
"label": "CSS Crítico",
|
||||
|
||||
@@ -15,20 +15,6 @@
|
||||
"required": true,
|
||||
"description": "Activa o desactiva la barra de notificación superior"
|
||||
},
|
||||
"show_on_pages": {
|
||||
"type": "select",
|
||||
"label": "Mostrar en",
|
||||
"default": "all",
|
||||
"editable": true,
|
||||
"required": true,
|
||||
"options": {
|
||||
"all": "Todas las páginas",
|
||||
"home": "Solo página de inicio",
|
||||
"posts": "Solo posts individuales",
|
||||
"pages": "Solo páginas"
|
||||
},
|
||||
"description": "Define en qué páginas se mostrará la barra"
|
||||
},
|
||||
"show_on_desktop": {
|
||||
"type": "boolean",
|
||||
"label": "Mostrar en desktop",
|
||||
@@ -51,6 +37,13 @@
|
||||
"default": true,
|
||||
"editable": true,
|
||||
"description": "Inyectar CSS inline en <head> para optimizar LCP (componente above-the-fold)"
|
||||
},
|
||||
"hide_for_logged_in": {
|
||||
"type": "boolean",
|
||||
"label": "Ocultar para usuarios logueados",
|
||||
"default": false,
|
||||
"editable": true,
|
||||
"description": "No mostrar la barra a usuarios con sesión iniciada en WordPress"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Application\UseCases;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\WrapperVisibilityCheckerInterface;
|
||||
|
||||
/**
|
||||
* UseCase: Verificar si un wrapper de componente debe renderizarse
|
||||
*
|
||||
* Responsabilidad: Orquestar la lógica de verificación de visibilidad
|
||||
* combinando múltiples criterios:
|
||||
* 1. Componente habilitado (is_enabled)
|
||||
* 2. Visible en dispositivo actual (show_on_mobile/desktop)
|
||||
* 3. No excluido por reglas (categoría, post ID, URL, page visibility)
|
||||
*
|
||||
* @package ROITheme\Shared\Application\UseCases
|
||||
* @see Plan 99.15 - Fix Empty Layout Wrappers
|
||||
*/
|
||||
final class CheckWrapperVisibilityUseCase
|
||||
{
|
||||
public function __construct(
|
||||
private readonly WrapperVisibilityCheckerInterface $visibilityChecker
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Ejecuta la verificación de visibilidad del wrapper
|
||||
*
|
||||
* @param string $componentName Nombre del componente (kebab-case)
|
||||
* @param bool $isMobile True si es dispositivo móvil
|
||||
* @return bool True si el wrapper debe renderizarse
|
||||
*/
|
||||
public function execute(string $componentName, bool $isMobile): bool
|
||||
{
|
||||
// Criterio 1: Debe estar habilitado
|
||||
if (!$this->visibilityChecker->isEnabled($componentName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Criterio 2: Debe ser visible en el dispositivo actual
|
||||
if (!$this->visibilityChecker->isVisibleOnDevice($componentName, $isMobile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Criterio 3: No debe estar excluido
|
||||
if (!$this->visibilityChecker->isNotExcluded($componentName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user