diff --git a/README.md b/README.md
index c4014ea..0524f81 100644
--- a/README.md
+++ b/README.md
@@ -30,17 +30,22 @@ A WordPress plugin to manage slides for Hi-School websites.
Display slides on any page or post using the shortcode:
```
-[hssm_slides]
+[hssm_slider]
```
+`[hssm_slides]` is an alias for the same shortcode.
+
Shortcode parameters:
-- `count` - Number of slides to display (default: 5)
-- `category` - Filter slides by category (optional)
+- `site` - Target site slug (manager mode)
+- `date` - Preview date `YYYY-MM-DD` (manager mode)
+- `limit` or `count` - Maximum slides to display (default: 10)
+- `slide` - Optional slide ID to feature first
Examples:
```
-[hssm_slides count="3"]
-[hssm_slides count="10" category="featured"]
+[hssm_slider]
+[hssm_slides]
+[hssm_slider site="hi-school" limit="5"]
```
## File Structure
diff --git a/assets/css/dashboard.css b/assets/css/dashboard.css
index d569319..dc021af 100644
--- a/assets/css/dashboard.css
+++ b/assets/css/dashboard.css
@@ -7,6 +7,68 @@
* @since 1.0.0
*/
+/* Hide Divi theme header/logo on plugin tool pages */
+body.hssm-tool-page #main-header,
+body.hssm-tool-page #top-header,
+body.hssm-tool-page .et-l--header,
+body.hssm-tool-page header.et-l,
+body.hssm-tool-page .logo_container,
+body.hssm-tool-page #et-top-navigation,
+body.page-template-page-slide-preview #main-header,
+body.page-template-page-slide-preview #top-header,
+body.page-template-page-slide-preview .et-l--header,
+body.page-template-page-slide-preview header.et-l,
+body.page-template-page-slide-dashboard #main-header,
+body.page-template-page-slide-dashboard #top-header,
+body.page-template-page-slide-dashboard .et-l--header,
+body.page-template-page-slide-dashboard header.et-l {
+ display: none !important;
+}
+
+body.hssm-tool-page {
+ padding-top: 0 !important;
+}
+
+body.hssm-tool-page #page-container,
+body.hssm-tool-page #et-boc,
+body.hssm-tool-page .et-l--body {
+ padding-top: 0 !important;
+ margin-top: 0 !important;
+}
+
+.hssm-preview-approve-bar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+ max-width: 1400px;
+ margin: 20px auto 0;
+ padding: 16px 24px;
+ background: #fff8e5;
+ border: 1px solid #f0c36d;
+ border-radius: 8px;
+}
+
+.hssm-preview-approve-copy {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ color: #1d2327;
+}
+
+.hssm-preview-approve-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+@media (max-width: 782px) {
+ .hssm-preview-approve-bar {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+}
+
/* ==========================================
Full-Width Layout Override
========================================== */
@@ -1117,6 +1179,11 @@
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
+.hssm-slide-item-focus {
+ border-color: #2271b1;
+ box-shadow: 0 0 0 2px rgba(34, 113, 177, 0.15);
+}
+
.hssm-slide-info {
flex: 1;
}
@@ -1399,7 +1466,7 @@
.hssm-table-header {
display: grid;
- grid-template-columns: 80px 1fr 120px 180px 150px 140px;
+ grid-template-columns: 80px 1fr 150px 180px 150px 180px;
gap: 15px;
padding: 20px 25px;
background: #f8f9fa;
@@ -1416,7 +1483,7 @@
.hssm-slide-row {
display: grid;
- grid-template-columns: 80px 1fr 120px 180px 150px 140px;
+ grid-template-columns: 80px 1fr 150px 180px 150px 180px;
gap: 15px;
padding: 20px 25px;
border-bottom: 1px solid #f1f3f4;
@@ -1428,6 +1495,11 @@
background-color: #f8f9fa;
}
+.hssm-slide-row-focus {
+ background-color: #e8f5e9;
+ outline: 2px solid #46b450;
+}
+
.hssm-slide-row:last-child {
border-bottom: none;
}
@@ -1532,6 +1604,22 @@
border-color: #1e7e34;
}
+.hssm-slide-actions .hssm-approved-slide,
+.hssm-slide-actions .hssm-approved-slide:disabled,
+.hssm-slide-actions .hssm-approved-slide:hover {
+ background: #275191;
+ background-color: #275191;
+ border-color: #275191;
+ color: #fff;
+}
+
+.hssm-slide-actions .hssm-approved-slide:disabled {
+ opacity: 1;
+ cursor: default;
+ transform: none;
+ box-shadow: none;
+}
+
/* Empty State */
.hssm-empty-state {
text-align: center;
@@ -1872,4 +1960,69 @@
/* Overlap handling override */
.hssm-datepicker-start.hssm-datepicker-end a {
background: linear-gradient(135deg, #d4edda 50%, #f8d7da 50%) !important;
+}
+
+/*
+ * Theme CSS targets input[type="text"] (higher specificity than .hssm-input)
+ * and paints those fields black/pill-shaped. Keep plugin forms white.
+ */
+.hssm-dashboard-main input[type="text"],
+.hssm-dashboard-main input[type="url"],
+.hssm-dashboard-main input[type="email"],
+.hssm-dashboard-main input[type="search"],
+.hssm-dashboard-main input[type="number"],
+.hssm-dashboard-main input[type="date"],
+.hssm-dashboard-main input[type="tel"],
+.hssm-dashboard-main textarea,
+.hssm-dashboard-main select,
+.hssm-dashboard-main .hssm-input,
+.hssm-dashboard-main .hssm-textarea,
+.hssm-dashboard-main .hssm-select,
+input.hssm-input,
+input.hssm-input-small,
+textarea.hssm-textarea,
+select.hssm-select {
+ background-color: #ffffff !important;
+ color: #1d2327 !important;
+ border: 1px solid #ced4da !important;
+ border-radius: 4px !important;
+ color-scheme: light;
+}
+
+.hssm-dashboard-main input[type="text"]:focus,
+.hssm-dashboard-main input[type="url"]:focus,
+.hssm-dashboard-main input[type="email"]:focus,
+.hssm-dashboard-main input[type="search"]:focus,
+.hssm-dashboard-main input[type="number"]:focus,
+.hssm-dashboard-main input[type="date"]:focus,
+.hssm-dashboard-main input[type="tel"]:focus,
+.hssm-dashboard-main textarea:focus,
+.hssm-dashboard-main select:focus,
+.hssm-dashboard-main .hssm-input:focus,
+.hssm-dashboard-main .hssm-textarea:focus,
+.hssm-dashboard-main .hssm-select:focus,
+input.hssm-input:focus,
+input.hssm-input-small:focus,
+textarea.hssm-textarea:focus,
+select.hssm-select:focus {
+ background-color: #ffffff !important;
+ color: #1d2327 !important;
+ border-color: #007cba !important;
+}
+
+.hssm-dashboard-main input::placeholder,
+input.hssm-input::placeholder,
+input.hssm-input-small::placeholder,
+textarea.hssm-textarea::placeholder {
+ color: #6c757d;
+ opacity: 1;
+}
+
+.hssm-dashboard-main input:-webkit-autofill,
+.hssm-dashboard-main input:-webkit-autofill:hover,
+.hssm-dashboard-main input:-webkit-autofill:focus,
+input.hssm-input:-webkit-autofill,
+input.hssm-input-small:-webkit-autofill {
+ -webkit-box-shadow: 0 0 0 1000px #ffffff inset !important;
+ -webkit-text-fill-color: #1d2327 !important;
}
\ No newline at end of file
diff --git a/assets/css/frontend.css b/assets/css/frontend.css
deleted file mode 100644
index aa34b61..0000000
--- a/assets/css/frontend.css
+++ /dev/null
@@ -1,61 +0,0 @@
-/**
- * Hi-School Slide Manager - Frontend Styles
- */
-
-.hssm-slides-container {
- display: flex;
- flex-wrap: wrap;
- gap: 20px;
- margin: 20px 0;
-}
-
-.hssm-slide {
- background: #fff;
- border: 1px solid #ddd;
- border-radius: 8px;
- padding: 20px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- flex: 1;
- min-width: 300px;
- transition: transform 0.3s ease, box-shadow 0.3s ease;
-}
-
-.hssm-slide:hover {
- transform: translateY(-2px);
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
-}
-
-.hssm-slide h3 {
- margin: 0 0 15px 0;
- color: #333;
- font-size: 1.4em;
- border-bottom: 2px solid #0073aa;
- padding-bottom: 10px;
-}
-
-.hssm-slide-content {
- margin-bottom: 15px;
- line-height: 1.6;
- color: #555;
-}
-
-.hssm-slide-image {
- text-align: center;
-}
-
-.hssm-slide-image img {
- max-width: 100%;
- height: auto;
- border-radius: 4px;
-}
-
-/* Responsive design */
-@media (max-width: 768px) {
- .hssm-slides-container {
- flex-direction: column;
- }
-
- .hssm-slide {
- min-width: auto;
- }
-}
\ No newline at end of file
diff --git a/assets/js/dashboard.js b/assets/js/dashboard.js
index f37cb47..cd0ab94 100644
--- a/assets/js/dashboard.js
+++ b/assets/js/dashboard.js
@@ -23,20 +23,24 @@
hasMore: false,
filters: {
status: '',
- site: '',
- client: ''
+ site: ''
},
// Edit mode state
isEditMode: false,
editingSlideId: null,
currentSlides: null,
+ focusSlideId: null,
// Initialize the dashboard
init: function() {
+ const params = new URLSearchParams(window.location.search);
+ this.focusSlideId = params.get('hssm_focus') || null;
+
this.bindEvents();
this.initializeTabs();
this.addInitialSlideForm();
+ this.populateFilterDropdowns();
this.loadExistingSlides();
// Initialize site data
window.hssmSites = window.hssmSites || [];
@@ -64,8 +68,6 @@
$(document).on('click', '.hssm-remove-button', this.removeButtonRow.bind(this));
// Slide actions
- $(document).on('click', '.hssm-save-slide', this.saveSlide.bind(this));
- $(document).on('click', '#save-all-slides', this.saveAllSlides.bind(this));
$(document).on('click', '.hssm-preview-slide', this.previewSlide.bind(this));
$(document).on('click', '.hssm-schedule-slide', this.scheduleSlide.bind(this));
$(document).on('click', '.hssm-publish-slide', this.publishSlide.bind(this));
@@ -89,6 +91,8 @@
// Filtering and management (slide row edit/delete bound in bindSlideActions after list load)
$(document).on('click', '#apply-filters', this.applyFilters.bind(this));
+ $(document).on('click', '#clear-filters', this.clearFilters.bind(this));
+ $(document).on('change', '#filter-site, #filter-status', this.applyFilters.bind(this));
// Edit form actions — register once; do not re-bind in populateEditForm (prevents duplicate toasts)
$(document).on('click.hssmDashboard', '.hssm-cancel-edit, .hssm-cancel-edit-btn', (e) => {
@@ -104,14 +108,18 @@
// Additional functionality
$(document).on('click', '#import-slides', this.importSlides.bind(this));
$(document).on('click', '#export-slides', this.exportSlides.bind(this));
- $(document).on('click', '#save-draft-all', this.saveAllAsDrafts.bind(this));
-
+
// Notification Settings
$(document).on('submit', '#notification-settings-form', this.saveNotificationSettings.bind(this));
},
// Initialize tab functionality
initializeTabs: function() {
+ // Confirmation / email links should land on the manage list.
+ if (this.focusSlideId) {
+ sessionStorage.setItem('hssm_active_tab', 'manage-slides');
+ }
+
// Check for saved tab in sessionStorage
const savedTab = sessionStorage.getItem('hssm_active_tab');
@@ -425,84 +433,6 @@
});
},
- // Save individual slide
- saveSlide: function(e) {
- e.preventDefault();
-
- const formId = $(e.currentTarget).data('form');
- const $form = $(`.hssm-slide-form[data-form-id="${formId}"]`);
-
- this.submitSlideForm($form);
- },
-
- // Save all slides
- saveAllSlides: function(e) {
- if (e) e.preventDefault();
-
- const $forms = $('.hssm-slide-form');
- let savedCount = 0;
- let errorCount = 0;
-
- $forms.each((index, form) => {
- this.submitSlideForm($(form), () => {
- savedCount++;
- this.checkAllSaved(savedCount, errorCount, $forms.length);
- }, () => {
- errorCount++;
- this.checkAllSaved(savedCount, errorCount, $forms.length);
- });
- });
- },
-
- // Check if all forms have been processed
- checkAllSaved: function(savedCount, errorCount, totalCount) {
- if (savedCount + errorCount === totalCount) {
- if (errorCount === 0) {
- this.showMessage(`All ${savedCount} slides saved successfully!`, 'success');
- } else {
- this.showMessage(`${savedCount} slides saved, ${errorCount} failed.`, 'warning');
- }
- }
- },
-
- // Submit individual slide form
- submitSlideForm: function($form, successCallback, errorCallback) {
- if (!this.validateForm($form)) {
- if (errorCallback) errorCallback();
- return;
- }
-
- const formData = this.collectFormData($form);
-
- $form.addClass('hssm-loading');
-
- formData.append('nonce', hssmDashboard.nonce);
- $.ajax({
- url: hssmDashboard.ajaxurl,
- type: 'POST',
- data: formData,
- processData: false,
- contentType: false,
- success: (response) => {
- $form.removeClass('hssm-loading');
-
- if (response.success) {
- this.showMessage(response.data.message, 'success');
- this.clearForm($form);
- if (successCallback) successCallback();
- } else {
- this.showMessage(response.data.message || 'Save failed', 'error');
- if (errorCallback) errorCallback();
- }
- },
- error: () => {
- $form.removeClass('hssm-loading');
- this.showMessage('Save failed. Please try again.', 'error');
- if (errorCallback) errorCallback();
- }
- });
- },
-
// Clear all field errors in a form
clearFieldErrors: function($form) {
$form.find('.hssm-field-error').remove();
@@ -771,7 +701,11 @@
type: 'POST',
data: {
action: 'hssm_get_slides',
- nonce: hssmDashboard.nonce
+ nonce: hssmDashboard.nonce,
+ page: this.currentPage,
+ per_page: this.perPage,
+ status_filter: this.filters.status || '',
+ site_filter: this.filters.site || ''
},
beforeSend: function(xhr, settings) {
// Optional: Add loading indicators
@@ -779,6 +713,7 @@
success: (response) => {
if (response.success && response.data && response.data.slides) {
this.renderSlidesList(response.data.slides);
+ this.highlightFocusedSlide();
} else {
console.error('Failed to get slides:', response);
$slidesList.html('
Failed to load slides. Response format unexpected.
');
@@ -807,7 +742,7 @@
${this.hasActiveFilters() ? 'No slides match the current filters.' : 'Create your first slide to get started!'}
+
@@ -1302,6 +1294,38 @@
});
},
+ approveSlide: function(slideId, approveAction) {
+ const confirmText = (hssmDashboard.strings && hssmDashboard.strings.approve_confirm)
+ ? hssmDashboard.strings.approve_confirm
+ : 'Approve this slide?';
+
+ if (!confirm(confirmText)) {
+ return;
+ }
+
+ $.ajax({
+ url: hssmDashboard.ajaxurl,
+ type: 'POST',
+ data: {
+ action: 'hssm_approve_slide',
+ nonce: hssmDashboard.nonce,
+ slide_id: slideId,
+ approve_action: approveAction || 'schedule'
+ },
+ success: (response) => {
+ if (response.success) {
+ this.showMessage((hssmDashboard.strings && hssmDashboard.strings.approve_success) || 'Slide approved.', 'success');
+ this.loadExistingSlides();
+ } else {
+ this.showMessage(response.data?.message || 'Failed to approve slide', 'error');
+ }
+ },
+ error: () => {
+ this.showMessage('Failed to approve slide. Please try again.', 'error');
+ }
+ });
+ },
+
// Toggle slide status
toggleSlideStatus: function(slideId) {
$.ajax({
@@ -1344,15 +1368,73 @@
}, dismissMs);
},
- // Placeholder methods for future implementation
- previewSlide: function(e) {
- e.preventDefault();
- this.showMessage('Preview functionality coming soon!', 'info');
+ populateFilterDropdowns: function() {
+ const $site = $('#filter-site');
+
+ if ($site.length && window.hssmSites && $site.children().length <= 1) {
+ window.hssmSites.forEach((site) => {
+ $site.append(`
`);
+ });
+ }
+ },
+
+ hasActiveFilters: function() {
+ return !!(this.filters.status || this.filters.site);
+ },
+
+ readFiltersFromForm: function() {
+ this.filters.status = $('#filter-status').val() || '';
+ this.filters.site = $('#filter-site').val() || '';
},
applyFilters: function(e) {
+ if (e && e.preventDefault) {
+ e.preventDefault();
+ }
+ this.readFiltersFromForm();
+ this.currentPage = 1;
+ this.loadExistingSlides();
+ },
+
+ clearFilters: function(e) {
+ if (e && e.preventDefault) {
+ e.preventDefault();
+ }
+ this.filters = { status: '', site: '' };
+ $('#filter-status, #filter-site').val('');
+ this.currentPage = 1;
+ this.loadExistingSlides();
+ },
+
+ buildPreviewUrl: function(slide) {
+ const base = (hssmDashboard.previewUrl || '/slide-preview/').replace(/\/?$/, '/');
+ const params = new URLSearchParams();
+
+ if (slide && slide.id) {
+ params.set('hssm_preview', slide.id);
+ }
+ if (slide && slide.start_date) {
+ params.set('hssm_date', slide.start_date);
+ }
+ if (slide && slide.site_slugs && slide.site_slugs.length) {
+ params.set('hssm_site', slide.site_slugs[0]);
+ }
+
+ const query = params.toString();
+ return query ? `${base}?${query}` : base;
+ },
+
+ previewSlide: function(e) {
e.preventDefault();
- this.showMessage('Filter functionality coming soon!', 'info');
+ const slideId = $(e.currentTarget).data('slide-id') || this.editingSlideId;
+
+ if (!slideId) {
+ this.showMessage((hssmDashboard.strings && hssmDashboard.strings.preview_save_first) || 'Save the slide first, then preview it as a banner.', 'info');
+ return;
+ }
+
+ const slide = this.findSlideById(slideId) || { id: slideId };
+ window.open(this.buildPreviewUrl(slide), '_blank', 'noopener');
},
// Load analytics data
@@ -1386,31 +1468,6 @@
this.showMessage('Slide export functionality coming soon!', 'info');
},
- // Save all slides as drafts
- saveAllAsDrafts: function(e) {
- e.preventDefault();
-
- const $forms = $('.hssm-slide-form');
- let savedCount = 0;
- let errorCount = 0;
-
- $forms.each((index, form) => {
- const $form = $(form);
-
- // Override status to draft
- const formData = this.collectFormData($form);
- formData.append('post_status', 'draft');
-
- this.submitSlideForm($form, () => {
- savedCount++;
- this.checkAllSaved(savedCount, errorCount, $forms.length);
- }, () => {
- errorCount++;
- this.checkAllSaved(savedCount, errorCount, $forms.length);
- });
- });
- },
-
// Load notification settings
loadNotificationSettings: function() {
$.ajax({
diff --git a/assets/js/frontend.js b/assets/js/frontend.js
deleted file mode 100644
index ad1e0cb..0000000
--- a/assets/js/frontend.js
+++ /dev/null
@@ -1,79 +0,0 @@
-/**
- * Hi-School Slide Manager - Frontend JavaScript
- */
-
-jQuery(document).ready(function($) {
- 'use strict';
-
- // Initialize slide functionality
- initializeSlides();
-
- /**
- * Initialize slide functionality
- */
- function initializeSlides() {
- $('.hssm-slides-container').each(function() {
- var $container = $(this);
- var $slides = $container.find('.hssm-slide');
-
- // Add fade-in animation
- $slides.each(function(index) {
- var $slide = $(this);
- setTimeout(function() {
- $slide.addClass('hssm-fade-in');
- }, index * 100);
- });
-
- // Add click handlers for interactive slides
- $slides.on('click', function() {
- var $slide = $(this);
- $slide.toggleClass('hssm-slide-expanded');
- });
- });
- }
-
- /**
- * Handle slide interactions
- */
- $(document).on('click', '.hssm-slide-toggle', function(e) {
- e.preventDefault();
- var $button = $(this);
- var $slide = $button.closest('.hssm-slide');
- var $content = $slide.find('.hssm-slide-content');
-
- $content.slideToggle(300);
- $button.text($content.is(':visible') ? 'Hide' : 'Show');
- });
-
- /**
- * Handle keyboard navigation
- */
- $(document).on('keydown', '.hssm-slide', function(e) {
- if (e.which === 13 || e.which === 32) { // Enter or Space
- e.preventDefault();
- $(this).click();
- }
- });
-});
-
-// Add CSS classes via JavaScript for animations
-document.addEventListener('DOMContentLoaded', function() {
- var style = document.createElement('style');
- style.textContent = `
- .hssm-slide {
- opacity: 0;
- transform: translateY(20px);
- transition: opacity 0.5s ease, transform 0.5s ease;
- }
-
- .hssm-slide.hssm-fade-in {
- opacity: 1;
- transform: translateY(0);
- }
-
- .hssm-slide.hssm-slide-expanded {
- transform: scale(1.02);
- }
- `;
- document.head.appendChild(style);
-});
\ No newline at end of file
diff --git a/assets/js/preview.js b/assets/js/preview.js
index 580a773..8cbc9ed 100644
--- a/assets/js/preview.js
+++ b/assets/js/preview.js
@@ -110,7 +110,7 @@
action: 'hssm_approve_slide',
nonce: hssmPreview.nonce,
slide_id: slideId,
- approve_action: 'schedule' // Smart approve: schedules if future date exists, otherwise publishes
+ approve_action: $btn.data('approve-action') || 'schedule'
},
success: (response) => {
if (response.success) {
diff --git a/hi-school-slide-manager.php b/hi-school-slide-manager.php
index cfc221d..a5bff2a 100644
--- a/hi-school-slide-manager.php
+++ b/hi-school-slide-manager.php
@@ -3,7 +3,7 @@
* Plugin Name: Hi-School Slide Manager
* Plugin URI: https://hi-school.com/slide-manager
* Description: Multi-site slide management system for Hi-School websites. Allows staff to create and schedule slides on the main site, display them as headers on companion sites via REST API, with advanced preview and editing capabilities.
- * Version: 1.1.2
+ * Version: 1.1.9
* Author: Hi-School Development Team
* Author URI: https://hi-school.com
* License: GPL v2 or later
@@ -22,7 +22,7 @@ if ( ! defined( 'ABSPATH' ) ) {
}
// Plugin constants
-define( 'HSSM_VERSION', '1.1.2' );
+define( 'HSSM_VERSION', '1.1.9' );
define( 'HSSM_PLUGIN_FILE', __FILE__ );
define( 'HSSM_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'HSSM_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
@@ -50,6 +50,79 @@ function hssm_debug_log( $message ) {
error_log( 'HSSM: ' . $message );
}
+/**
+ * Permalink for a published page using a plugin template.
+ *
+ * Prefers a matching slug, then any page with the template, including children.
+ * get_pages() with hierarchical=1 can skip child pages, so this uses WP_Query.
+ *
+ * @since 1.1.3
+ * @param string $template Page template filename.
+ * @param string $fallback_path Optional path used when no page is found.
+ * @return string
+ */
+function hssm_get_page_url_by_template( $template, $fallback_path = '' ) {
+ $preferred_slug = '';
+ if ( 'page-slide-preview.php' === $template ) {
+ $preferred_slug = 'slide-preview';
+ } elseif ( 'page-slide-dashboard.php' === $template ) {
+ $preferred_slug = 'slide-dashboard';
+ } elseif ( 'page-slide-confirmation.php' === $template ) {
+ $preferred_slug = 'slide-confirmation';
+ }
+
+ $query = new WP_Query(
+ array(
+ 'post_type' => 'page',
+ 'post_status' => array( 'publish', 'private' ),
+ 'posts_per_page' => 20,
+ 'no_found_rows' => true,
+ 'meta_key' => '_wp_page_template',
+ 'meta_value' => $template,
+ 'orderby' => array(
+ 'menu_order' => 'ASC',
+ 'ID' => 'ASC',
+ ),
+ )
+ );
+
+ $match = null;
+ if ( $query->have_posts() ) {
+ foreach ( $query->posts as $page ) {
+ if ( $preferred_slug && $preferred_slug === $page->post_name ) {
+ $match = $page;
+ break;
+ }
+ if ( ! $match ) {
+ $match = $page;
+ }
+ }
+ }
+ wp_reset_postdata();
+
+ if ( $match ) {
+ return get_permalink( $match->ID );
+ }
+
+ $path_candidates = array();
+ if ( $preferred_slug ) {
+ $path_candidates[] = 'slide-dashboard/' . $preferred_slug;
+ $path_candidates[] = $preferred_slug;
+ }
+ if ( $fallback_path ) {
+ $path_candidates[] = trim( $fallback_path, '/' );
+ }
+
+ foreach ( array_unique( $path_candidates ) as $path ) {
+ $page = get_page_by_path( $path );
+ if ( $page ) {
+ return get_permalink( $page->ID );
+ }
+ }
+
+ return $fallback_path ? home_url( $fallback_path ) : '';
+}
+
// Include core files
require_once HSSM_PLUGIN_DIR . 'includes/class-hssm-autoloader.php';
require_once HSSM_PLUGIN_DIR . 'includes/class-hssm-main.php';
diff --git a/includes/class-hssm-admin.php b/includes/class-hssm-admin.php
index b351cc2..1722ce6 100644
--- a/includes/class-hssm-admin.php
+++ b/includes/class-hssm-admin.php
@@ -90,7 +90,6 @@ class HSSM_Admin {
register_setting( 'hssm_settings', 'hssm_plugin_mode' );
register_setting( 'hssm_settings', 'hssm_api_url' );
register_setting( 'hssm_settings', 'hssm_client_site_slug' );
- register_setting( 'hssm_settings', 'hssm_notification_emails' );
register_setting( 'hssm_settings', 'hssm_cache_duration' );
register_setting( 'hssm_settings', 'hssm_api_keys' );
register_setting( 'hssm_settings', 'hssm_client_api_key' );
@@ -158,14 +157,6 @@ class HSSM_Admin {
'hssm_general'
);
- add_settings_field(
- 'hssm_notification_emails',
- __( 'Notification Recipients', 'hi-school-slide-manager' ),
- array( $this, 'notification_emails_field' ),
- 'hssm-settings',
- 'hssm_notifications'
- );
-
add_settings_field(
'hssm_api_keys',
__( 'API Keys', 'hi-school-slide-manager' ),
@@ -312,10 +303,6 @@ Headers: X-HSSM-API-Key: your-api-key
"name": "Hi-School Pharmacy"
}
],
- "client": {
- "slug": "client-name",
- "name": "Client Name"
- },
"status": "publish",
"created": "2025-01-01 12:00:00",
"modified": "2025-01-01 12:00:00"
@@ -415,12 +402,6 @@ Headers: X-HSSM-API-Key: your-api-key
if ( isset( $_POST['hssm_cache_duration'] ) ) {
update_option( 'hssm_cache_duration', absint( $_POST['hssm_cache_duration'] ) );
}
- if ( isset( $_POST['hssm_notification_emails'] ) ) {
- $emails = explode( "\n", str_replace( "\r", "", $_POST['hssm_notification_emails'] ) );
- $emails = array_map( 'sanitize_email', $emails );
- $emails = array_filter( $emails );
- update_option( 'hssm_notification_emails', $emails );
- }
add_settings_error( 'hssm_settings', 'settings_saved', __( 'Settings saved successfully.', 'hi-school-slide-manager' ), 'success' );
}
@@ -456,7 +437,11 @@ Headers: X-HSSM-API-Key: your-api-key
* Notifications section callback
*/
public function notifications_section_callback() {
- echo '
' . __( 'Configure email notifications for new slides.', 'hi-school-slide-manager' ) . '
';
+ $dashboard_url = hssm_get_page_url_by_template( 'page-slide-dashboard.php', '/slide-dashboard/' );
+ echo '
' . esc_html__( 'Email recipients are managed on the Slide Dashboard Notifications tab.', 'hi-school-slide-manager' ) . '
';
+ if ( $dashboard_url ) {
+ echo '
' . esc_html__( 'Open Slide Dashboard', 'hi-school-slide-manager' ) . '
';
+ }
}
/**
@@ -526,15 +511,6 @@ Headers: X-HSSM-API-Key: your-api-key
echo '
' . __( 'Recommended cache duration for API responses (seconds)', 'hi-school-slide-manager' ) . '';
}
- /**
- * Notification emails field
- */
- public function notification_emails_field() {
- $emails = get_option( 'hssm_notification_emails', array() );
- echo '
';
- echo '
' . __( 'Enter email addresses (one per line) to receive notifications when new slides are published.', 'hi-school-slide-manager' ) . '';
- }
-
/**
* API keys field
*/
diff --git a/includes/class-hssm-api-client.php b/includes/class-hssm-api-client.php
index 63d78eb..8609ac2 100644
--- a/includes/class-hssm-api-client.php
+++ b/includes/class-hssm-api-client.php
@@ -44,10 +44,11 @@ class HSSM_API_Client {
$full_url = trailingslashit( $api_url ) . $endpoint;
$args = array(
- 'timeout' => 15,
- 'headers' => array(
+ 'timeout' => 15,
+ 'sslverify' => $this->should_verify_ssl( $full_url ),
+ 'headers' => array(
'X-HSSM-API-Key' => $api_key,
- 'Accept' => 'application/json',
+ 'Accept' => 'application/json',
),
);
@@ -82,6 +83,27 @@ class HSSM_API_Client {
*
* @since 1.1.0
*/
+ /**
+ * Verify SSL except on local Local/WP Engine hosts with self-signed certs.
+ *
+ * @since 1.1.3
+ * @param string $url Request URL.
+ * @return bool
+ */
+ private function should_verify_ssl( $url ) {
+ $host = wp_parse_url( $url, PHP_URL_HOST );
+ $local_hosts = array( 'localhost', '127.0.0.1' );
+ $is_local = in_array( $host, $local_hosts, true ) || ( is_string( $host ) && preg_match( '/\.local$/', $host ) );
+
+ /**
+ * Filter whether the client API should verify SSL certificates.
+ *
+ * @param bool $verify Whether to verify SSL. False for *.local hosts.
+ * @param string $url Request URL.
+ */
+ return (bool) apply_filters( 'hssm_api_sslverify', ! $is_local, $url );
+ }
+
public function clear_cache() {
$site_slug = get_option( 'hssm_client_site_slug' );
if ( ! empty( $site_slug ) ) {
diff --git a/includes/class-hssm-frontend.php b/includes/class-hssm-frontend.php
index 562dab1..4a9671a 100644
--- a/includes/class-hssm-frontend.php
+++ b/includes/class-hssm-frontend.php
@@ -38,13 +38,12 @@ class HSSM_Frontend {
// Register page template
add_filter( 'theme_page_templates', array( $this, 'register_page_template' ) );
add_filter( 'page_template', array( $this, 'load_page_template' ) );
+ add_filter( 'request', array( $this, 'keep_preview_page_query' ) );
// Handle page routing
add_action( 'template_redirect', array( $this, 'handle_page_routing' ) );
// AJAX handlers
- add_action( 'wp_ajax_hssm_save_slide', array( $this, 'ajax_save_slide' ) );
- add_action( 'wp_ajax_hssm_upload_image', array( $this, 'ajax_upload_image' ) );
add_action( 'wp_ajax_hssm_get_slides', array( $this, 'ajax_get_slides' ) );
add_action( 'wp_ajax_hssm_get_slide_for_edit', array( $this, 'ajax_get_slide_for_edit' ) );
add_action( 'wp_ajax_hssm_delete_slide', array( $this, 'ajax_delete_slide' ) );
@@ -54,7 +53,6 @@ class HSSM_Frontend {
add_action( 'wp_ajax_hssm_duplicate_slide', array( $this, 'ajax_duplicate_slide' ) );
add_action( 'wp_ajax_hssm_toggle_slide_status', array( $this, 'ajax_toggle_slide_status' ) );
add_action( 'wp_ajax_hssm_approve_slide', array( $this, 'ajax_approve_slide' ) );
- add_action( 'wp_ajax_hssm_preview_slides', array( $this, 'ajax_preview_slides' ) );
// Notification Settings AJAX
add_action( 'wp_ajax_hssm_get_notification_settings', array( $this, 'ajax_get_notification_settings' ) );
@@ -63,9 +61,12 @@ class HSSM_Frontend {
// Frontend assets
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) );
+ add_filter( 'body_class', array( $this, 'add_tool_page_body_class' ) );
// Filter slides query on preview page
add_action( 'pre_get_posts', array( $this, 'filter_preview_slides_query' ) );
+
+ add_action( 'admin_init', array( $this, 'maybe_nest_plugin_pages' ) );
}
/**
@@ -114,6 +115,30 @@ class HSSM_Frontend {
return $template;
}
+
+ /**
+ * Prevent ?hssm_slide= from hijacking the preview page.
+ *
+ * The slide CPT used query_var "hssm_slide", so that GET param turned
+ * the page request into a missing slide query and WordPress 404'd.
+ *
+ * @since 1.1.3
+ * @param array $query_vars Parsed request vars.
+ * @return array
+ */
+ public function keep_preview_page_query( $query_vars ) {
+ if ( empty( $query_vars['pagename'] ) ) {
+ return $query_vars;
+ }
+
+ unset( $query_vars['hssm_slide'], $query_vars['hssm_slide_item'] );
+
+ if ( isset( $query_vars['name'] ) && isset( $query_vars['post_type'] ) && 'hssm_slide' === $query_vars['post_type'] ) {
+ unset( $query_vars['name'], $query_vars['post_type'] );
+ }
+
+ return $query_vars;
+ }
/**
* Handle frontend page routing
@@ -122,15 +147,39 @@ class HSSM_Frontend {
*/
public function handle_page_routing() {
global $post;
-
- // Check if this is our slide dashboard page
- if ( is_page() && get_page_template_slug( $post->ID ) === 'page-slide-dashboard.php' ) {
- // Check authentication with Members plugin
- if ( ! $this->check_dashboard_access() ) {
- $this->redirect_to_login();
- return;
- }
+
+ if ( ! is_page() || ! $post ) {
+ return;
}
+
+ $template = get_page_template_slug( $post->ID );
+ if ( ! in_array( $template, array( 'page-slide-dashboard.php', 'page-slide-preview.php' ), true ) ) {
+ return;
+ }
+
+ if ( ! $this->check_dashboard_access() ) {
+ $this->redirect_to_login();
+ }
+ }
+
+ /**
+ * Mark dashboard and preview as plugin tool pages for CSS.
+ *
+ * @since 1.1.5
+ * @param array $classes Body classes.
+ * @return array
+ */
+ public function add_tool_page_body_class( $classes ) {
+ if ( ! is_page() ) {
+ return $classes;
+ }
+
+ $template = get_page_template_slug();
+ if ( in_array( $template, array( 'page-slide-dashboard.php', 'page-slide-preview.php' ), true ) ) {
+ $classes[] = 'hssm-tool-page';
+ }
+
+ return $classes;
}
/**
@@ -182,8 +231,8 @@ class HSSM_Frontend {
* @since 1.0.0
*/
private function redirect_to_login() {
- $login_url = wp_login_url( get_permalink() );
- wp_redirect( $login_url );
+ $redirect = home_url( add_query_arg( array() ) );
+ wp_safe_redirect( wp_login_url( $redirect ) );
exit;
}
@@ -207,12 +256,13 @@ class HSSM_Frontend {
if ( ! $is_dashboard && ! $is_preview ) {
return;
}
-
- // Check access (preview page may be public, but dashboard requires auth)
- if ( $is_dashboard && ! $this->check_dashboard_access() ) {
+
+ if ( ! $this->check_dashboard_access() ) {
return;
}
-
+
+ wp_enqueue_style( 'dashicons' );
+
// Enqueue styles (both pages need dashboard CSS)
wp_enqueue_style(
'hssm-dashboard',
@@ -239,6 +289,7 @@ class HSSM_Frontend {
wp_localize_script( 'hssm-dashboard', 'hssmDashboard', array(
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'hssm_dashboard_nonce' ),
+ 'previewUrl' => hssm_get_page_url_by_template( 'page-slide-preview.php', '/slide-preview/' ),
'strings' => array(
'save_success' => __( 'Slide saved successfully!', 'hi-school-slide-manager' ),
'save_error' => __( 'Error saving slide. Please try again.', 'hi-school-slide-manager' ),
@@ -246,6 +297,9 @@ class HSSM_Frontend {
'delete_success' => __( 'Slide deleted successfully!', 'hi-school-slide-manager' ),
'required_fields' => __( 'Please fill in all required fields.', 'hi-school-slide-manager' ),
'max_forms' => __( 'Maximum 5 slide forms allowed at once.', 'hi-school-slide-manager' ),
+ 'preview_save_first' => __( 'Save the slide first, then preview it as a banner.', 'hi-school-slide-manager' ),
+ 'approve_confirm' => __( 'Approve this slide?', 'hi-school-slide-manager' ),
+ 'approve_success' => __( 'Slide approved.', 'hi-school-slide-manager' ),
),
) );
@@ -286,188 +340,6 @@ class HSSM_Frontend {
}
}
- /**
- * AJAX handler for image uploads
- *
- * @since 1.0.0
- */
- public function ajax_upload_image() {
- if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'hssm_dashboard_nonce' ) || ! current_user_can( 'upload_files' ) ) {
- wp_send_json_error( array( 'message' => 'Unauthorized' ) );
- return;
- }
-
- // This is handled by media_handle_upload in standard WP AJAX flow usually,
- // but if we need custom handling it goes here.
- // For now, assuming standard media uploader is used on frontend which might not need this
- // if using wp.media JS library directly.
- // However, if we need to process the upload specifically for this plugin:
-
- if ( ! empty( $_FILES['file'] ) ) {
- $file = $_FILES['file'];
- $attachment_id = media_handle_upload( 'file', 0 );
-
- if ( is_wp_error( $attachment_id ) ) {
- wp_send_json_error( array( 'message' => 'Upload failed: ' . $attachment_id->get_error_message() ) );
- } else {
- wp_send_json_success( array(
- 'attachment_id' => $attachment_id,
- 'url' => wp_get_attachment_url( $attachment_id )
- ) );
- }
- } else {
- wp_send_json_error( array( 'message' => 'No file provided' ) );
- }
- }
-
- /**
- * AJAX handler for previewing slides
- *
- * @since 1.0.0
- */
- public function ajax_preview_slides() {
- if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'hssm_dashboard_nonce' ) || ! current_user_can( 'read' ) ) {
- wp_send_json_error( array( 'message' => 'Unauthorized' ) );
- return;
- }
-
- $site_slug = isset( $_POST['site'] ) ? sanitize_text_field( $_POST['site'] ) : '';
- $date = isset( $_POST['date'] ) ? sanitize_text_field( $_POST['date'] ) : current_time( 'Y-m-d' );
-
- // This logic mirrors hssm_get_preview_slides_for_date in the template
- // We need to fetch slides that would be active on this date for this site
-
- $meta_query = array(
- 'relation' => 'AND',
- array(
- 'relation' => 'OR',
- array(
- 'key' => '_hssm_start_date',
- 'value' => '',
- 'compare' => '=',
- ),
- array(
- 'key' => '_hssm_start_date',
- 'value' => $date,
- 'compare' => '<=',
- ),
- array(
- 'key' => '_hssm_start_date',
- 'compare' => 'NOT EXISTS',
- ),
- ),
- array(
- 'relation' => 'OR',
- array(
- 'key' => '_hssm_end_date',
- 'value' => $date,
- 'compare' => '>=',
- ),
- array(
- 'key' => '_hssm_end_date',
- 'compare' => 'NOT EXISTS',
- ),
- array(
- 'key' => '_hssm_end_date',
- 'value' => '',
- 'compare' => '=',
- ),
- ),
- );
-
- $tax_query = array();
- if ( ! empty( $site_slug ) ) {
- $tax_query[] = array(
- 'taxonomy' => 'hssm_site',
- 'field' => 'slug',
- 'terms' => $site_slug,
- );
- }
-
- $args = array(
- 'post_type' => 'hssm_slide',
- 'post_status' => array( 'publish', 'future' ),
- 'posts_per_page' => -1,
- 'orderby' => 'meta_value_num',
- 'meta_key' => '_hssm_order',
- 'order' => 'ASC',
- 'meta_query' => $meta_query,
- 'tax_query' => $tax_query
- );
-
- $slides = get_posts( $args );
- $formatted_slides = array();
-
- foreach ( $slides as $slide ) {
- $formatted_slides[] = $this->format_slide_for_frontend( $slide->ID );
- }
-
- wp_send_json_success( array( 'slides' => $formatted_slides ) );
- }
-
- /**
- * AJAX handler for saving slides
- *
- * @since 1.0.0
- */
- public function ajax_save_slide() {
- if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'hssm_dashboard_nonce' ) || ! $this->check_dashboard_access() ) {
- wp_send_json_error( array( 'message' => 'Unauthorized' ) );
- return;
- }
-
- // Sanitize and validate input
- $slide_data = $this->sanitize_slide_data( $_POST );
-
- if ( ! $this->validate_slide_data( $slide_data ) ) {
- wp_send_json_error( array( 'message' => 'Invalid slide data' ) );
- }
-
- // Create the slide post
- $post_args = array(
- 'post_type' => 'hssm_slide',
- 'post_title' => $slide_data['title'],
- 'post_content' => $slide_data['content'],
- 'post_excerpt' => $slide_data['content'],
- 'post_status' => $slide_data['post_status'],
- 'post_author' => get_current_user_id(),
- );
-
- // If scheduling for future, set the post date
- if ( $slide_data['post_status'] === 'future' && ! empty( $slide_data['start_date'] ) ) {
- $post_args['post_date'] = $slide_data['start_date'] . ' 00:00:00';
- $post_args['post_date_gmt'] = get_gmt_from_date( $post_args['post_date'] );
- }
-
- $slide_id = wp_insert_post( $post_args );
-
- if ( is_wp_error( $slide_id ) ) {
- wp_send_json_error( array( 'message' => 'Failed to create slide' ) );
- }
-
- // Save meta data
- $this->save_slide_meta( $slide_id, $slide_data );
-
- // Save taxonomies
- $this->save_slide_taxonomies( $slide_id, $slide_data );
-
- // Set featured image if provided
- if ( ! empty( $slide_data['featured_image_id'] ) ) {
- set_post_thumbnail( $slide_id, $slide_data['featured_image_id'] );
- }
-
- wp_send_json_success( array(
- 'message' => 'Slide saved successfully!',
- 'slide_id' => $slide_id,
- 'slide_data' => $this->format_slide_for_frontend( $slide_id )
- ) );
- }
-
- /**
- * AJAX handler for image uploads
- *
- * @since 1.0.0
- */
/**
* Sanitize slide data from form
*
@@ -504,10 +376,9 @@ class HSSM_Frontend {
'content' => wp_kses_post( $data['content'] ?? '' ),
'buttons' => $this->sanitize_buttons( $buttons ),
'target_sites' => array_map( 'sanitize_text_field', $target_sites ),
- 'client' => sanitize_text_field( $data['client'] ?? '' ),
'start_date' => sanitize_text_field( $data['start_date'] ?? '' ),
'end_date' => sanitize_text_field( $data['end_date'] ?? '' ),
- 'post_status' => in_array( $data['post_status'] ?? 'draft', array( 'draft', 'publish', 'future' ) ) ? $data['post_status'] : 'draft',
+ 'post_status' => in_array( $data['post_status'] ?? 'draft', array( 'draft', 'publish', 'future' ), true ) ? ( $data['post_status'] ?? 'draft' ) : 'draft',
'auto_publish' => ! empty( $data['auto_publish'] ),
'auto_unpublish' => ! empty( $data['auto_unpublish'] ),
'order' => intval( $data['order'] ?? 0 ),
@@ -646,11 +517,6 @@ class HSSM_Frontend {
} else {
hssm_debug_log( 'No target sites to save for slide ' . $slide_id );
}
-
- // Set client
- if ( ! empty( $data['client'] ) ) {
- wp_set_post_terms( $slide_id, array( $data['client'] ), 'hssm_client' );
- }
}
/**
@@ -671,7 +537,6 @@ class HSSM_Frontend {
'buttons' => get_post_meta( $slide_id, '_hssm_buttons', true ),
'featured_image' => get_the_post_thumbnail_url( $slide_id, 'medium' ),
'target_sites' => wp_get_post_terms( $slide_id, 'hssm_site', array( 'fields' => 'slugs' ) ),
- 'client' => wp_get_post_terms( $slide_id, 'hssm_client', array( 'fields' => 'slugs' ) ),
'status' => $slide->post_status,
'created' => $slide->post_date,
);
@@ -699,8 +564,10 @@ class HSSM_Frontend {
// Get filter parameters
$status_filter = isset( $_POST['status_filter'] ) ? sanitize_text_field( $_POST['status_filter'] ) : '';
+ if ( 'scheduled' === $status_filter ) {
+ $status_filter = 'future';
+ }
$site_filter = isset( $_POST['site_filter'] ) ? sanitize_text_field( $_POST['site_filter'] ) : '';
- $client_filter = isset( $_POST['client_filter'] ) ? sanitize_text_field( $_POST['client_filter'] ) : '';
$month_filter = isset( $_POST['month_filter'] ) ? sanitize_text_field( $_POST['month_filter'] ) : ''; // Format: YYYY-MM
$year_filter = isset( $_POST['year_filter'] ) ? intval( $_POST['year_filter'] ) : 0;
@@ -734,21 +601,6 @@ class HSSM_Frontend {
}
}
- // Add client filter using taxonomy query
- if ( ! empty( $client_filter ) ) {
- // Get client term by slug or ID
- $client_term = is_numeric( $client_filter )
- ? get_term( intval( $client_filter ), 'hssm_client' )
- : get_term_by( 'slug', $client_filter, 'hssm_client' );
- if ( $client_term && ! is_wp_error( $client_term ) ) {
- $args['tax_query'][] = array(
- 'taxonomy' => 'hssm_client',
- 'field' => 'term_id',
- 'terms' => $client_term->term_id,
- );
- }
- }
-
// Add month/year filter using date query
if ( ! empty( $month_filter ) ) {
// Format: YYYY-MM
@@ -876,9 +728,11 @@ class HSSM_Frontend {
$site_terms = wp_get_post_terms( $slide->ID, 'hssm_site', array( 'fields' => 'all' ) );
$site_names = array();
+ $site_slugs = array();
if ( ! empty( $site_terms ) && ! is_wp_error( $site_terms ) ) {
foreach ( $site_terms as $site_term ) {
$site_names[] = $site_term->name;
+ $site_slugs[] = $site_term->slug;
}
}
@@ -900,8 +754,8 @@ class HSSM_Frontend {
'status' => $slide->post_status,
'status_display' => $status_display,
'sites' => $site_names,
+ 'site_slugs' => $site_slugs,
'target_sites' => $site_names,
- 'client' => $this->get_slide_client_name( $slide->ID ),
'start_date' => $this->format_dashboard_date( $start_date ),
'end_date' => $this->format_dashboard_date( $end_date ),
'auto_publish' => isset( $slide_meta['_hssm_auto_publish'][0] ) ? $slide_meta['_hssm_auto_publish'][0] : '',
@@ -965,8 +819,7 @@ class HSSM_Frontend {
return;
}
- // Check permissions
- if ( ! current_user_can( 'edit_posts' ) ) {
+ if ( ! $this->check_dashboard_access() ) {
wp_send_json_error( array( 'message' => 'Permission denied' ) );
return;
}
@@ -994,9 +847,9 @@ class HSSM_Frontend {
if ( $result ) {
hssm_debug_log( "Slide $slide_id approved with action: $approve_action" );
- // Trigger 2: Notification to Creator (Author) when approved
$notifications = new HSSM_Notifications();
$notifications->notify_creator_slide_approved( $slide_id );
+ $notifications->notify_itc_slide_approved( $slide_id );
wp_send_json_success( array(
'message' => 'Slide approved successfully',
@@ -1301,21 +1154,6 @@ class HSSM_Frontend {
return implode( ', ', $site_names );
}
-
- /**
- * Get client name for a slide from taxonomy
- *
- * @since 1.0.0
- * @param int $slide_id Slide ID
- * @return string Client name or empty string
- */
- private function get_slide_client_name( $slide_id ) {
- $client_terms = wp_get_post_terms( $slide_id, 'hssm_client', array( 'fields' => 'all' ) );
- if ( ! empty( $client_terms ) && ! is_wp_error( $client_terms ) ) {
- return $client_terms[0]->name;
- }
- return '';
- }
/**
* Get available sites for targeting
@@ -1355,6 +1193,112 @@ class HSSM_Frontend {
)
);
}
+
+ /**
+ * Nest preview and confirmation pages under the slide dashboard.
+ *
+ * @since 1.1.3
+ */
+ public function maybe_nest_plugin_pages() {
+ if ( ! current_user_can( 'manage_options' ) ) {
+ return;
+ }
+
+ $dashboard_query = new WP_Query(
+ array(
+ 'post_type' => 'page',
+ 'post_status' => array( 'publish', 'private' ),
+ 'posts_per_page' => 1,
+ 'no_found_rows' => true,
+ 'meta_key' => '_wp_page_template',
+ 'meta_value' => 'page-slide-dashboard.php',
+ )
+ );
+
+ if ( ! $dashboard_query->have_posts() ) {
+ wp_reset_postdata();
+ return;
+ }
+
+ $parent_id = (int) $dashboard_query->posts[0]->ID;
+ wp_reset_postdata();
+
+ $this->maybe_create_confirmation_page( $parent_id );
+
+ $child_templates = array( 'page-slide-preview.php', 'page-slide-confirmation.php' );
+
+ foreach ( $child_templates as $template ) {
+ $pages_query = new WP_Query(
+ array(
+ 'post_type' => 'page',
+ 'post_status' => array( 'publish', 'private' ),
+ 'posts_per_page' => 20,
+ 'no_found_rows' => true,
+ 'meta_key' => '_wp_page_template',
+ 'meta_value' => $template,
+ )
+ );
+
+ if ( ! $pages_query->have_posts() ) {
+ wp_reset_postdata();
+ continue;
+ }
+
+ foreach ( $pages_query->posts as $page ) {
+ if ( (int) $page->ID === $parent_id ) {
+ continue;
+ }
+
+ if ( (int) $page->post_parent !== $parent_id ) {
+ wp_update_post(
+ array(
+ 'ID' => $page->ID,
+ 'post_parent' => $parent_id,
+ )
+ );
+ }
+ }
+ wp_reset_postdata();
+ }
+ }
+
+ /**
+ * Create the ITC confirmation page when it is missing.
+ *
+ * @since 1.1.3
+ * @param int $parent_id Dashboard page ID.
+ */
+ private function maybe_create_confirmation_page( $parent_id ) {
+ $existing = new WP_Query(
+ array(
+ 'post_type' => 'page',
+ 'post_status' => array( 'publish', 'private' ),
+ 'posts_per_page' => 1,
+ 'no_found_rows' => true,
+ 'meta_key' => '_wp_page_template',
+ 'meta_value' => 'page-slide-confirmation.php',
+ )
+ );
+
+ if ( $existing->have_posts() ) {
+ wp_reset_postdata();
+ return;
+ }
+ wp_reset_postdata();
+
+ wp_insert_post(
+ array(
+ 'post_title' => 'Slide Confirmation',
+ 'post_name' => 'slide-confirmation',
+ 'post_status' => 'publish',
+ 'post_type' => 'page',
+ 'post_parent' => absint( $parent_id ),
+ 'meta_input' => array(
+ '_wp_page_template' => 'page-slide-confirmation.php',
+ ),
+ )
+ );
+ }
/**
* AJAX handler for duplicating slides
@@ -1439,14 +1383,6 @@ class HSSM_Frontend {
if ( is_wp_error( $updated ) ) {
wp_send_json_error( array( 'message' => 'Failed to update slide status' ) );
}
-
- // Trigger notification if published via toggle?
- // Logic check: If toggled to publish, it means it's Live.
- if ( $new_status === 'publish' ) {
- // Trigger 3: Notification to ITCGuys when slide goes Live (via manual toggle)
- $notifications = new HSSM_Notifications();
- $notifications->notify_itc_slide_live( $slide_id );
- }
wp_send_json_success( array(
'message' => 'Slide status updated successfully',
diff --git a/includes/class-hssm-main.php b/includes/class-hssm-main.php
index 385610b..b9cae36 100644
--- a/includes/class-hssm-main.php
+++ b/includes/class-hssm-main.php
@@ -177,6 +177,10 @@ class HSSM_Main {
// Cron hooks for scheduled slides
add_action( 'hssm_check_scheduled_slides', array( $this, 'check_scheduled_slides' ) );
+
+ if ( 'manager' === $plugin_mode && $this->email_notifications ) {
+ add_action( 'transition_post_status', array( $this->email_notifications, 'on_slide_status_change' ), 10, 3 );
+ }
}
/**
@@ -232,7 +236,7 @@ class HSSM_Main {
*/
public function enqueue_frontend_assets() {
// Prevent slider assets from loading on admin-facing "frontend" pages
- if ( is_page_template('page-slide-dashboard.php') || is_page_template('page-slide-preview.php') ) {
+ if ( is_page_template('page-slide-dashboard.php') || is_page_template('page-slide-preview.php') || is_page_template('page-slide-confirmation.php') ) {
// These pages will load their own specific styles (admin-slides-dashboard.css) or rely on explicit shortcode enqueueing.
return;
}
@@ -312,23 +316,28 @@ class HSSM_Main {
* @return array List of companion sites
*/
public static function get_companion_sites() {
- return array(
+ $sites = array(
array(
- 'slug' => 'hi-school',
- 'name' => 'Hi-School Pharmacy',
- 'description' => 'Hi-School Pharmacy companion site'
+ 'slug' => 'hi-school',
+ 'name' => 'Hi-School Pharmacy',
+ 'description' => 'Hi-School Pharmacy companion site',
+ 'url' => '',
),
array(
- 'slug' => 'one-stop',
- 'name' => 'One Stop Hardware',
- 'description' => 'One Stop Hardware companion site'
+ 'slug' => 'one-stop',
+ 'name' => 'One Stop Hardware',
+ 'description' => 'One Stop Hardware companion site',
+ 'url' => '',
),
array(
- 'slug' => 'ace',
- 'name' => 'Ace Hardware',
- 'description' => 'Ace Hardware companion site'
+ 'slug' => 'ace',
+ 'name' => 'Ace Hardware',
+ 'description' => 'Ace Hardware companion site',
+ 'url' => '',
)
);
+
+ return apply_filters( 'hssm_companion_sites', $sites );
}
/**
@@ -447,7 +456,10 @@ class HSSM_Main {
'hssm_plugin_mode' => 'manager',
'hssm_api_url' => '',
'hssm_client_site_slug' => '',
- 'hssm_notification_emails' => array( get_option( 'admin_email' ) ),
+ 'hssm_notification_settings' => array(
+ 'editor_emails' => get_option( 'admin_email' ),
+ 'itc_emails' => '',
+ ),
'hssm_default_slide_duration' => 30, // days
'hssm_cache_duration' => 3600, // 1 hour
'hssm_api_version' => 'v1',
diff --git a/includes/class-hssm-notifications.php b/includes/class-hssm-notifications.php
index 0f16501..ed0dd8c 100644
--- a/includes/class-hssm-notifications.php
+++ b/includes/class-hssm-notifications.php
@@ -35,17 +35,42 @@ class HSSM_Notifications {
public static function get_settings() {
$defaults = array(
'editor_emails' => '',
- 'itc_emails' => ''
+ 'itc_emails' => '',
);
-
- $settings = get_option( 'hssm_notification_settings', $defaults );
-
- // Ensure we have an array and it contains our expected keys
+
+ $settings = get_option( 'hssm_notification_settings', false );
if ( ! is_array( $settings ) ) {
- return $defaults;
+ $settings = $defaults;
+ } else {
+ $settings = wp_parse_args( $settings, $defaults );
}
-
- return wp_parse_args( $settings, $defaults );
+
+ $has_current = '' !== trim( (string) $settings['editor_emails'] ) || '' !== trim( (string) $settings['itc_emails'] );
+ if ( $has_current ) {
+ return $settings;
+ }
+
+ $legacy = get_option( 'hssm_notification_emails', array() );
+ if ( empty( $legacy ) ) {
+ return $settings;
+ }
+
+ if ( is_array( $legacy ) ) {
+ $legacy_emails = array_filter( array_map( 'sanitize_email', $legacy ) );
+ $legacy_string = implode( ',', $legacy_emails );
+ } else {
+ $legacy_string = sanitize_text_field( (string) $legacy );
+ }
+
+ if ( '' === $legacy_string ) {
+ return $settings;
+ }
+
+ $settings['editor_emails'] = $legacy_string;
+ update_option( 'hssm_notification_settings', $settings );
+ delete_option( 'hssm_notification_emails' );
+
+ return $settings;
}
/**
@@ -89,6 +114,215 @@ class HSSM_Notifications {
return array_unique( $clean_emails );
}
+ /**
+ * Confirmation token for ITC live emails (does not depend on a logged-in session).
+ *
+ * @since 1.1.3
+ * @param int $slide_id Slide ID.
+ * @return string
+ */
+ public static function get_confirm_token( $slide_id ) {
+ return hash( 'sha256', absint( $slide_id ) . '|' . wp_salt( 'auth' ) );
+ }
+
+ /**
+ * Whether a confirmation token is valid.
+ *
+ * @since 1.1.3
+ * @param int $slide_id Slide ID.
+ * @param string $token Token from the email link.
+ * @return bool
+ */
+ public static function is_valid_confirm_token( $slide_id, $token ) {
+ return is_string( $token ) && hash_equals( self::get_confirm_token( $slide_id ), $token );
+ }
+
+ /**
+ * Preview page URL for a slide.
+ *
+ * @since 1.1.3
+ * @param int $slide_id Slide ID.
+ * @return string
+ */
+ private function get_preview_link( $slide_id ) {
+ $start_date = get_post_meta( $slide_id, '_hssm_start_date', true );
+ if ( $start_date && preg_match( '/^(\d{4}-\d{2}-\d{2})/', $start_date, $matches ) ) {
+ $date = $matches[1];
+ } else {
+ $date = current_time( 'Y-m-d' );
+ }
+
+ $sites = wp_get_post_terms( $slide_id, 'hssm_site', array( 'fields' => 'slugs' ) );
+ $first_site = ( ! is_wp_error( $sites ) && ! empty( $sites ) ) ? $sites[0] : '';
+
+ return add_query_arg(
+ array(
+ 'hssm_date' => $date,
+ 'hssm_site' => $first_site,
+ 'hssm_preview' => $slide_id,
+ ),
+ hssm_get_page_url_by_template( 'page-slide-preview.php', '/slide-preview/' )
+ );
+ }
+
+ /**
+ * ITC confirmation page URL.
+ *
+ * @since 1.1.3
+ * @param int $slide_id Slide ID.
+ * @return string
+ */
+ private function get_confirmation_link( $slide_id ) {
+ return add_query_arg(
+ array(
+ 'slide_id' => $slide_id,
+ 'hssm_token' => self::get_confirm_token( $slide_id ),
+ ),
+ hssm_get_page_url_by_template( 'page-slide-confirmation.php', '/slide-confirmation/' )
+ );
+ }
+
+ /**
+ * Front dashboard URL.
+ *
+ * @since 1.1.3
+ * @return string
+ */
+ private function get_dashboard_link( $slide_id = 0 ) {
+ $url = hssm_get_page_url_by_template( 'page-slide-dashboard.php', '/slide-dashboard/' );
+ if ( $slide_id ) {
+ $url = add_query_arg( 'hssm_focus', absint( $slide_id ), $url );
+ }
+ return $url;
+ }
+
+ /**
+ * Whether the slide is currently visible on companion sites.
+ *
+ * @since 1.1.3
+ * @param int $slide_id Slide ID.
+ * @return bool
+ */
+ private function is_slide_currently_live( $slide_id ) {
+ $post = get_post( $slide_id );
+ if ( ! $post || 'publish' !== $post->post_status ) {
+ return false;
+ }
+
+ $now = current_time( 'timestamp' );
+ $start = get_post_meta( $slide_id, '_hssm_start_date', true );
+ if ( $start ) {
+ $start_ts = strtotime( $start );
+ if ( false !== $start_ts && $start_ts > $now ) {
+ return false;
+ }
+ }
+
+ $end = get_post_meta( $slide_id, '_hssm_end_date', true );
+ if ( $end ) {
+ $end_ts = strtotime( $end );
+ if ( false !== $end_ts && $end_ts < $now ) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Human-readable site list, with URLs when configured.
+ *
+ * @since 1.1.3
+ * @param int $slide_id Slide ID.
+ * @return string
+ */
+ private function format_site_lines( $slide_id ) {
+ $terms = wp_get_post_terms( $slide_id, 'hssm_site' );
+ if ( is_wp_error( $terms ) || empty( $terms ) ) {
+ return 'None';
+ }
+
+ $catalog = array();
+ if ( class_exists( 'HSSM_Main' ) ) {
+ foreach ( HSSM_Main::get_companion_sites() as $site ) {
+ $catalog[ $site['slug'] ] = $site;
+ }
+ }
+
+ $lines = array();
+ foreach ( $terms as $term ) {
+ $line = $term->name . ' (' . $term->slug . ')';
+ if ( ! empty( $catalog[ $term->slug ]['url'] ) ) {
+ $line .= ' - ' . $catalog[ $term->slug ]['url'];
+ }
+ $lines[] = $line;
+ }
+
+ return implode( "\n", $lines );
+ }
+
+ /**
+ * Mark a one-time notification as sent.
+ *
+ * @since 1.1.3
+ * @param int $slide_id Slide ID.
+ * @param string $key Meta key.
+ * @return bool True if this is the first send.
+ */
+ private function mark_sent_once( $slide_id, $key ) {
+ if ( get_post_meta( $slide_id, $key, true ) ) {
+ return false;
+ }
+
+ update_post_meta( $slide_id, $key, current_time( 'mysql' ) );
+ return true;
+ }
+
+ /**
+ * Send ITC the go-live email when a slide first reaches publish.
+ *
+ * @since 1.1.3
+ * @param string $new_status New status.
+ * @param string $old_status Previous status.
+ * @param WP_Post $post Post object.
+ */
+ public function on_slide_status_change( $new_status, $old_status, $post ) {
+ if ( ! $post instanceof WP_Post || 'hssm_slide' !== $post->post_type ) {
+ return;
+ }
+
+ if ( 'publish' !== $new_status || 'publish' === $old_status ) {
+ return;
+ }
+
+ $this->notify_itc_slide_live( $post->ID );
+ }
+
+ /**
+ * Notify ITC of already-published slides whose start date just arrived.
+ *
+ * @since 1.1.3
+ */
+ public function notify_due_live_slides() {
+ $slides = get_posts(
+ array(
+ 'post_type' => 'hssm_slide',
+ 'post_status' => 'publish',
+ 'posts_per_page' => 50,
+ 'meta_query' => array(
+ array(
+ 'key' => '_hssm_itc_live_sent',
+ 'compare' => 'NOT EXISTS',
+ ),
+ ),
+ )
+ );
+
+ foreach ( $slides as $slide ) {
+ $this->notify_itc_slide_live( $slide->ID );
+ }
+ }
+
/**
* Send notification to Editor when slide is Scheduled or Published Immediately
*
@@ -105,47 +339,23 @@ class HSSM_Notifications {
}
$slide = get_post( $slide_id );
- if ( ! $slide ) return;
-
- $target_sites = wp_get_post_terms( $slide_id, 'hssm_site', array( 'fields' => 'slugs' ) );
- $site_slugs = ! empty( $target_sites ) ? implode( ',', $target_sites ) : '';
-
- // Determine publish date
- $publish_date = ( $status === 'scheduled' )
- ? get_post_meta( $slide_id, '_hssm_start_date', true )
- : current_time( 'Y-m-d' );
-
- // Construct preview link
- // page-slide-preview.php logic usually takes 'date' and 'site' params
- // We'll link to the preview page. Assuming there is a page with the template, or we use a custom URL structure if defined.
- // The instructions mentioned: "generated link to the slide-preview page with params containing the slide-title, the publish-date... and the site slugs"
-
- $preview_page_url = home_url( '/slide-preview/' ); // Basic assumption, adjust if needed based on setup
- // Better: find the page using the template
- $pages = get_pages(array(
- 'meta_key' => '_wp_page_template',
- 'meta_value' => 'page-slide-preview.php'
- ));
-
- if ( ! empty( $pages ) ) {
- $preview_page_url = get_permalink( $pages[0]->ID );
+ if ( ! $slide ) {
+ return;
}
- $preview_link = add_query_arg( array(
- 'preview_title' => urlencode( $slide->post_title ),
- 'date' => $publish_date,
- 'sites' => $site_slugs,
- 'preview_mode' => 'editor_check'
- ), $preview_page_url );
+ $start_date = get_post_meta( $slide_id, '_hssm_start_date', true );
+ $publish_date = $start_date ? $start_date : current_time( 'Y-m-d' );
+ $status_label = ( 'scheduled' === $status ) ? 'Scheduled' : 'Published Immediately';
- $subject = sprintf( 'New Slide %s: "%s"', ucfirst( $status ), $slide->post_title );
-
- $message = "A new slide has been " . ( $status === 'scheduled' ? 'Scheduled' : 'Published Immediately' ) . ".\n\n";
- $message .= "Title: " . $slide->post_title . "\n";
- $message .= "Target Date: " . $publish_date . "\n";
- $message .= "Target Sites: " . implode( ', ', $target_sites ) . "\n\n";
- $message .= "Preview Link: " . $preview_link . "\n\n";
- $message .= "Please review the slide.";
+ $subject = sprintf( 'New Slide %s: "%s"', $status_label, $slide->post_title );
+
+ $message = "A new slide has been {$status_label}.\n\n";
+ $message .= 'Title: ' . $slide->post_title . "\n";
+ $message .= 'Target Date: ' . $publish_date . "\n";
+ $message .= "Target Sites:\n" . $this->format_site_lines( $slide_id ) . "\n\n";
+ $message .= 'Preview: ' . $this->get_preview_link( $slide_id ) . "\n";
+ $message .= 'Dashboard: ' . $this->get_dashboard_link( $slide_id ) . "\n\n";
+ $message .= 'Please review the slide.';
foreach ( $emails as $email ) {
wp_mail( $email, $subject, $message );
@@ -159,80 +369,102 @@ class HSSM_Notifications {
*/
public function notify_creator_slide_approved( $slide_id ) {
$slide = get_post( $slide_id );
- if ( ! $slide ) return;
+ if ( ! $slide ) {
+ return;
+ }
$author_email = get_the_author_meta( 'user_email', $slide->post_author );
-
- if ( ! is_email( $author_email ) ) return;
+ if ( ! is_email( $author_email ) ) {
+ return;
+ }
$subject = 'Your Slide Has Been Approved: "' . $slide->post_title . '"';
-
- $message = "Good news! Your slide has been approved.\n\n";
- $message .= "Title: " . $slide->post_title . "\n";
- $message .= "It is now scheduled/published according to your settings.";
+
+ $message = "Good news! Your slide has been approved.\n\n";
+ $message .= 'Title: ' . $slide->post_title . "\n";
+ $message .= "Target Sites:\n" . $this->format_site_lines( $slide_id ) . "\n\n";
+ $message .= 'Preview: ' . $this->get_preview_link( $slide_id ) . "\n";
+ $message .= 'Dashboard: ' . $this->get_dashboard_link( $slide_id ) . "\n\n";
+ $message .= 'It is now scheduled or published according to your settings.';
wp_mail( $author_email, $subject, $message );
}
+ /**
+ * Notify ITC to review date range after approval.
+ *
+ * @since 1.1.3
+ * @param int $slide_id Slide ID.
+ */
+ public function notify_itc_slide_approved( $slide_id ) {
+ if ( ! $this->mark_sent_once( $slide_id, '_hssm_itc_approved_sent' ) ) {
+ return;
+ }
+
+ $settings = self::get_settings();
+ $emails = $this->get_emails_array( $settings['itc_emails'] );
+ if ( empty( $emails ) ) {
+ hssm_debug_log( 'No ITC emails configured for approval notification.' );
+ return;
+ }
+
+ $slide = get_post( $slide_id );
+ if ( ! $slide ) {
+ return;
+ }
+
+ $start_date = get_post_meta( $slide_id, '_hssm_start_date', true );
+ $end_date = get_post_meta( $slide_id, '_hssm_end_date', true );
+
+ $subject = 'Slide Approved for Review: "' . $slide->post_title . '"';
+
+ $message = "A slide has been approved. Please review the date range and preview before it goes live.\n\n";
+ $message .= 'Title: ' . $slide->post_title . "\n";
+ $message .= 'Start Date: ' . ( $start_date ? $start_date : 'Immediate' ) . "\n";
+ $message .= 'End Date: ' . ( $end_date ? $end_date : 'None' ) . "\n";
+ $message .= "Target Sites:\n" . $this->format_site_lines( $slide_id ) . "\n\n";
+ $message .= 'Preview: ' . $this->get_preview_link( $slide_id ) . "\n";
+ $message .= 'Dashboard: ' . $this->get_dashboard_link( $slide_id );
+
+ foreach ( $emails as $email ) {
+ wp_mail( $email, $subject, $message );
+ }
+ }
+
/**
* Send notification to ITCGuys when slide goes LIVE
*
* @param int $slide_id Slide ID
*/
public function notify_itc_slide_live( $slide_id ) {
+ if ( ! $this->is_slide_currently_live( $slide_id ) ) {
+ return;
+ }
+
+ if ( ! $this->mark_sent_once( $slide_id, '_hssm_itc_live_sent' ) ) {
+ return;
+ }
+
$settings = self::get_settings();
- $emails = $this->get_emails_array( $settings['itc_emails'] );
-
- if ( empty( $emails ) ) return;
+ $emails = $this->get_emails_array( $settings['itc_emails'] );
+ if ( empty( $emails ) ) {
+ hssm_debug_log( 'No ITC emails configured for live notification.' );
+ return;
+ }
$slide = get_post( $slide_id );
- if ( ! $slide ) return;
+ if ( ! $slide ) {
+ return;
+ }
- // Generate Confirmation Link
- // This link needs to trigger the "Slide is Live" confirmation page
- // We probably need a specialized page or endpoint for this.
- // Let's assume we use the Dashboard page with a specific action param or a separate page.
- // User requested: "We need a page 'Slide is Live' ... with a green confirmation button"
- // Let's use the preview page or dashboard page with a specific query arg to show this UI.
- // Or better, a dedicated endpoint. For now, let's point to the dashboard with a "confirm_live" param.
-
- $confirm_url = home_url( '/slide-live-confirmation/' ); // Ideally we create this page or rewrite rule
- // For now, let's use the dashboard url with arguments, and we'll handle the UI there or create a rewrite.
- // Actually, creating a specific page 'Slide is Live' was requested.
- // I'll create a new template `page-slide-confirmation.php` later.
-
- // Find confirmation page if exists, or construct URL
- $pages = get_pages(array(
- 'meta_key' => '_wp_page_template',
- 'meta_value' => 'page-slide-confirmation.php'
- ));
- $confirmation_page_url = !empty($pages) ? get_permalink($pages[0]->ID) : home_url('/?hssm_action=confirm_live');
-
- $confirm_link = add_query_arg( array(
- 'slide_id' => $slide_id,
- 'token' => wp_create_nonce( 'hssm_confirm_live_' . $slide_id ) // Note: Nonces depend on user session, might be tricky for email links if user isn't logged in.
- // Better to use a hash that doesn't expire or relies on a shared secret if ITC guys are not logged in.
- // Assuming ITC guys are logged in users for now or we just use a hash.
- ), $confirmation_page_url );
-
- // Since email links are clicked outside sessions often, maybe just a hash of slide ID + secret salt.
- $token = hash( 'sha256', $slide_id . 'hssm_secret_salt' );
- $confirm_link = add_query_arg( array(
- 'slide_id' => $slide_id,
- 'hssm_token' => $token
- ), $confirmation_page_url );
-
-
- $target_sites = wp_get_post_terms( $slide_id, 'hssm_site', array( 'fields' => 'slugs' ) );
-
$subject = 'ACTION REQUIRED: Slide is LIVE - "' . $slide->post_title . '"';
-
- $message = "A slide has just gone LIVE.\n\n";
- $message .= "Title: " . $slide->post_title . "\n";
- $message .= "Sites: " . implode( ', ', $target_sites ) . "\n\n";
+
+ $message = "A slide has just gone LIVE.\n\n";
+ $message .= 'Title: ' . $slide->post_title . "\n";
+ $message .= "Sites:\n" . $this->format_site_lines( $slide_id ) . "\n\n";
$message .= "Please check the sites to ensure it is displaying correctly.\n";
- $message .= "Once verified, click the link below to confirm to the Editor and Author:\n\n";
- $message .= $confirm_link;
+ $message .= "Once verified, click the confirmation link to notify the Editor and Author:\n\n";
+ $message .= $this->get_confirmation_link( $slide_id );
foreach ( $emails as $email ) {
wp_mail( $email, $subject, $message );
@@ -243,32 +475,43 @@ class HSSM_Notifications {
* Send FINAL notification to Editor and Author (triggered by ITC confirmation)
*
* @param int $slide_id Slide ID
+ * @return bool True if the confirmation email was sent.
*/
public function notify_all_confirmed_live( $slide_id ) {
$slide = get_post( $slide_id );
- if ( ! $slide ) return;
+ if ( ! $slide ) {
+ return false;
+ }
- // Editor Emails
- $settings = self::get_settings();
+ if ( ! $this->mark_sent_once( $slide_id, '_hssm_live_confirmed' ) ) {
+ return false;
+ }
+
+ $settings = self::get_settings();
$editor_emails = $this->get_emails_array( $settings['editor_emails'] );
-
- // Author Email
- $author_email = get_the_author_meta( 'user_email', $slide->post_author );
-
- $recipients = array_merge( $editor_emails, array( $author_email ) );
- $recipients = array_unique( array_filter( $recipients ) );
+ $author_email = get_the_author_meta( 'user_email', $slide->post_author );
+ $recipients = array_unique( array_filter( array_merge( $editor_emails, array( $author_email ) ) ) );
- if ( empty( $recipients ) ) return;
+ if ( empty( $recipients ) ) {
+ hssm_debug_log( 'No recipients for live confirmation email.' );
+ return true;
+ }
$subject = 'CONFIRMED: Slide is Live and Verified - "' . $slide->post_title . '"';
-
- $message = "The slide has been verified as LIVE by the ITC team.\n\n";
- $message .= "Title: " . $slide->post_title . "\n";
- $message .= "Status: Live & Verified\n\n";
- $message .= "You can view the slide on the live sites.";
+
+ $message = "The slide has been verified as LIVE by the ITC team.\n\n";
+ $message .= 'Title: ' . $slide->post_title . "\n";
+ $message .= "Status: Live & Verified\n";
+ $message .= "Sites:\n" . $this->format_site_lines( $slide_id ) . "\n\n";
+ $message .= 'Dashboard: ' . $this->get_dashboard_link( $slide_id ) . "\n";
+ $message .= 'Preview: ' . $this->get_preview_link( $slide_id );
foreach ( $recipients as $email ) {
- wp_mail( $email, $subject, $message );
+ if ( is_email( $email ) ) {
+ wp_mail( $email, $subject, $message );
+ }
}
+
+ return true;
}
}
diff --git a/includes/class-hssm-rest-api.php b/includes/class-hssm-rest-api.php
index a6251fa..206a5a6 100644
--- a/includes/class-hssm-rest-api.php
+++ b/includes/class-hssm-rest-api.php
@@ -360,7 +360,6 @@ class HSSM_REST_API {
'auto_unpublish' => get_post_meta( $slide->ID, '_hssm_auto_unpublish', true ) === '1',
),
'target_sites' => $this->get_slide_target_sites( $slide->ID ),
- 'client' => $this->get_slide_client( $slide->ID ),
'status' => $slide->post_status,
'created' => $slide->post_date,
'modified' => $slide->post_modified,
@@ -427,27 +426,6 @@ class HSSM_REST_API {
return $sites_data;
}
- /**
- * Get slide client
- *
- * @since 1.0.0
- * @param int $slide_id Slide ID
- * @return array|null Client data
- */
- private function get_slide_client( $slide_id ) {
- $clients = wp_get_post_terms( $slide_id, 'hssm_client' );
-
- if ( ! is_wp_error( $clients ) && ! empty( $clients ) ) {
- $client = $clients[0];
- return array(
- 'slug' => $client->slug,
- 'name' => $client->name,
- );
- }
-
- return null;
- }
-
/**
* Get slide count for a site
*
diff --git a/includes/class-hssm-shortcode.php b/includes/class-hssm-shortcode.php
index 9cc4a38..57364e8 100644
--- a/includes/class-hssm-shortcode.php
+++ b/includes/class-hssm-shortcode.php
@@ -22,6 +22,7 @@ class HSSM_Shortcode {
*/
public function __construct() {
add_shortcode( 'hssm_slider', array( $this, 'render_shortcode' ) );
+ add_shortcode( 'hssm_slides', array( $this, 'render_shortcode' ) );
}
/**
@@ -49,14 +50,37 @@ class HSSM_Shortcode {
$slides = $result;
} else {
// Manager mode - Fetch locally
- $site_slug = isset( $atts['site'] ) ? sanitize_text_field( $atts['site'] ) : get_option( 'hssm_client_site_slug', 'hi-school' );
- $date = isset( $atts['date'] ) ? sanitize_text_field( $atts['date'] ) : current_time( 'Y-m-d' );
+ if ( isset( $atts['count'] ) && ! isset( $atts['limit'] ) ) {
+ $atts['limit'] = $atts['count'];
+ }
+
+ $atts = shortcode_atts(
+ array(
+ 'site' => get_option( 'hssm_client_site_slug', 'hi-school' ),
+ 'date' => current_time( 'Y-m-d' ),
+ 'limit' => 10,
+ 'slide' => 0,
+ 'include_pending' => '0',
+ ),
+ $atts,
+ 'hssm_slider'
+ );
+
+ $site_slug = sanitize_text_field( $atts['site'] );
+ $date = sanitize_text_field( $atts['date'] );
+ $focus_slide_id = absint( $atts['slide'] );
+ $include_pending = filter_var( $atts['include_pending'], FILTER_VALIDATE_BOOLEAN );
+
+ $post_status = array( 'publish', 'future' );
+ if ( $include_pending ) {
+ $post_status[] = 'pending';
+ }
// Build query args similar to what's in the REST API or Frontend class
$args = array(
'post_type' => 'hssm_slide',
- 'posts_per_page' => isset( $atts['limit'] ) ? intval( $atts['limit'] ) : 10,
- 'post_status' => array( 'publish', 'future', 'pending' ),
+ 'posts_per_page' => intval( $atts['limit'] ),
+ 'post_status' => $post_status,
'meta_key' => '_hssm_order',
'orderby' => 'meta_value_num',
'order' => 'ASC',
@@ -94,21 +118,14 @@ class HSSM_Shortcode {
if ( $query->have_posts() ) {
while ( $query->have_posts() ) {
$query->the_post();
- $slide_id = get_the_ID();
-
- // Format data to match what the API returns
- $slides[] = array(
- 'id' => $slide_id,
- 'title' => get_the_title(),
- 'subtitle' => get_post_meta( $slide_id, '_hssm_subtitle', true ),
- 'content' => get_the_content(),
- 'buttons' => get_post_meta( $slide_id, '_hssm_buttons', true ),
- 'featured_image' => get_the_post_thumbnail_url( $slide_id, 'full' ),
- 'hide_all_text' => get_post_meta( $slide_id, '_hssm_hide_all_text', true ) === '1',
- );
+ $slides[] = $this->format_slide_for_slider( get_the_ID() );
}
wp_reset_postdata();
}
+
+ if ( $focus_slide_id ) {
+ $slides = $this->prepend_focus_slide( $slides, $focus_slide_id, $include_pending );
+ }
}
if ( empty( $slides ) ) {
@@ -118,4 +135,77 @@ class HSSM_Shortcode {
$slider = new HSSM_Slider();
return $slider->render( $slides, $atts );
}
+
+ /**
+ * Format a slide for the public slider.
+ *
+ * @since 1.1.3
+ * @param int $slide_id Slide ID.
+ * @return array|null
+ */
+ private function format_slide_for_slider( $slide_id ) {
+ $slide = get_post( $slide_id );
+ if ( ! $slide || 'hssm_slide' !== $slide->post_type ) {
+ return null;
+ }
+
+ return array(
+ 'id' => $slide->ID,
+ 'title' => $slide->post_title,
+ 'subtitle' => get_post_meta( $slide->ID, '_hssm_subtitle', true ),
+ 'content' => $slide->post_content,
+ 'buttons' => get_post_meta( $slide->ID, '_hssm_buttons', true ),
+ 'featured_image' => get_the_post_thumbnail_url( $slide->ID, 'full' ),
+ 'hide_all_text' => get_post_meta( $slide->ID, '_hssm_hide_all_text', true ) === '1',
+ );
+ }
+
+ /**
+ * Put a specific slide first in the slider.
+ *
+ * Pending and draft slides are only prepended when include_pending is true
+ * (preview). The public slider stays publish + future.
+ *
+ * @since 1.1.3
+ * @param array $slides Existing slides.
+ * @param int $slide_id Focus slide ID.
+ * @param bool $include_pending Whether pending/draft focus slides are allowed.
+ * @return array
+ */
+ private function prepend_focus_slide( $slides, $slide_id, $include_pending = false ) {
+ foreach ( $slides as $index => $slide ) {
+ if ( (int) $slide['id'] === (int) $slide_id ) {
+ if ( 0 !== $index ) {
+ $focus = $slides[ $index ];
+ unset( $slides[ $index ] );
+ array_unshift( $slides, $focus );
+ }
+ return array_values( $slides );
+ }
+ }
+
+ $post = get_post( $slide_id );
+ if ( ! $post || 'hssm_slide' !== $post->post_type ) {
+ return $slides;
+ }
+
+ $allowed_statuses = array( 'publish', 'future' );
+ if ( $include_pending ) {
+ $allowed_statuses[] = 'pending';
+ if ( current_user_can( 'edit_post', $slide_id ) ) {
+ $allowed_statuses[] = 'draft';
+ }
+ }
+
+ if ( ! in_array( $post->post_status, $allowed_statuses, true ) ) {
+ return $slides;
+ }
+
+ $formatted = $this->format_slide_for_slider( $slide_id );
+ if ( $formatted ) {
+ array_unshift( $slides, $formatted );
+ }
+
+ return $slides;
+ }
}
diff --git a/includes/class-hssm-slider.php b/includes/class-hssm-slider.php
index 011a964..3cb091a 100644
--- a/includes/class-hssm-slider.php
+++ b/includes/class-hssm-slider.php
@@ -51,9 +51,25 @@ class HSSM_Slider {
}
$hide_text = isset( $slide['hide_all_text'] ) && $slide['hide_all_text'];
+ $valid_buttons = array();
+ if ( ! empty( $slide['buttons'] ) && is_array( $slide['buttons'] ) ) {
+ $valid_buttons = array_filter(
+ $slide['buttons'],
+ function( $button ) {
+ return ! empty( $button['text'] ) && ! empty( $button['url'] );
+ }
+ );
+ }
+ $has_text = ! $hide_text && (
+ ! empty( $slide['title'] ) ||
+ ! empty( $slide['subtitle'] ) ||
+ ! empty( $slide['content'] ) ||
+ ! empty( $valid_buttons )
+ );
?>
+
@@ -70,25 +86,18 @@ class HSSM_Slider {
-
-
-
-
-
+
+
+
diff --git a/includes/class-hssm-slides-cpt.php b/includes/class-hssm-slides-cpt.php
index 85b06e0..222530b 100644
--- a/includes/class-hssm-slides-cpt.php
+++ b/includes/class-hssm-slides-cpt.php
@@ -36,14 +36,6 @@ class HSSM_Slides_CPT {
*/
const SITE_TAXONOMY = 'hssm_site';
- /**
- * Client taxonomy slug
- *
- * @since 1.0.0
- * @var string
- */
- const CLIENT_TAXONOMY = 'hssm_client';
-
/**
* Constructor
*
@@ -99,7 +91,7 @@ class HSSM_Slides_CPT {
'publicly_queryable' => true,
'show_ui' => true,
'show_in_menu' => true,
- 'query_var' => true,
+ 'query_var' => 'hssm_slide_item',
'rewrite' => array( 'slug' => 'slide' ),
'capability_type' => 'post',
'has_archive' => true,
@@ -147,33 +139,6 @@ class HSSM_Slides_CPT {
register_taxonomy( self::SITE_TAXONOMY, array( self::POST_TYPE ), $site_args );
- // Client taxonomy for organizing slides by client (optional)
- $client_labels = array(
- 'name' => _x( 'Clients', 'taxonomy general name', 'hi-school-slide-manager' ),
- 'singular_name' => _x( 'Client', 'taxonomy singular name', 'hi-school-slide-manager' ),
- 'search_items' => __( 'Search Clients', 'hi-school-slide-manager' ),
- 'all_items' => __( 'All Clients', 'hi-school-slide-manager' ),
- 'parent_item' => __( 'Parent Client', 'hi-school-slide-manager' ),
- 'parent_item_colon' => __( 'Parent Client:', 'hi-school-slide-manager' ),
- 'edit_item' => __( 'Edit Client', 'hi-school-slide-manager' ),
- 'update_item' => __( 'Update Client', 'hi-school-slide-manager' ),
- 'add_new_item' => __( 'Add New Client', 'hi-school-slide-manager' ),
- 'new_item_name' => __( 'New Client Name', 'hi-school-slide-manager' ),
- 'menu_name' => __( 'Clients', 'hi-school-slide-manager' ),
- );
-
- $client_args = array(
- 'hierarchical' => false,
- 'labels' => $client_labels,
- 'show_ui' => true,
- 'show_admin_column' => true,
- 'query_var' => true,
- 'rewrite' => array( 'slug' => 'slide-client' ),
- 'show_in_rest' => true,
- );
-
- register_taxonomy( self::CLIENT_TAXONOMY, array( self::POST_TYPE ), $client_args );
-
// Add default terms for the three companion sites
$this->create_default_site_terms();
}
@@ -535,7 +500,6 @@ class HSSM_Slides_CPT {
public function slide_targeting_meta_box( $post ) {
// Get current terms
$current_sites = wp_get_post_terms( $post->ID, self::SITE_TAXONOMY, array( 'fields' => 'ids' ) );
- $current_clients = wp_get_post_terms( $post->ID, self::CLIENT_TAXONOMY, array( 'fields' => 'ids' ) );
// Get all available site terms
$site_terms = get_terms( array(
@@ -543,12 +507,6 @@ class HSSM_Slides_CPT {
'hide_empty' => false,
) );
- // Get all available client terms
- $client_terms = get_terms( array(
- 'taxonomy' => self::CLIENT_TAXONOMY,
- 'hide_empty' => false,
- ) );
-
?>