From da83beaec31b23da413abf424755a7fcccdf7ad4 Mon Sep 17 00:00:00 2001 From: Desmond Date: Sun, 1 Nov 2026 21:41:37 -0800 Subject: [PATCH] email working - remove clients - preview polish --- README.md | 15 +- assets/css/dashboard.css | 157 ++++++++- assets/css/frontend.css | 61 ---- assets/js/dashboard.js | 291 +++++++++------- assets/js/frontend.js | 79 ----- assets/js/preview.js | 2 +- hi-school-slide-manager.php | 77 ++++- includes/class-hssm-admin.php | 34 +- includes/class-hssm-api-client.php | 28 +- includes/class-hssm-frontend.php | 436 ++++++++++------------- includes/class-hssm-main.php | 36 +- includes/class-hssm-notifications.php | 475 +++++++++++++++++++------- includes/class-hssm-rest-api.php | 22 -- includes/class-hssm-shortcode.php | 122 ++++++- includes/class-hssm-slider.php | 41 ++- includes/class-hssm-slides-cpt.php | 149 ++------ templates/page-slide-confirmation.php | 93 ++--- templates/page-slide-dashboard.php | 157 +-------- templates/page-slide-preview.php | 127 ++++++- 19 files changed, 1342 insertions(+), 1060 deletions(-) delete mode 100644 assets/css/frontend.css delete mode 100644 assets/js/frontend.js 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 @@

No slides found

-

Create your first slide to get started!

+

${this.hasActiveFilters() ? 'No slides match the current filters.' : 'Create your first slide to get started!'}

@@ -833,8 +768,11 @@ slides.forEach(slide => { const imageUrl = slide.featured_image_url || ''; const statusClass = this.getStatusClass(slide.status); + const statusLabel = this.getStatusLabel(slide); const sitesText = this.formatSitesList(slide.sites || slide.target_sites); const scheduleText = this.formatScheduleText(slide); + const isPending = slide.status === 'pending'; + const isApproved = slide.status === 'publish' || slide.status === 'future'; html += `
@@ -848,7 +786,7 @@
${slide.subtitle || ''}
- ${slide.status} + ${statusLabel}
${scheduleText}
@@ -858,15 +796,28 @@
+ + ${isPending ? ` + + ` : isApproved ? ` + + ` : ` + `} @@ -885,17 +836,49 @@ this.bindSlideActions(); }, + highlightFocusedSlide: function() { + if (!this.focusSlideId) { + return; + } + + const $row = $('.hssm-slide-row[data-slide-id="' + this.focusSlideId + '"]'); + if (!$row.length) { + return; + } + + $row.addClass('hssm-slide-row-focus'); + if ($row[0] && $row[0].scrollIntoView) { + $row[0].scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + }, + // Helper function to get status CSS class getStatusClass: function(status) { const statusMap = { 'publish': 'published', 'draft': 'draft', 'future': 'scheduled', + 'pending': 'pending', 'private': 'private' }; return statusMap[status] || 'unknown'; }, + getStatusLabel: function(slide) { + if (slide.status_display) { + return slide.status_display; + } + + const labels = { + publish: 'Published', + draft: 'Draft', + future: 'Scheduled', + pending: 'Pending Approval', + private: 'Private' + }; + return labels[slide.status] || slide.status; + }, + // Helper function to format sites list formatSitesList: function(sites) { if (!sites || sites.length === 0) return 'No sites'; @@ -943,6 +926,12 @@ const slideId = $(e.currentTarget).data('slide-id'); this.toggleSlideStatus(slideId); }); + + $(document).off('click.hssmSlideActions', '.hssm-approve-slide').on('click.hssmSlideActions', '.hssm-approve-slide', (e) => { + e.preventDefault(); + const $btn = $(e.currentTarget); + this.approveSlide($btn.data('slide-id'), $btn.data('approve-action') || 'schedule'); + }); }, // Edit slide function @@ -1097,6 +1086,9 @@
+ @@ -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, - ) ); - ?>

@@ -569,27 +527,6 @@ class HSSM_Slides_CPT {
-
-

- - - -

- - - - -

- -
-