From 40099aa68a1a4bfc974c8f938deb48f643567040 Mon Sep 17 00:00:00 2001 From: Desmond Date: Sun, 8 Feb 2026 21:39:31 -0800 Subject: [PATCH] Initial commit of slide manager plugin --- FRONTEND_SETUP.md | 121 ++ Notifications.md | 26 + README.md | 110 ++ assets/css/admin-slides-dashboard.css | 409 ++++++ assets/css/admin.css | 75 + assets/css/dashboard.css | 1875 +++++++++++++++++++++++++ assets/css/frontend.css | 61 + assets/css/slider.css | 296 ++++ assets/js/admin-sortable.js | 207 +++ assets/js/admin.js | 223 +++ assets/js/dashboard.js | 1492 ++++++++++++++++++++ assets/js/frontend.js | 79 ++ assets/js/preview.js | 166 +++ assets/js/slider.js | 109 ++ fuck-you-google.html | 0 hi-school-slide-manager.php | 109 ++ includes/class-hssm-admin.php | 551 ++++++++ includes/class-hssm-api-client.php | 91 ++ includes/class-hssm-autoloader.php | 53 + includes/class-hssm-frontend.php | 1486 ++++++++++++++++++++ includes/class-hssm-main.php | 520 +++++++ includes/class-hssm-notifications.php | 274 ++++ includes/class-hssm-rest-api.php | 532 +++++++ includes/class-hssm-shortcode.php | 121 ++ includes/class-hssm-slider.php | 133 ++ includes/class-hssm-slides-cpt.php | 946 +++++++++++++ mimic-carousel.html | 214 +++ mop-up.md | 7 + prompt-builder-reference-and-notes.md | 19 + templates/page-slide-confirmation.php | 112 ++ templates/page-slide-dashboard.php | 369 +++++ templates/page-slide-preview.php | 498 +++++++ 32 files changed, 11284 insertions(+) create mode 100644 FRONTEND_SETUP.md create mode 100644 Notifications.md create mode 100644 README.md create mode 100644 assets/css/admin-slides-dashboard.css create mode 100644 assets/css/admin.css create mode 100644 assets/css/dashboard.css create mode 100644 assets/css/frontend.css create mode 100644 assets/css/slider.css create mode 100644 assets/js/admin-sortable.js create mode 100644 assets/js/admin.js create mode 100644 assets/js/dashboard.js create mode 100644 assets/js/frontend.js create mode 100644 assets/js/preview.js create mode 100644 assets/js/slider.js create mode 100644 fuck-you-google.html create mode 100644 hi-school-slide-manager.php create mode 100644 includes/class-hssm-admin.php create mode 100644 includes/class-hssm-api-client.php create mode 100644 includes/class-hssm-autoloader.php create mode 100644 includes/class-hssm-frontend.php create mode 100644 includes/class-hssm-main.php create mode 100644 includes/class-hssm-notifications.php create mode 100644 includes/class-hssm-rest-api.php create mode 100644 includes/class-hssm-shortcode.php create mode 100644 includes/class-hssm-slider.php create mode 100644 includes/class-hssm-slides-cpt.php create mode 100644 mimic-carousel.html create mode 100644 mop-up.md create mode 100644 prompt-builder-reference-and-notes.md create mode 100644 templates/page-slide-confirmation.php create mode 100644 templates/page-slide-dashboard.php create mode 100644 templates/page-slide-preview.php diff --git a/FRONTEND_SETUP.md b/FRONTEND_SETUP.md new file mode 100644 index 0000000..8eb48af --- /dev/null +++ b/FRONTEND_SETUP.md @@ -0,0 +1,121 @@ +# Hi-School Slide Manager Frontend Dashboard Setup + +## Quick Setup Guide + +### 1. Create the Dashboard Page + +1. Go to WordPress Admin → Pages → Add New +2. Set the page title to: **Slide Dashboard** +3. Set the page slug to: **slide-dashboard** +4. In the Page Attributes section, select the template: **Slide Dashboard** +5. Publish the page + +### 2. Full-Width Dashboard Features + +The dashboard now includes: + +- **🎯 Custom Header Bar**: Professional navigation with Home, Admin, and Logout buttons +- **📊 Status Bar**: Real-time statistics for active slides, connected sites, and last update +- **📝 Enhanced Tabs**: Create Slides, Manage Slides, Preview Banner, and Analytics +- **🔄 Bulk Actions**: Save all slides, save as drafts, import/export functionality +- **📱 Responsive Design**: Works perfectly on all devices + +### 3. Navigation Features + +**Header Navigation:** +- 🏠 **Home Button**: Quick return to site homepage +- ⚙️ **Admin Button**: Access WordPress admin dashboard +- 👤 **User Menu**: Shows current user and logout option + +**Tab Navigation:** +- ➕ **Create Slides**: Add up to 5 slide forms simultaneously +- 📋 **Manage Slides**: View, edit, and delete existing slides +- 👁️ **Preview Banner**: See how slides appear on companion sites +- 📈 **Analytics**: View slide performance and statistics + +### 4. Set Up Members Plugin (if not already configured) + +1. Install and activate the Members plugin +2. Go to Users → Roles → Add New Role +3. Create a role called "Slide Manager" with these capabilities: + - `read` + - `edit_posts` + - `upload_files` + - `hssm_manage_slides` (custom capability) + +### 5. Test the Dashboard + +1. Create a test user with the "Slide Manager" role +2. Navigate to `/slide-dashboard/` on your site +3. You should see the full-width slide management interface + +### 6. Dashboard Workflow + +**Creating Slides:** +1. Click "Add New Slide Form" (up to 5 forms at once) +2. Fill in slide details: title, subtitle, content, client +3. Upload background image with live preview +4. Add up to 5 CTA buttons per slide +5. Select target sites (Hi-School, One Stop, Ace) +6. Set scheduling dates and auto-publish options +7. Save individual slides or use "Save All Slides" + +**Managing Slides:** +1. Filter by site, client, or status +2. Edit existing slides inline +3. Delete slides with confirmation +4. Export slide data for backup + +**Preview System:** +1. Select target site and preview date +2. See real-time banner preview +3. Email preview to stakeholders +4. Download screenshots for approval + +### 5. Template Override (Optional) + +If you want to customize the dashboard template: + +1. Copy `templates/page-slide-dashboard.php` to your active theme +2. Customize as needed - the plugin will use the theme version + +### 6. API Key Management + +- Go to WordPress Admin → Slides → Settings +- Manage API keys for companion sites +- Test REST API endpoints + +### 7. Companion Site Integration + +Use these REST API endpoints on companion sites: + +- `GET /wp-json/hssm/v1/slides/{site}` - Get slides for a specific site +- `GET /wp-json/hssm/v1/sites` - Get available sites +- `GET /wp-json/hssm/v1/slide/{id}` - Get specific slide +- `GET /wp-json/hssm/v1/health` - Check API health + +### 8. Divi 5 Group Carousel Integration + +The slides are designed to work with Divi 5's Group Carousel module: + +1. Add a Group Carousel module to your header +2. Use the REST API to fetch slide data +3. Populate carousel items with slide content +4. Apply background images and CTA buttons + +### 9. Troubleshooting + +**Dashboard not loading?** +- Check if user has proper permissions +- Verify Members plugin is active +- Check error logs for PHP errors + +**AJAX not working?** +- Verify nonce generation +- Check user permissions +- Enable WordPress debug mode + +**Template not found?** +- Ensure template file exists in plugin directory +- Check file permissions +- Verify page template is set correctly \ No newline at end of file diff --git a/Notifications.md b/Notifications.md new file mode 100644 index 0000000..0975355 --- /dev/null +++ b/Notifications.md @@ -0,0 +1,26 @@ +# Notifications + +The notifications Tab will have notification cofigurations with the following fields: + +Notifications for "Editor": This will be the person approving the Slides. When the slides are created, this person will be sent a notification with a . +Notifications for "ITCGuys" This notification will be sent when the slide is Approved for an I.T.C.G to review and test the date range to make sure slide is correctly set. When the slides go live, a notification will be sent to make sure slide is live and correctly set. After confirming the slide is live ther will be a link to a 'send "LIVE" confirmation' which will be sent to Editor and Creator of the slide, notifying them that it has been checked. + +Notification Tab in the Slide Dashboard + +**Email fields:** +- Editor: +- I.T.C.Guys: + +The creator will be the Author of the slides. + +Notification Triggers: +- Notification to "Editor" when slide is sucsessfully scheduled or Published Immidiately + - This notification will contain a generated link to the slide-preview page with params containing the slide-title, the publish-date that the slide is scheduled to be live, and the site slugs which when clicked, will display the slides that will be live on that date. +- Notification when the slide has been approved sent to the creator (author). +- Notification sent when slide goes live sent to ITCG reps who will double-check that it has been published, clear the cache if needed. + - This notification will have a link to the site(s) the slide has been published on, and a link to a confirmation page (this page will run a script to generate and send the LAST notification to Editor and Author) +- Final Notification sent to "Editor" and "Author": Generated by a script, will contain a link to the sites that have the slide, it will also have a link to the dashboard with the params in the link. + +We need a page "Slide is Live" with text that reads "I confirm that {slide-title} is Live. with a green confirmation button that will the generate and send the Last email. + +We need a tab on the Slide Dashoard for the Email fields to set for notifications. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c4014ea --- /dev/null +++ b/README.md @@ -0,0 +1,110 @@ +# Hi-School Slide Manager + +A WordPress plugin to manage slides for Hi-School websites. + +## Features + +- Custom post type for slides management +- Easy-to-use admin interface +- Shortcode support for displaying slides +- Responsive design +- Image upload support +- Customizable slide display + +## Installation + +1. Upload the plugin files to the `/wp-content/plugins/hi-school-slide-manager/` directory +2. Activate the plugin through the 'Plugins' screen in WordPress +3. Use the 'Slides' menu in the WordPress admin to manage your slides + +## Usage + +### Admin Interface + +1. Navigate to "Slides" in your WordPress admin menu +2. Create new slides with titles, content, and images +3. Manage existing slides through the standard WordPress interface + +### Shortcode + +Display slides on any page or post using the shortcode: + +``` +[hssm_slides] +``` + +Shortcode parameters: +- `count` - Number of slides to display (default: 5) +- `category` - Filter slides by category (optional) + +Examples: +``` +[hssm_slides count="3"] +[hssm_slides count="10" category="featured"] +``` + +## File Structure + +``` +hi-school-slide-manager/ +├── hi-school-slide-manager.php # Main plugin file +├── includes/ +│ ├── HSSM_Autoloader.php # Autoloader class +│ └── HSSM_Plugin.php # Main plugin class +├── assets/ +│ ├── css/ +│ │ ├── frontend.css # Frontend styles +│ │ └── admin.css # Admin styles +│ └── js/ +│ ├── frontend.js # Frontend JavaScript +│ └── admin.js # Admin JavaScript +└── README.md # This file +``` + +## Hooks and Filters + +### Actions +- `hssm_before_slide_display` - Fired before displaying a slide +- `hssm_after_slide_display` - Fired after displaying a slide + +### Filters +- `hssm_slide_content` - Filter slide content before display +- `hssm_shortcode_atts` - Filter shortcode attributes + +## Customization + +### CSS Classes + +The plugin uses the following CSS classes for styling: +- `.hssm-slides-container` - Main container for slides +- `.hssm-slide` - Individual slide wrapper +- `.hssm-slide-content` - Slide content area +- `.hssm-slide-image` - Slide image container + +### Custom Templates + +You can override the slide display by creating custom templates in your theme: +- `single-hssm_slide.php` - Single slide template +- `archive-hssm_slide.php` - Slide archive template + +## Requirements + +- WordPress 5.0 or higher +- PHP 7.4 or higher + +## License + +This plugin is licensed under the GPL v2 or later. + +## Support + +For support and questions, please contact the plugin developer. + +## Changelog + +### 1.0.0 +- Initial release +- Basic slide management functionality +- Shortcode support +- Admin interface +- Responsive design \ No newline at end of file diff --git a/assets/css/admin-slides-dashboard.css b/assets/css/admin-slides-dashboard.css new file mode 100644 index 0000000..115c42e --- /dev/null +++ b/assets/css/admin-slides-dashboard.css @@ -0,0 +1,409 @@ +/** + * Hi-School Slide Manager - Admin Slides Dashboard Styles + */ + +/* General dashboard elements */ +.hssm-dashboard-main { + background: #f0f2f5; + padding: 20px; + border-radius: 8px; +} + +.hssm-tab-header h2 { + color: #333; + font-size: 20px; + margin-bottom: 15px; +} + +/* Styles for the slide list within the 'Manage Slides' tab */ +.hssm-slides-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 20px; + margin-top: 20px; +} + +.hssm-slide-item { + background: #ffffff; + border: 1px solid #e0e0e0; + border-radius: 8px; + padding: 15px; + display: flex; + flex-direction: column; + gap: 10px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.hssm-slide-item:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.hssm-slide-item .hssm-slide-info { + flex-grow: 1; +} + +.hssm-slide-item .hssm-slide-title { + font-size: 1.1em !important; + font-weight: 600; + color: #2c3338 !important; + margin-bottom: 5px !important; + line-height: 1.3 !important; +} + +.hssm-slide-item .hssm-slide-subtitle { + font-size: 0.9em; + color: #555d66; + margin-bottom: 10px; + line-height: 1.4; +} + +.hssm-slide-item .hssm-slide-meta { + font-size: 0.85em; + color: #72777c; + line-height: 1.6; +} + +.hssm-slide-item .hssm-status-badge { + display: inline-block; + padding: 3px 8px; + border-radius: 4px; + font-size: 0.8em; + font-weight: 700; + text-transform: uppercase; + margin-bottom: 5px; +} + +.hssm-slide-item .hssm-status-pending { + background-color: #ffba00; + color: #fff; +} + +.hssm-slide-item .hssm-status-publish { + background-color: #46b450; + color: #fff; +} + +.hssm-slide-item .hssm-slide-thumbnail { + width: 100%; + padding-bottom: 56.25%; /* 16:9 aspect ratio */ + background-size: cover; + background-position: center; + background-color: #f0f0f0; + border-radius: 4px; + margin-top: 10px; +} + +.hssm-slide-item .hssm-slide-actions { + display: flex; + gap: 8px; + margin-top: 10px; +} + +/* Styles for the slide forms in the 'Create Slides' tab */ +.hssm-slide-form { + background: #ffffff; + border: 1px solid #e0e0e0; + border-radius: 8px; + padding: 20px; + margin-bottom: 20px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); +} + +.hssm-form-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding-bottom: 10px; + border-bottom: 1px solid #eee; +} + +.hssm-form-header h3 { + margin: 0; + font-size: 1.2em; + color: #333; +} + +.hssm-remove-form { + background: none; + border: none; + color: #a00; + cursor: pointer; + font-size: 1.5em; +} + +.hssm-form-grid { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 20px; +} + +.hssm-form-section h4 { + font-size: 1em; + color: #555d66; + margin-top: 0; + margin-bottom: 15px; + padding-bottom: 5px; + border-bottom: 1px solid #eee; +} + +.hssm-field-row { + display: flex; + gap: 15px; + margin-bottom: 15px; +} + +.hssm-field-row .hssm-field { + flex: 1; +} + +.hssm-field label { + display: block; + font-weight: 500; + margin-bottom: 5px; + color: #333; + font-size: 0.9em; +} + +.hssm-input, .hssm-textarea, .hssm-select { + width: 100%; + padding: 8px 10px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 0.9em; + color: #333; + background-color: #f9f9f9; + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.07); +} + +.hssm-input[type="date"] { + padding: 7px 10px; +} + +.hssm-input:focus, .hssm-textarea:focus, .hssm-select:focus { + border-color: #007cba; + box-shadow: 0 0 0 1px #007cba; + outline: none; +} + +.hssm-textarea { + min-height: 80px; + resize: vertical; +} + +.hssm-image-preview-small { + width: 100%; + height: 120px; /* Smaller preview */ + background-color: #eee; + border: 1px dashed #ccc; + border-radius: 4px; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + margin-bottom: 10px; + background-size: cover; + background-position: center; + position: relative; + overflow: hidden; +} + +.hssm-image-preview-small .hssm-placeholder { + color: #888; + font-size: 0.9em; +} + +.hssm-image-preview-small img { + max-width: 100%; + max-height: 100%; + display: block; + object-fit: cover; +} + +.hssm-image-controls { + display: flex; + gap: 8px; + margin-bottom: 15px; +} + +.hssm-button-row-compact { + display: flex; + gap: 8px; + align-items: center; + margin-bottom: 8px; +} + +.hssm-button-row-compact .hssm-input-small { + flex: 1; + padding: 6px 8px; + font-size: 0.85em; +} + +.hssm-btn-tiny { + padding: 4px 8px; + font-size: 0.8em; + background-color: #dc3232; + color: #fff; + border: none; + border-radius: 3px; + cursor: pointer; +} + +.hssm-btn-tiny:hover { + background-color: #b30000; +} + +.hssm-checkboxes-compact label { + font-size: 0.9em; + color: #333; + margin-bottom: 5px; +} + +.hssm-checkbox-compact input[type="checkbox"] { + margin-right: 5px; +} + +.hssm-form-actions { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 20px; + padding-top: 15px; + border-top: 1px solid #eee; +} + +/* Override some general plugin button styles for admin context if needed */ +.hssm-btn-primary { + background-color: #007cba; + color: #fff; + border-color: #007cba; +} + +.hssm-btn-primary:hover { + background-color: #006799; + border-color: #006799; +} + +.hssm-btn-secondary { + background-color: #6c757d; + color: #fff; + border-color: #6c757d; +} + +.hssm-btn-secondary:hover { + background-color: #5a6268; + border-color: #545b62; +} + +.hssm-btn-success { + background-color: #28a745; + color: #fff; + border-color: #28a745; +} + +.hssm-btn-success:hover { + background-color: #218838; + border-color: #1e7e34; +} + +.hssm-btn-outline { + background-color: transparent; + color: #6c757d; + border: 1px solid #6c757d; +} + +.hssm-btn-outline:hover { + background-color: #6c757d; + color: #fff; +} + +.hssm-btn-large { + padding: 12px 25px; + font-size: 1em; +} + +.hssm-btn-small { + padding: 6px 12px; + font-size: 0.85em; +} + +.hssm-btn .dashicons { + vertical-align: middle; + margin-right: 5px; +} + +/* Specific overrides for the slide preview elements that should look like dashboard items */ +/* This applies to the list *below* the main slider preview on the slide preview page */ +body.page-template-page-slide-preview .hssm-slides-list.hssm-mt-20 .hssm-slide-item { + flex-direction: row; + align-items: center; + gap: 20px; + padding: 15px; + margin-bottom: 10px; + background: #fdfdfd; + border: 1px solid #eee; + color: #333; /* Ensure general text is legible */ +} + +body.page-template-page-slide-preview .hssm-slides-list.hssm-mt-20 .hssm-slide-thumbnail { + width: 120px; + min-width: 120px; + height: 70px; /* Adjusted for smaller, list-like thumbnail */ + padding-bottom: 0; + border-radius: 4px; + margin: 0; +} + +body.page-template-page-slide-preview .hssm-slides-list.hssm-mt-20 .hssm-slide-info { + flex-grow: 1; +} + +body.page-template-page-slide-preview .hssm-slides-list.hssm-mt-20 .hssm-slide-title { + font-size: 1.1em !important; + color: #23282d !important; + margin-bottom: 3px !important; +} + +body.page-template-page-slide-preview .hssm-slides-list.hssm-mt-20 .hssm-slide-subtitle { + font-size: 0.9em; + color: #555d66; + margin-bottom: 5px; +} + +body.page-template-page-slide-preview .hssm-slides-list.hssm-mt-20 .hssm-slide-meta { + font-size: 0.8em; + color: #72777c; +} + +body.page-template-page-slide-preview .hssm-slides-list.hssm-mt-20 .hssm-slide-actions { + flex-direction: row; + flex-wrap: nowrap; + gap: 5px; + margin-top: 0; +} + +body.page-template-page-slide-preview .hssm-slides-list.hssm-mt-20 .hssm-slide-item .hssm-slide-title { + background-color: #007cba; + color: #ffffff; + padding: 5px 10px; + border-radius: 4px; + display: inline-block; +} + +body.page-template-page-slide-preview .hssm-slides-list.hssm-mt-20 .hssm-slide-item .hssm-slide-subtitle, +body.page-template-page-slide-preview .hssm-slides-list.hssm-mt-20 .hssm-slide-item .hssm-slide-meta { + background-color: #444; + color: #ffffff; + padding: 3px 8px; + border-radius: 3px; + display: inline-block; + margin-top: 5px; +} + +body.page-template-page-slide-preview .hssm-slides-list.hssm-mt-20 .hssm-btn { + padding: 5px 10px; + font-size: 0.8em; +} diff --git a/assets/css/admin.css b/assets/css/admin.css new file mode 100644 index 0000000..1355ddb --- /dev/null +++ b/assets/css/admin.css @@ -0,0 +1,75 @@ +/** + * Hi-School Slide Manager - Admin Styles + */ + +.hssm-admin-page { + max-width: 1200px; + margin: 20px 0; +} + +.hssm-admin-header { + background: #fff; + border: 1px solid #ccd0d4; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); + padding: 20px; + margin-bottom: 20px; +} + +.hssm-admin-header h1 { + margin: 0; + color: #23282d; +} + +.hssm-settings-form { + background: #fff; + border: 1px solid #ccd0d4; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); + padding: 20px; +} + +.hssm-form-group { + margin-bottom: 20px; +} + +.hssm-form-group label { + display: block; + margin-bottom: 5px; + font-weight: 600; + color: #23282d; +} + +.hssm-form-group input, +.hssm-form-group select, +.hssm-form-group textarea { + width: 100%; + max-width: 400px; + padding: 8px 12px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; +} + +.hssm-form-group textarea { + min-height: 100px; + resize: vertical; +} + +.hssm-btn { + background: #0073aa; + color: #fff; + border: none; + padding: 10px 20px; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + transition: background-color 0.3s ease; +} + +.hssm-btn:hover { + background: #005a87; +} + +.hssm-btn:focus { + outline: 2px solid #0073aa; + outline-offset: 2px; +} \ No newline at end of file diff --git a/assets/css/dashboard.css b/assets/css/dashboard.css new file mode 100644 index 0000000..d569319 --- /dev/null +++ b/assets/css/dashboard.css @@ -0,0 +1,1875 @@ +/** + * Hi-School Slide Manager Dashboard Styles + * + * Full-width Divi-compatible styles for the slide management dashboard + * + * @package Hi_School_Slide_Manager + * @since 1.0.0 + */ + +/* ========================================== + Full-Width Layout Override + ========================================== */ + +#main-content .container-fluid { + max-width: 100%; + padding: 0; + margin: 0; +} + +#content-area.full-width { + width: 100%; + margin: 0; + padding: 0; +} + +/* ========================================== + Custom Header Bar + ========================================== */ + +.hssm-header-bar { + background: #ff0000;; + color: #fff; + padding: 0; + margin: 0; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + position: relative; + z-index: 1000; +} + +.hssm-header-content { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 40px; + max-width: 1400px; + margin: 0 auto; +} + +.hssm-logo-section { + display: flex; + flex-direction: column; + gap: 5px; +} + +.hssm-site-title { + margin: 0; + font-size: 1.8em; + font-weight: 600; + color: #fff; + display: flex; + align-items: center; + gap: 12px; +} + +.hssm-icon { + font-size: 1.2em; + filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.3)); +} + +.hssm-site-subtitle { + font-size: 0.9em; + color: #bdc3c7; + font-weight: 300; + margin-left: 44px; +} + +.hssm-header-nav { + display: flex; + align-items: center; + gap: 15px; +} + +.hssm-nav-btn { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 10px 18px; + background: rgba(255, 255, 255, 0.1); + color: #fff; + text-decoration: none; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + transition: all 0.3s ease; + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.hssm-nav-btn:hover { + background: rgba(255, 255, 255, 0.2); + color: #fff; + text-decoration: none; + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +} + +.hssm-nav-home { + background: rgb(255, 255, 255, 0.3); + border-color: rgb(255, 255, 255, 0.5); +} + +.hssm-nav-admin { + background: rgb(255, 255, 255, 0.3); + border-color: rgb(255, 255, 255, 0.5); +} + +.hssm-nav-preview { + background: rgb(255, 255, 255, 0.3); + border-color: rgb(255, 255, 255, 0.5); +} + +.hssm-nav-logout { + background: rgb(255, 255, 255, 0.3); + border-color: rgb(255, 255, 255, 0.5); +} + +.hssm-user-menu { + display: flex; + align-items: center; + gap: 15px; + margin-left: 20px; + padding-left: 20px; + border-left: 1px solid rgba(255, 255, 255, 0.2); +} + +.hssm-user-name { + font-size: 14px; + color: #ecf0f1; + font-weight: 500; +} + +/* ========================================== + Dashboard Main Content + ========================================== */ + +.hssm-dashboard-main { + padding: 30px 40px; + max-width: 1400px; + margin: 0 auto; + min-height: calc(100vh - 120px); +} + +/* ========================================== + Enhanced Navigation Tabs + ========================================== */ + +.hssm-dashboard-nav { + margin-bottom: 30px; + background: #fff; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.hssm-nav-tabs { + display: flex; + list-style: none; + margin: 0; + padding: 0; + background: #f8f9fa; + border-bottom: 1px solid #e9ecef; +} + +.hssm-nav-tabs li { + flex: 1; +} + +.hssm-nav-tabs a { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 18px 20px; + background: transparent; + color: #6c757d; + text-decoration: none; + border-bottom: 3px solid transparent; + transition: all 0.3s ease; + font-weight: 500; + font-size: 14px; +} + +.hssm-nav-tabs li.active a, +.hssm-nav-tabs a:hover { + background: #fff; + color: #2c3e50; + border-bottom-color: #3498db; +} + +.hssm-nav-tabs li.active a { + font-weight: 600; +} + +/* ========================================== + Tab Content Enhancement + ========================================== */ + +.hssm-tab-content { + display: none; + background: #fff; + padding: 0; + border-radius: 8px; + min-height: 600px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + overflow: hidden; + margin-top: 0; +} + +.hssm-tab-content.active { + display: block; +} + +.hssm-tab-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 25px 30px; + border-bottom: 1px solid #e9ecef; + background: #f8f9fa; + margin: 0; +} + +.hssm-tab-header h2 { + margin: 0; + color: #2c3e50; + font-size: 1.5em; + font-weight: 600; +} + +.hssm-tab-actions { + display: flex; + gap: 12px; + align-items: center; +} + +/* ========================================== + Messages + ========================================== */ + +.hssm-messages { + margin-bottom: 20px; +} + +.hssm-message { + padding: 12px 16px; + margin-bottom: 10px; + border-radius: 4px; + border-left: 4px solid; + background: #fff; +} + +.hssm-message.success { + border-left-color: #4caf50; + background-color: #f1f8e9; + color: #2e7d32; +} + +.hssm-message.error { + border-left-color: #f44336; + background-color: #ffebee; + color: #c62828; +} + +.hssm-message.warning { + border-left-color: #ff9800; + background-color: #fff3e0; + color: #ef6c00; +} + +.hssm-message.info { + border-left-color: #2196f3; + background-color: #e3f2fd; + color: #1565c0; +} + +/* ========================================== + Form Styling + ========================================== */ + +.hssm-tab-content input[type=text], .hssm-tab-content input[type=text]:focus, .hssm-textarea, .hssm-textarea:focus { + color:#212529 !important; +} + +.hssm-slide-form { + background: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 8px; + margin-bottom: 30px; + overflow: hidden; +} + +.hssm-form-header { + display: flex; + justify-content: space-between; + align-items: center; + background: #6c757d; + color: #fff; + padding: 15px 20px; +} + +.hssm-form-header h3 { + font-family: Open Sans,Arial,sans-serif; + color: #fff; + margin: 0; + font-size: 1.4em; + font-weight: 600; +} + +.hssm-remove-form { + background: none; + border: none; + color: #fff; + cursor: pointer; + padding: 5px; + border-radius: 3px; + transition: background-color 0.2s; +} + +.hssm-remove-form:hover { + background-color: rgba(255, 255, 255, 0.2); +} + +.hssm-form-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 30px; + padding: 30px; +} + +.hssm-form-section { + background: #fff; + padding: 20px; + border-radius: 6px; + border: 1px solid #e5e5e5; +} + +.hssm-form-section h4 { + margin: 0 0 20px 0; + color: #275191; + font-size: 1.1em; + font-weight: 600; + border-bottom: 2px solid #ffffff; + padding-bottom: 8px; +} + +.hssm-buttons-section { + grid-column: 1 / -1; +} + +/* ========================================== + Form Fields + ========================================== */ + +.hssm-field { + margin-bottom: 20px; +} + +.hssm-field-group { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 15px; +} + +.hssm-field label { + display: block; + margin-bottom: 5px; + font-weight: 500; + color: #275191; +} + +.hssm-input, +.hssm-textarea, +.hssm-select { + width: 100%; + padding: 10px 12px; + border: 1px solid #ced4da; + border-radius: 4px; + font-size: 14px; + background-color: #ffffff; + color: #275191; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} + +.hssm-input:focus, +.hssm-textarea:focus, +.hssm-select:focus { + outline: none; + border-color: #ffffff; + box-shadow: 0 0 0 2px rgba(0, 124, 186, 0.25); +} + +.hssm-textarea { + resize: vertical; + min-height: 80px; +} + +/* ========================================== + Checkboxes and Radio Buttons + ========================================== */ + +.hssm-checkboxes { + display: flex; + flex-direction: column; + gap: 8px; +} + +.hssm-checkbox { + display: flex; + align-items: center; + font-weight: normal !important; + margin-bottom: 0 !important; + cursor: pointer; +} + +.hssm-checkbox input[type="checkbox"] { + width: auto !important; + margin-right: 8px; + margin-bottom: 0; +} + +.hssm-radio-group { + display: flex; + flex-direction: column; + gap: 10px; + margin-top: 8px; +} + +.hssm-radio { + display: flex; + align-items: center; + font-weight: normal !important; + margin-bottom: 0 !important; + cursor: pointer; + padding: 8px 12px; + border-radius: 4px; + transition: background-color 0.2s ease; +} + +.hssm-radio:hover { + background-color: #f8f9fa; +} + +.hssm-radio input[type="radio"] { + width: auto !important; + margin-right: 10px; + margin-bottom: 0; +} + +.hssm-help-text { + display: block; + margin-top: 5px; + font-size: 12px; + color: #6c757d; + font-style: italic; +} + +/* ========================================== + Image Upload + ========================================== */ + +.hssm-image-upload { + text-align: center; +} + +.hssm-image-preview { + width: 100%; + height: 200px; + border: 2px dashed #ced4da; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 15px; + cursor: pointer; + transition: border-color 0.3s ease; + background-size: cover; + background-position: center; + background-repeat: no-repeat; +} + +.hssm-image-preview:hover { + border-color: #ffffff; +} + +.hssm-image-preview.has-image { + border-style: solid; +} + +.hssm-placeholder { + color: #6c757d; + font-size: 14px; +} + +.hssm-image-preview.has-image .hssm-placeholder { + display: none; +} + +/* ========================================== + Enhanced Buttons + ========================================== */ + +.hssm-btn { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 12px 20px; + border: 1px solid transparent; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + text-decoration: none; + cursor: pointer; + transition: all 0.2s ease; + background-color: #f8f9fa; + color: #275191; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.hssm-btn:hover { + text-decoration: none; + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); +} + +.hssm-btn-primary { + background: linear-gradient(135deg, #3498db 0%, #275191 100%); + color: #fff; + border-color: #275191; +} + +.hssm-btn-primary:hover { + background: linear-gradient(135deg, #3498db 0%, #21618c 100%); + color: #fff; +} + +.hssm-btn-success { + background: linear-gradient(135deg, #27ae60 0%, #229954 100%); + color: #fff; + border-color: #229954; +} + +.hssm-btn-success:hover { + background: linear-gradient(135deg, #229954 0%, #1e8449 100%); + color: #fff; +} + +.hssm-btn-secondary { + background: linear-gradient(135deg, #95a5a6 0%, #7f8c8d 100%); + color: #fff; + border-color: #7f8c8d; +} + +.hssm-btn-secondary:hover { + background: linear-gradient(135deg, #7f8c8d 0%, #6c757d 100%); + color: #fff; +} + +.hssm-btn-outline { + background: transparent; + color: #6c757d; + border: 2px solid #dee2e6; +} + +.hssm-btn-outline:hover { + background: #f8f9fa; + border-color: #adb5bd; +} + +.hssm-btn-small { + padding: 8px 14px; + font-size: 12px; +} + +.hssm-btn-large { + padding: 16px 32px; + font-size: 16px; + font-weight: 600; +} + +/* ========================================== + Analytics Cards + ========================================== */ + +.hssm-analytics-content { + padding: 30px; +} + +.hssm-analytics-cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 20px; + margin-bottom: 30px; +} + +.hssm-analytics-card { + background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%); + padding: 25px; + border-radius: 8px; + border: 1px solid #e9ecef; + text-align: center; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.hssm-analytics-card:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +.hssm-analytics-card h3 { + margin: 0 0 15px 0; + color: #6c757d; + font-size: 14px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.hssm-analytics-number { + font-size: 2.5em; + font-weight: 700; + color: #2c3e50; + line-height: 1; +} + +/* ========================================== + Form Enhancements - Compact Layout + ========================================== */ + +.hssm-slide-form { + background: #fff; + border: 1px solid #e9ecef; + border-radius: 8px; + margin-bottom: 25px; + overflow: hidden; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + transition: box-shadow 0.2s ease; +} + +.hssm-slide-form:hover { + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.hssm-form-header { + display: flex; + justify-content: space-between; + align-items: center; + background: #ff0000; + color: #fff; + padding: 12px 20px; +} + +.hssm-form-header h3 { + margin: 0; + font-size: 1em; + font-weight: 600; +} + +.hssm-remove-form { + background: rgba(231, 76, 60, 0.2); + border: 1px solid rgba(231, 76, 60, 0.3); + color: #fff; + cursor: pointer; + padding: 6px 10px; + border-radius: 4px; + transition: all 0.2s ease; + font-size: 11px; +} + +.hssm-remove-form:hover { + background: rgba(231, 76, 60, 0.4); + border-color: rgba(231, 76, 60, 0.5); +} + +.hssm-form-grid { + display: grid; + grid-template-columns: 1fr 300px; + gap: 20px; + padding: 20px; +} + +.hssm-form-section { + background: #f8f9fa; + padding: 15px; + border-radius: 6px; + border: 1px solid #e9ecef; +} + +.hssm-form-section h4 { + margin: 0 0 15px 0; + color: #275191; + font-size: 0.95em; + font-weight: 600; + border-bottom: 2px solid #3498db; + padding-bottom: 6px; +} + +/* Compact field layouts */ +.hssm-field { + margin-bottom: 15px; +} + +.hssm-field-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; + margin-bottom: 15px; +} + +.hssm-field label { + display: block; + margin-bottom: 4px; + font-weight: 500; + color: #ff0000; + font-size: 13px; +} + +.hssm-input, +.hssm-textarea, +.hssm-select { + width: 100%; + padding: 8px 10px; + border: 1px solid #ced4da; + border-radius: 4px; + font-size: 13px; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} + +.hssm-textarea { + resize: vertical; + min-height: 60px; +} + +/* Compact image upload */ +.hssm-image-upload-compact { + text-align: center; +} + +.hssm-image-preview-small { + width: 100%; + height: 150px; + border: 2px dashed #ced4da; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 10px; + cursor: pointer; + transition: border-color 0.3s ease; + background-size: cover; + background-position: center; + background-repeat: no-repeat; +} + +.hssm-image-preview-small:hover { + border-color: #ffffff; +} + +.hssm-image-preview-small.has-image { + border-style: solid; +} + +.hssm-image-controls { + display: flex; + flex-direction: column; + gap: 5px; +} + +/* Compact checkboxes */ +.hssm-checkboxes-compact { + display: flex; + flex-direction: column; + gap: 6px; +} + +.hssm-checkbox-compact { + display: flex; + align-items: center; + font-weight: normal !important; + margin-bottom: 0 !important; + cursor: pointer; + font-size: 13px; +} + +.hssm-checkbox-compact input[type="checkbox"] { + width: auto !important; + margin-right: 6px; + margin-bottom: 0; +} + +/* Compact button management */ +.hssm-buttons-container-compact { + margin-bottom: 10px; +} + +.hssm-button-row-compact { + display: grid; + grid-template-columns: 1fr 1fr auto; + gap: 5px; + align-items: center; + margin-bottom: 8px; + padding: 8px; + background: #fff; + border-radius: 4px; + border: 1px solid #e9ecef; +} + +.hssm-input-small { + padding: 6px 8px !important; + font-size: 12px !important; + margin-bottom: 0 !important; +} + +.hssm-btn-tiny { + padding: 4px 8px; + font-size: 12px; + min-width: auto; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; +} + +/* Enhanced form actions */ +.hssm-form-actions { + display: flex; + gap: 12px; + justify-content: center; + padding: 15px 20px; + background: #f1f3f4; + border-top: 1px solid #e5e5e5; +} + +.hssm-schedule-slide { + background: linear-gradient(135deg, #3498db 0%, #275191 100%); + color: #fff; + border-color: #275191; +} + +.hssm-schedule-slide:hover { + background: linear-gradient(135deg, #275191 0%, #3498db 100%); + color: #fff; +} + +.hssm-publish-slide { + background: linear-gradient(135deg, #27ae60 0%, #229954 100%); + color: #fff; + border-color: #229954; +} + +.hssm-publish-slide:hover { + background: linear-gradient(135deg, #229954 0%, #1e8449 100%); + color: #fff; +} + +/* Loading states for compact forms */ +.hssm-slide-form.hssm-loading { + opacity: 0.7; + pointer-events: none; + position: relative; +} + +.hssm-slide-form.hssm-loading::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 20px; + height: 20px; + margin: -10px 0 0 -10px; + border: 2px solid #3498db; + border-radius: 50%; + border-top-color: transparent; + animation: spin 1s linear infinite; + z-index: 9999; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* Image upload states */ +.hssm-image-preview-small.has-image span { + display: none; +} + +.hssm-image-preview-small:not(.has-image) { + color: #6c757d; + font-size: 13px; +} + +/* Form transitions */ +.hssm-slide-form { + transition: all 0.3s ease; +} + +.hssm-slide-form:hover { + transform: translateY(-2px); +} + +/* ========================================== + Responsive Design + ========================================== */ + +@media (max-width: 1200px) { + .hssm-form-grid { + grid-template-columns: 1fr 250px; + gap: 15px; + padding: 15px; + } +} + +@media (max-width: 992px) { + .hssm-form-grid { + grid-template-columns: 1fr; + gap: 15px; + } + + .hssm-image-upload-compact { + max-width: 300px; + margin: 0 auto; + } + + .hssm-form-actions { + flex-direction: column; + align-items: stretch; + } + + .hssm-form-actions .hssm-btn { + margin-bottom: 8px; + } + + .hssm-form-actions .hssm-btn:last-child { + margin-bottom: 0; + } +} + +@media (max-width: 768px) { + .hssm-field-row { + grid-template-columns: 1fr; + gap: 8px; + } + + .hssm-button-row-compact { + grid-template-columns: 1fr; + gap: 8px; + } + + .hssm-image-preview-small { + height: 100px; + } + + .hssm-form-section { + padding: 12px; + } + + .hssm-form-header { + padding: 10px 15px; + } + + .hssm-form-header h3 { + font-size: 0.9em; + } +} + +/* ========================================== + Filters Enhancement + ========================================== */ + +.hssm-filters { + display: flex; + gap: 15px; + align-items: center; + flex-wrap: wrap; + padding: 20px; + background: #f8f9fa; + border-radius: 6px; + margin-bottom: 20px; +} + +.hssm-filters .hssm-select { + width: auto; + min-width: 150px; +} + +.hssm-preview-controls, +.hssm-analytics-controls { + display: flex; + gap: 15px; + align-items: center; + flex-wrap: wrap; +} + +/* ========================================== + Bulk Actions Enhancement + ========================================== */ + +.hssm-bulk-actions { + display: flex; + gap: 15px; + justify-content: center; + align-items: center; + margin-top: 30px; + padding: 25px; + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); + border-radius: 8px; + border: 1px solid #dee2e6; +} + +/* ========================================== + Button Management + ========================================== */ + +.hssm-button-row { + display: grid; + grid-template-columns: 1fr 1fr auto; + gap: 10px; + align-items: center; + margin-bottom: 15px; + padding: 15px; + background: #f8f9fa; + border-radius: 4px; + border: 1px solid #e9ecef; +} + +.hssm-button-text, +.hssm-button-url { + margin-bottom: 0 !important; +} + +.hssm-remove-button { + background-color: #dc3545; + color: #fff; + border-color: #dc3545; + padding: 8px 12px; + font-size: 12px; +} + +.hssm-remove-button:hover { + background-color: #c82333; + border-color: #c82333; + color: #fff; +} + +/* ========================================== + Form Actions + ========================================== */ + +.hssm-form-actions { + display: flex; + gap: 15px; + padding: 20px 30px; + background: #f1f3f4; + border-top: 1px solid #e5e5e5; +} + +.hssm-bulk-actions { + display: flex; + gap: 15px; + justify-content: center; + margin-top: 30px; + padding: 30px; + background: #f8f9fa; + border-radius: 8px; + border: 1px solid #dee2e6; +} + +/* ========================================== + Filters + ========================================== */ + +.hssm-filters { + display: flex; + gap: 15px; + align-items: center; + flex-wrap: wrap; +} + +.hssm-filters .hssm-select { + width: auto; + min-width: 150px; +} + +/* ========================================== + Slides List + ========================================== */ + +.hssm-slides-list { + min-height: 400px; +} + +.hssm-slide-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20px; + margin-bottom: 15px; + background: #fff; + border: 1px solid #dee2e6; + border-radius: 6px; + transition: box-shadow 0.2s ease; +} + +.hssm-slide-item:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.hssm-slide-info { + flex: 1; +} + +.hssm-slide-title { + font-size: 1.2em; + font-weight: 600; + margin-bottom: 5px; + color: #333; +} + +.hssm-slide-meta { + color: #6c757d; + font-size: 0.9em; +} + +.hssm-slide-actions { + display: flex; + gap: 10px; +} + +/* ========================================== + Banner Preview + ========================================== */ + +.hssm-banner-preview { + min-height: 300px; + background: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 6px; + margin-bottom: 20px; + overflow: hidden; +} + +.hssm-preview-controls { + display: flex; + gap: 15px; + align-items: center; + flex-wrap: wrap; +} + +.hssm-preview-actions { + display: flex; + gap: 15px; + justify-content: center; +} + +/* ========================================== + Loading States + ========================================== */ + +.hssm-loading { + position: relative; + opacity: 0.6; + pointer-events: none; +} + +.hssm-loading::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 20px; + height: 20px; + margin: -10px 0 0 -10px; + border: 2px solid #ffffff; + border-radius: 50%; + border-top-color: transparent; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* ========================================== + Responsive Design + ========================================== */ + +@media (max-width: 1200px) { + .hssm-header-content, + .hssm-dashboard-main { + padding-left: 20px; + padding-right: 20px; + } +} + +@media (max-width: 768px) { + .hssm-header-content { + flex-direction: column; + align-items: flex-start; + gap: 20px; + padding: 20px; + } + + .hssm-header-nav { + width: 100%; + justify-content: space-between; + flex-wrap: wrap; + gap: 10px; + } + + .hssm-user-menu { + margin-left: 0; + padding-left: 0; + border-left: none; + border-top: 1px solid rgba(255, 255, 255, 0.2); + padding-top: 15px; + width: 100%; + justify-content: space-between; + } + + .hssm-dashboard-main { + padding: 20px 15px; + } + + .hssm-status-bar { + flex-direction: column; + gap: 15px; + } + + .hssm-nav-tabs { + flex-wrap: wrap; + } + + .hssm-nav-tabs li { + flex: 1; + min-width: 120px; + } + + .hssm-nav-tabs a { + padding: 15px 10px; + font-size: 12px; + } + + .hssm-tab-header { + flex-direction: column; + align-items: flex-start; + gap: 15px; + padding: 20px; + } + + .hssm-tab-actions { + width: 100%; + justify-content: stretch; + } + + .hssm-tab-actions .hssm-btn { + flex: 1; + justify-content: center; + } + + .hssm-form-grid { + grid-template-columns: 1fr; + gap: 20px; + padding: 20px; + } + + .hssm-field-group { + grid-template-columns: 1fr; + } + + .hssm-button-row { + grid-template-columns: 1fr; + gap: 10px; + } + + .hssm-filters, + .hssm-preview-controls, + .hssm-analytics-controls { + flex-direction: column; + align-items: stretch; + } + + .hssm-filters .hssm-select, + .hssm-preview-controls .hssm-select, + .hssm-analytics-controls .hssm-select { + width: 100%; + } + + .hssm-bulk-actions, + .hssm-preview-actions { + flex-direction: column; + } + + .hssm-bulk-actions .hssm-btn, + .hssm-preview-actions .hssm-btn { + width: 100%; + justify-content: center; + } + + .hssm-slide-item { + flex-direction: column; + align-items: stretch; + gap: 15px; + } + + .hssm-slide-actions { + justify-content: center; + gap: 10px; + } + + .hssm-slide-actions .hssm-btn { + flex: 1; + } + + .hssm-analytics-cards { + grid-template-columns: 1fr; + } +} + +@media (max-width: 480px) { + .hssm-site-title { + font-size: 1.4em; + } + + .hssm-site-subtitle { + font-size: 0.8em; + margin-left: 36px; + } + + .hssm-nav-btn { + padding: 8px 12px; + font-size: 12px; + } + + .hssm-nav-tabs a { + padding: 12px 8px; + font-size: 11px; + } + + .hssm-btn { + padding: 10px 15px; + font-size: 13px; + } + + .hssm-btn-large { + padding: 14px 20px; + font-size: 14px; + } +} + +/* ========================================== + Utility Classes + ========================================== */ + +.hssm-hidden { + display: none !important; +} + +.hssm-text-center { + text-align: center; +} + +.hssm-text-right { + text-align: right; +} + +.hssm-mb-0 { + margin-bottom: 0 !important; +} + +.hssm-mt-20 { + margin-top: 20px !important; +} + +.hssm-p-20 { + padding: 20px !important; +} + +/* Slides List Styles */ +.hssm-slides-table { + background: #fff; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.hssm-table-header { + display: grid; + grid-template-columns: 80px 1fr 120px 180px 150px 140px; + gap: 15px; + padding: 20px 25px; + background: #f8f9fa; + border-bottom: 1px solid #e9ecef; + font-weight: 600; + color: #275191; + font-size: 14px; +} + +.hssm-table-body { + max-height: 600px; + overflow-y: auto; +} + +.hssm-slide-row { + display: grid; + grid-template-columns: 80px 1fr 120px 180px 150px 140px; + gap: 15px; + padding: 20px 25px; + border-bottom: 1px solid #f1f3f4; + align-items: center; + transition: background-color 0.2s; +} + +.hssm-slide-row:hover { + background-color: #f8f9fa; +} + +.hssm-slide-row:last-child { + border-bottom: none; +} + +.hssm-slide-thumbnail { + width: 60px; + height: 40px; + background-color: #f1f3f4; + background-size: cover; + background-position: center; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + color: #6c757d; +} + +.hssm-slide-title { + font-weight: 600; + color: #212529; + margin-bottom: 4px; + font-size: 14px; + line-height: 1.3; +} + +.hssm-slide-subtitle { + color: #6c757d; + font-size: 12px; + line-height: 1.3; +} + +.hssm-status-badge { + display: inline-block; + padding: 4px 8px; + border-radius: 12px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.hssm-status-published { + background: #d4edda; + color: #155724; +} + +.hssm-status-draft { + background: #f8d7da; + color: #721c24; +} + +.hssm-status-scheduled { + background: #d1ecf1; + color: #0c5460; +} + +.hssm-status-private { + background: #f3e2f3; + color: #6f2c91; +} + +.hssm-status-pending { + background: #fff3cd; + color: #856404; +} + +.hssm-schedule-info, .hssm-sites-info { + font-size: 12px; + color: #6c757d; + line-height: 1.4; +} + +.hssm-slide-actions { + display: flex; + gap: 5px; + flex-wrap: wrap; +} + +.hssm-slide-actions .hssm-btn { + padding: 6px 8px; + min-width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; +} + +.hssm-slide-actions .dashicons { + font-size: 16px; + width: 16px; + height: 16px; +} + +.hssm-btn-success { + background-color: #28a745; + border-color: #28a745; + color: #fff; +} + +.hssm-btn-success:hover { + background-color: #218838; + border-color: #1e7e34; +} + +/* Empty State */ +.hssm-empty-state { + text-align: center; + padding: 60px 30px; + color: #6c757d; +} + +.hssm-empty-icon { + font-size: 48px; + color: #dee2e6; + margin-bottom: 20px; +} + +.hssm-empty-state h3 { + color: #275191; + margin-bottom: 10px; + font-size: 20px; + font-weight: 600; +} + +.hssm-empty-state p { + margin-bottom: 25px; + font-size: 14px; +} + +/* Responsive adjustments */ +@media (max-width: 1200px) { + .hssm-table-header, + .hssm-slide-row { + grid-template-columns: 80px 1fr 100px 140px 120px 120px; + gap: 10px; + } +} + +@media (max-width: 768px) { + .hssm-table-header, + .hssm-slide-row { + grid-template-columns: 60px 1fr 80px 100px; + gap: 8px; + } + + .hssm-col-dates, + .hssm-col-sites { + display: none; + } + + .hssm-slide-actions { + flex-direction: column; + gap: 3px; + } +} + +/* ========================================== + Edit Mode Styles + ========================================== */ + +.hssm-slide-form[data-editing-slide-id] { + border: 2px solid #ffffff; + background: linear-gradient(135deg, #f8fcff 0%, #e3f2fd 100%); + position: relative; +} + +.hssm-slide-form[data-editing-slide-id]::before { + content: ''; + position: absolute; + top: -2px; + left: -2px; + right: -2px; + bottom: -2px; + background: linear-gradient(45deg, #ffffff, #0073aa); + border-radius: 10px; + z-index: -1; + animation: editPulse 2s infinite; +} + +@keyframes editPulse { + 0%, 100% { opacity: 0.8; } + 50% { opacity: 1; } +} + +.hssm-edit-badge { + background: #ffffff; + color: #ff0000; + padding: 4px 8px; + border-radius: 4px; + font-size: 11px; + font-weight: bold; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-left: 10px; + animation: badgePulse 1.5s infinite; +} + +@keyframes badgePulse { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.05); } +} + +.hssm-cancel-edit { + background: #d63638; + color: white; + padding: 6px 12px; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + font-weight: 500; + transition: all 0.2s ease; +} + +.hssm-cancel-edit:hover { + background: #b32d2e; + transform: translateY(-1px); +} + +.hssm-update-slide { + background: #ffffff !important; + border-color: #ffffff !important; +} + +.hssm-update-slide:hover { + background: #005a87 !important; + border-color: #005a87 !important; +} + +.hssm-cancel-edit-btn { + background: #6c757d !important; + border-color: #6c757d !important; + color: white !important; +} + +.hssm-cancel-edit-btn:hover { + background: #545b62 !important; + border-color: #545b62 !important; +} + +/* ========================================== + Hide All Text Option Styles + ========================================== */ + +.hssm-hide-text-option { + background: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 4px; + padding: 12px 15px; + display: block !important; + margin: 10px 0; + position: relative; + transition: all 0.3s ease; +} + +.hssm-hide-text-option:hover { + background: #e9ecef; + border-color: #adb5bd; +} + +.hssm-hide-text-option input[type="checkbox"] { + margin-right: 10px; +} + +.hssm-hide-text-option input[type="checkbox"]:checked + * { + font-weight: 500; + color: #275191; +} + +.hssm-hide-text-option small { + font-style: italic; + color: #6c757d; + font-weight: normal !important; +} + +/* ========================================== + Field Validation Errors with Speech Bubble + ========================================== */ + +.hssm-field-error { + background-color: #f44336; + color: #fff; + padding: 8px 12px; + border-radius: 4px; + font-size: 13px; + margin-bottom: 8px; + margin-top: 4px; + position: relative; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.hssm-field-error::after { + content: ''; + position: absolute; + bottom: -6px; + left: 20px; + width: 0; + height: 0; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-top: 6px solid #f44336; +} + +.hssm-field-has-error .hssm-input, +.hssm-field-has-error .hssm-textarea, +.hssm-field-has-error .hssm-select { + border-color: #f44336; + box-shadow: 0 0 0 2px rgba(244, 67, 54, 0.1); +} + +.hssm-field-has-error.hssm-checkboxes-compact { + border: 1px solid #f44336; + border-radius: 4px; + padding: 8px; + background-color: rgba(244, 67, 54, 0.05); +} + +/* ========================================== + Datepicker Highlights + ========================================== */ + +.hssm-datepicker-start a { + background: #d4edda !important; + color: #155724 !important; + border: 1px solid #c3e6cb !important; + font-weight: bold !important; +} + +.hssm-datepicker-end a { + background: #f8d7da !important; + color: #721c24 !important; + border: 1px solid #f5c6cb !important; + font-weight: bold !important; +} + +/* Overlap handling */ +.hssm-datepicker-start.hssm-datepicker-end a { + background: linear-gradient(135deg, #d4edda 50%, #f8d7da 50%) !important; + color: #333 !important; +} + +/* ========================================== + Datepicker Custom Theme (Modern Flat) + ========================================== */ +#ui-datepicker-div { + background: #fff; + border: 1px solid #ddd; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + border-radius: 6px; + padding: 10px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; + z-index: 9999 !important; +} + +.ui-datepicker-header { + background: transparent; + border: none; + padding-bottom: 10px; +} + +.ui-datepicker-title { + color: #333; + font-weight: 600; + font-size: 15px; + margin: 0; +} + +.ui-datepicker-prev, .ui-datepicker-next { + cursor: pointer; + background: #f1f3f4; + border-radius: 4px; + border: none; + top: 10px; +} + +.ui-datepicker-prev:hover, .ui-datepicker-next:hover { + background: #e9ecef; + border: none; +} + +.ui-datepicker-prev span, .ui-datepicker-next span { + margin-top: -8px; /* Center icon */ +} + +.ui-datepicker-calendar th { + color: #6c757d; + font-weight: 500; + font-size: 12px; + padding: 5px 0; +} + +.ui-datepicker-calendar td { + padding: 2px; +} + +.ui-datepicker-calendar td a, +.ui-datepicker-calendar td span { + display: block; + text-align: center; + padding: 6px; + border-radius: 4px; + color: #333; + text-decoration: none; + border: 1px solid transparent; + transition: all 0.2s ease; + background: #fff; +} + +.ui-datepicker-calendar td a:hover { + background: #f8f9fa; + border-color: #dee2e6; +} + +/* Current Day Highlight */ +.ui-datepicker-today a { + background: #f0f7ff; + color: #0073aa; + font-weight: 600; +} + +/* Selected Date Highlight */ +.ui-datepicker-current-day a { + background: #0073aa !important; + color: #fff !important; + border-color: #0073aa !important; +} + +/* Custom Highlighting overrides */ +.hssm-datepicker-start a { + background-color: #d4edda !important; /* Light green */ + color: #155724 !important; + border-color: #c3e6cb !important; + font-weight: 600; +} + +.hssm-datepicker-end a { + background-color: #f8d7da !important; /* Light red */ + color: #721c24 !important; + border-color: #f5c6cb !important; + font-weight: 600; +} + +/* Overlap handling override */ +.hssm-datepicker-start.hssm-datepicker-end a { + background: linear-gradient(135deg, #d4edda 50%, #f8d7da 50%) !important; +} \ No newline at end of file diff --git a/assets/css/frontend.css b/assets/css/frontend.css new file mode 100644 index 0000000..aa34b61 --- /dev/null +++ b/assets/css/frontend.css @@ -0,0 +1,61 @@ +/** + * 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/css/slider.css b/assets/css/slider.css new file mode 100644 index 0000000..b228058 --- /dev/null +++ b/assets/css/slider.css @@ -0,0 +1,296 @@ +/** + * HSSM Slider Styles + * Custom classes and styles based on client specifications + * Optimized for Classic Divi environments + */ + +:root { + --hssm-ratio: 42.85714%; /* 900 / 2100 * 100 */ + --hssm-red: #ff0000; + --hssm-bg-overlay: rgba(0, 0, 0, 0.3); + --hssm-shadow: 0.08em 0.08em 0px rgba(0, 0, 0, 0.6); + + /* Default Sizes */ + --hssm-title-size: 32px; + --hssm-subtitle-size: 20px; + --hssm-content-size: 18px; + --hssm-arrow-size: 50px; +} + +/* Force 80% width container */ +.hssm-slider-container { + position: relative !important; + width: 80% !important; + max-width: 1980px !important; + margin: 60px auto !important; + box-sizing: border-box !important; + padding: 0 !important; +} + +.hssm-slider { + position: relative !important; + width: 100% !important; +} + +.hssm-viewport { + position: relative !important; + overflow: hidden !important; + width: 100% !important; + border-radius: 20px !important; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35) !important; + background: #000 !important; +} + +.hssm-track { + display: flex !important; + transition: transform 0.6s cubic-bezier(0.45, 0.05, 0.55, 0.95) !important; + width: 100% !important; + padding: 0 !important; + margin: 0 !important; +} + +.hssm-slide { + flex: 0 0 100% !important; + width: 100% !important; + min-width: 100% !important; + position: relative !important; + margin: 0 !important; + padding: 0 !important; +} + +/* Aspect Ratio Maintainer - Very Aggressive */ +.hssm-slide-inner { + position: relative !important; + width: 100% !important; + height: 0 !important; + padding-bottom: var(--hssm-ratio) !important; + background-size: cover !important; + background-position: center !important; + background-repeat: no-repeat !important; + overflow: hidden !important; + display: block !important; +} + +/* Fallback for modern browsers */ +@supports (aspect-ratio: 21 / 9) { + .hssm-slide-inner { + padding-bottom: 0 !important; + height: auto !important; + aspect-ratio: 2100 / 900 !important; + min-height: 300px !important; /* Safety min-height */ + } +} + +.hssm-slide-content-wrapper { + position: absolute !important; + top: 0 !important; + left: 0 !important; + right: 0 !important; + bottom: 0 !important; + display: flex !important; + flex-direction: column !important; + justify-content: center !important; + align-items: center !important; + padding: 40px 5% !important; + z-index: 5 !important; +} + +.hssm-slide-content { + display: flex !important; + flex-direction: column !important; + align-items: center !important; + text-align: center !important; + width: 100% !important; + margin: 0 auto !important; +} + +/* Spacing between rows */ +.hssm-slide-content > * { + margin-bottom: 30px !important; +} + +.hssm-slide-content > *:last-child { + margin-bottom: 0 !important; +} + +/* Title Styles - 80% width */ +.hssm-slide-title { + font-size: var(--hssm-title-size) !important; + color: #ffffff !important; + font-weight: 600 !important; + line-height: 1.3em !important; + letter-spacing: 1px !important; + max-width: 80% !important; + margin: 0 0 30px 0 !important; +} + +.hssm-slide-title span { + background: var(--hssm-bg-overlay) !important; + padding: 5px 15px !important; + border-radius: 8px !important; + text-shadow: var(--hssm-shadow) !important; + display: inline-block !important; +} + +/* Subtitle Styles - 80% width */ +.hssm-slide-subtitle { + font-size: var(--hssm-subtitle-size) !important; + color: #ffffff !important; + font-weight: 600 !important; + line-height: 1em !important; + letter-spacing: 1px !important; + max-width: 80% !important; + margin: 0 0 30px 0 !important; +} + +.hssm-slide-subtitle span { + background: var(--hssm-bg-overlay) !important; + padding: 5px 12px !important; + border-radius: 6px !important; + text-shadow: var(--hssm-shadow) !important; + display: inline-block !important; +} + +/* Description / Content Styles - 50% width */ +.hssm-slide-description { + font-size: var(--hssm-content-size) !important; + color: #ffffff !important; + font-weight: 500 !important; + line-height: 1.6em !important; + letter-spacing: 1px !important; + max-width: 50% !important; + margin: 0 0 30px 0 !important; +} + +.hssm-slide-description span { + background: var(--hssm-bg-overlay) !important; + padding: 10px 20px !important; + border-radius: 8px !important; + text-shadow: var(--hssm-shadow) !important; + display: inline-block !important; +} + +/* Button Container */ +.hssm-button-container { + display: flex !important; + flex-wrap: wrap !important; + gap: 15px !important; + justify-content: center !important; +} + +.hssm-button { + background: var(--hssm-red) !important; + color: #ffffff !important; + font-size: 16px !important; + font-weight: 600 !important; + padding: 12px 25px !important; + border-radius: 8px !important; + text-decoration: none !important; + transition: transform 0.2s ease, background 0.2s ease !important; + text-transform: uppercase !important; + display: inline-block !important; + border: none !important; + box-shadow: 0 4px 10px rgba(0,0,0,0.2) !important; +} + +.hssm-button:hover { + transform: scale(1.05) !important; + background: #e60000 !important; +} + +/* Arrows */ +.hssm-arrow { + position: absolute !important; + top: 50% !important; + transform: translateY(-50%) !important; + width: var(--hssm-arrow-size) !important; + height: var(--hssm-arrow-size) !important; + background: var(--hssm-red) !important; + border: none !important; + border-radius: 50% !important; + color: white !important; + cursor: pointer !important; + z-index: 10 !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4) !important; + transition: all 0.3s ease !important; + padding: 0 !important; +} + +.hssm-arrow:hover { + background: #d00000 !important; + transform: translateY(-50%) scale(1.1) !important; +} + +.hssm-arrow-prev { left: -25px !important; } +.hssm-arrow-next { right: -25px !important; } + +.hssm-arrow-icon { + display: block !important; + width: 14px !important; + height: 14px !important; + border-top: 4px solid white !important; + border-right: 4px solid white !important; +} + +.hssm-arrow-prev .hssm-arrow-icon { + transform: rotate(-135deg) !important; + margin-left: 6px !important; +} + +.hssm-arrow-next .hssm-arrow-icon { + transform: rotate(45deg) !important; + margin-right: 4px !important; +} + +/* Responsive */ + +/* X-Large Screen Breakpoint: 1921px and up */ +@media (min-width: 1921px) { + :root { + --hssm-title-size: 35.2px; + --hssm-subtitle-size: 22px; + --hssm-content-size: 19.8px; + } +} + +@media (max-width: 1400px) { + .hssm-slider-container { width: 90% !important; } + .hssm-slide-description { max-width: 70% !important; } +} + +@media (max-width: 980px) { + :root { + --hssm-arrow-size: 44px; + } + .hssm-slider-container { width: 92% !important; margin: 40px auto !important; } + .hssm-slide-description { max-width: 85% !important; } + .hssm-arrow-prev { left: -22px !important; } + .hssm-arrow-next { right: -22px !important; } +} + +@media (max-width: 768px) { + .hssm-slider-container { width: 95% !important; margin: 30px auto !important; } + .hssm-viewport { border-radius: 12px !important; } + .hssm-slide-description { max-width: 95% !important; } + .hssm-slide-title, .hssm-slide-subtitle { max-width: 95% !important; } + .hssm-arrow { width: 40px !important; height: 40px !important; } + .hssm-arrow-prev { left: -15px !important; } + .hssm-arrow-next { right: -15px !important; } + + :root { + --hssm-title-size: 24px; + --hssm-subtitle-size: 16px; + --hssm-content-size: 14px; + } +} + +@media (max-width: 480px) { + .hssm-slider-container { width: 100% !important; border-radius: 0 !important; margin: 20px 0 !important; } + .hssm-viewport { border-radius: 0 !important; } + .hssm-arrow { width: 32px !important; height: 32px !important; } + .hssm-arrow-prev { left: 5px !important; } + .hssm-arrow-next { right: 5px !important; } +} diff --git a/assets/js/admin-sortable.js b/assets/js/admin-sortable.js new file mode 100644 index 0000000..f62f59b --- /dev/null +++ b/assets/js/admin-sortable.js @@ -0,0 +1,207 @@ +/** + * Hi-School Slide Manager - Admin Sortable functionality + * + * Handles drag-and-drop ordering of slides in the admin list table + */ + +jQuery(document).ready(function($) { + 'use strict'; + + // Only initialize on the slides list page + if (typeof hssmAjax === 'undefined' || !$('.wp-list-table tbody').length) { + return; + } + + // Initialize sortable functionality + initializeSortable(); + + /** + * Initialize jQuery UI sortable on the slides table + */ + function initializeSortable() { + const $tbody = $('.wp-list-table tbody'); + + // Make the table body sortable + $tbody.sortable({ + handle: '.hssm-drag-handle', + placeholder: 'ui-sortable-placeholder', + helper: function(e, tr) { + var $helper = tr.clone(); + $helper.children().each(function(index) { + $(this).width(tr.children().eq(index).width()); + }); + return $helper; + }, + start: function(e, ui) { + ui.placeholder.height(ui.item.height()); + ui.item.addClass('ui-sortable-helper'); + }, + stop: function(e, ui) { + ui.item.removeClass('ui-sortable-helper'); + updateSlideOrder(); + }, + tolerance: 'pointer', + cursor: 'move', + opacity: 0.8, + distance: 5 + }); + + // Add visual feedback + $tbody.on('mouseenter', '.hssm-drag-handle', function() { + $(this).closest('tr').addClass('hssm-sortable-hover'); + }); + + $tbody.on('mouseleave', '.hssm-drag-handle', function() { + $(this).closest('tr').removeClass('hssm-sortable-hover'); + }); + + // Add CSS for hover effect + if (!$('#hssm-sortable-styles').length) { + $(' + + ' . __( 'General plugin settings.', 'hi-school-slide-manager' ) . '

'; + } + + /** + * Notifications section callback + */ + public function notifications_section_callback() { + echo '

' . __( 'Configure email notifications for new slides.', 'hi-school-slide-manager' ) . '

'; + } + + /** + * API section callback + */ + public function api_section_callback() { + echo '

' . __( 'Manage API keys for companion sites.', 'hi-school-slide-manager' ) . '

'; + } + + /** + * Plugin mode field + */ + public function plugin_mode_field() { + $value = get_option( 'hssm_plugin_mode', 'manager' ); + ?> + +

+ '; + echo '

' . __( 'The REST API base URL of the Manager site.', 'hi-school-slide-manager' ) . '

'; + } + + /** + * Client site slug field + */ + public function client_site_slug_field() { + $value = get_option( 'hssm_client_site_slug', '' ); + ?> + +

+ '; + 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 + */ + public function api_keys_field() { + $api_keys = get_option( 'hssm_api_keys', array() ); + + echo '
'; + + if ( empty( $api_keys ) ) { + echo '

' . __( 'No API keys generated yet.', 'hi-school-slide-manager' ) . '

'; + } else { + echo ''; + echo ''; + echo ''; + + foreach ( $api_keys as $index => $key ) { + echo ''; + echo ''; + echo ''; + echo ''; + } + + echo '
' . __( 'API Key', 'hi-school-slide-manager' ) . '' . __( 'Actions', 'hi-school-slide-manager' ) . '
' . esc_html( $key ) . ''; + echo ' '; + echo ''; + echo ''; + echo '
'; + } + + echo '

'; + echo '

' . __( 'API keys are used by companion sites to authenticate and fetch slides.', 'hi-school-slide-manager' ) . '

'; + + echo '
'; + + // Add JavaScript for copy functionality + ?> + + 15, + 'headers' => array( + 'X-HSSM-API-Key' => $api_key, + 'Accept' => 'application/json', + ), + ); + + $response = wp_remote_get( $full_url, $args ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + $code = wp_remote_retrieve_response_code( $response ); + $body = wp_remote_retrieve_body( $response ); + + if ( 200 !== $code ) { + return new WP_Error( 'api_error', sprintf( __( 'API returned code %d: %s', 'hi-school-slide-manager' ), $code, $body ) ); + } + + $data = json_decode( $body, true ); + if ( ! is_array( $data ) || ! isset( $data['slides'] ) ) { + return new WP_Error( 'invalid_response', __( 'Invalid API response format.', 'hi-school-slide-manager' ) ); + } + + $slides = $data['slides']; + + // Cache the results + set_transient( $cache_key, $slides, $cache_duration ); + + return $slides; + } + + /** + * Clear the slides cache + * + * @since 1.1.0 + */ + public function clear_cache() { + $site_slug = get_option( 'hssm_client_site_slug' ); + if ( ! empty( $site_slug ) ) { + delete_transient( 'hssm_remote_slides_' . $site_slug ); + } + } +} diff --git a/includes/class-hssm-autoloader.php b/includes/class-hssm-autoloader.php new file mode 100644 index 0000000..4310331 --- /dev/null +++ b/includes/class-hssm-autoloader.php @@ -0,0 +1,53 @@ +ID ); + + if ( in_array( $slug, array( 'page-slide-dashboard.php', 'page-slide-preview.php', 'page-slide-confirmation.php' ), true ) ) { + // First try the theme directory + $theme_template = trailingslashit( get_template_directory() ) . $slug; + + if ( file_exists( $theme_template ) ) { + return $theme_template; + } + + // Fallback to our plugin templates + $plugin_template = HSSM_PLUGIN_DIR . 'templates/' . $slug; + if ( file_exists( $plugin_template ) ) { + return $plugin_template; + } + } + } + + return $template; + } + + /** + * Handle frontend page routing + * + * @since 1.0.0 + */ + 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; + } + } + } + + /** + * Check if user has access to the dashboard + * + * @since 1.0.0 + * @param bool $check_upload Whether to also check upload capabilities + * @return bool Access granted + */ + private function check_dashboard_access( $check_upload = false ) { + // Check if user is logged in + if ( ! is_user_logged_in() ) { + error_log( 'Dashboard access denied: User not logged in' ); + return false; + } + + $user_id = get_current_user_id(); + error_log( 'Checking dashboard access for user ID: ' . $user_id ); + + // Check Members plugin capability FIRST + if ( function_exists( 'members_user_can' ) && members_user_can( $user_id, 'hssm_manage_slides' ) ) { + error_log( 'Members plugin check: PASS (hssm_manage_slides)' ); + + if ( $check_upload ) { + $can_upload = current_user_can( 'upload_files' ); + error_log( 'Upload files capability check: ' . ($can_upload ? 'PASS' : 'FAIL') ); + return $can_upload; + } + + return true; + } + + // Fallback to WordPress capabilities - be more permissive + $can_edit = current_user_can( 'edit_posts' ) || current_user_can( 'publish_posts' ) || current_user_can( 'manage_options' ); + error_log( 'WordPress capability check (edit_posts/publish_posts/manage_options): ' . ($can_edit ? 'PASS' : 'FAIL') ); + + if ( $check_upload ) { + $can_upload = current_user_can( 'upload_files' ); + error_log( 'Upload files capability check: ' . ($can_upload ? 'PASS' : 'FAIL') ); + return $can_edit && $can_upload; + } + + return $can_edit; + } + + /** + * Redirect to login page + * + * @since 1.0.0 + */ + private function redirect_to_login() { + $login_url = wp_login_url( get_permalink() ); + wp_redirect( $login_url ); + exit; + } + + /** + * Enqueue frontend assets + * + * @since 1.0.0 + */ + public function enqueue_assets() { + global $post; + + // Only load on our dashboard or preview pages + if ( ! is_page() ) { + return; + } + + $template_slug = get_page_template_slug( $post->ID ); + $is_dashboard = ( $template_slug === 'page-slide-dashboard.php' ); + $is_preview = ( $template_slug === 'page-slide-preview.php' ); + + if ( ! $is_dashboard && ! $is_preview ) { + return; + } + + // Check access (preview page may be public, but dashboard requires auth) + if ( $is_dashboard && ! $this->check_dashboard_access() ) { + return; + } + + // Enqueue styles (both pages need dashboard CSS) + wp_enqueue_style( + 'hssm-dashboard', + HSSM_PLUGIN_URL . 'assets/css/dashboard.css', + array(), + HSSM_VERSION + ); + + // Dashboard page needs full JS; preview page only needs styles + if ( $is_dashboard ) { + // Enqueue scripts + wp_enqueue_script( 'jquery-ui-sortable' ); + wp_enqueue_media(); // For image uploads + + wp_enqueue_script( + 'hssm-dashboard', + HSSM_PLUGIN_URL . 'assets/js/dashboard.js', + array( 'jquery', 'jquery-ui-sortable' ), + HSSM_VERSION, + true + ); + + // Localize script + wp_localize_script( 'hssm-dashboard', 'hssmDashboard', array( + 'ajaxurl' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'hssm_dashboard_nonce' ), + 'strings' => array( + 'save_success' => __( 'Slide saved successfully!', 'hi-school-slide-manager' ), + 'save_error' => __( 'Error saving slide. Please try again.', 'hi-school-slide-manager' ), + 'delete_confirm' => __( 'Are you sure you want to delete this slide?', 'hi-school-slide-manager' ), + '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' ), + ), + ) ); + + // Add sites data for JavaScript + wp_localize_script( 'hssm-dashboard', 'hssmSites', $this->get_available_sites() ); + } + + // Preview page specific JS + if ( $is_preview ) { + wp_enqueue_script( 'jquery-ui-datepicker' ); + // Enqueue jQuery UI CSS (using a reliable CDN or local if available, + // but WP doesn't ship with a default jQuery UI theme CSS) + wp_enqueue_style( + 'jquery-ui-style', + 'https://code.jquery.com/ui/1.13.2/themes/smoothness/jquery-ui.css', + array(), + '1.13.2' + ); + + wp_enqueue_script( + 'hssm-preview', + HSSM_PLUGIN_URL . 'assets/js/preview.js', + array( 'jquery', 'jquery-ui-datepicker' ), + HSSM_VERSION, + true + ); + + // Localize script for preview page too + wp_localize_script( 'hssm-preview', 'hssmPreview', array( + 'ajaxurl' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'hssm_dashboard_nonce' ), + 'strings' => array( + 'approve_confirm' => __( 'Are you sure you want to approve this slide?', 'hi-school-slide-manager' ), + 'approve_success' => __( 'Slide approved successfully!', 'hi-school-slide-manager' ), + 'approve_error' => __( 'Error approving slide. Please try again.', 'hi-school-slide-manager' ), + ), + ) ); + } + } + + /** + * AJAX handler for image uploads + * + * @since 1.0.0 + */ + public function ajax_upload_image() { + // Verify nonce and permissions + if ( ! wp_verify_nonce( $_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() { + // Verify nonce and permissions + if ( ! wp_verify_nonce( $_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( + 'key' => '_hssm_start_date', + 'value' => $date, + 'compare' => '<=', + ), + 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() { + // Verify nonce and permissions + if ( ! wp_verify_nonce( $_POST['nonce'], 'hssm_dashboard_nonce' ) || ! $this->check_dashboard_access() ) { + wp_send_json_error( array( 'message' => 'Unauthorized' ) ); + } + + // 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 + * + * @since 1.0.0 + * @param array $data Raw form data + * @return array Sanitized data + */ + private function sanitize_slide_data( $data ) { + error_log( 'Raw form data received: ' . print_r( $data, true ) ); + + // Handle target_sites - it might come as an array or need to be extracted + $target_sites = array(); + if ( isset( $data['target_sites'] ) ) { + if ( is_array( $data['target_sites'] ) ) { + $target_sites = $data['target_sites']; + } else { + // If it's a single value, make it an array + $target_sites = array( $data['target_sites'] ); + } + } + + // Handle buttons - decode JSON if it's a string + $buttons = $data['buttons'] ?? array(); + if ( is_string( $buttons ) ) { + $decoded = json_decode( stripslashes( $buttons ), true ); + if ( is_array( $decoded ) ) { + $buttons = $decoded; + } + } + + $sanitized = array( + 'title' => sanitize_text_field( $data['title'] ?? '' ), + 'subtitle' => sanitize_text_field( $data['subtitle'] ?? '' ), + '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', + 'auto_publish' => ! empty( $data['auto_publish'] ), + 'auto_unpublish' => ! empty( $data['auto_unpublish'] ), + 'order' => intval( $data['order'] ?? 0 ), + 'featured_image_id' => intval( $data['featured_image_id'] ?? 0 ), + 'hide_all_text' => ! empty( $data['hide_all_text'] ) && $data['hide_all_text'] === '1', + ); + + error_log( 'Sanitized slide data: ' . print_r( $sanitized, true ) ); + error_log( 'Target sites count: ' . count( $sanitized['target_sites'] ) ); + return $sanitized; + } + + /** + * Sanitize buttons array + * + * @since 1.0.0 + * @param array $buttons Raw buttons data + * @return array Sanitized buttons + */ + private function sanitize_buttons( $buttons ) { + $sanitized = array(); + + if ( is_array( $buttons ) ) { + foreach ( $buttons as $button ) { + if ( ! empty( $button['text'] ) || ! empty( $button['url'] ) ) { + $sanitized[] = array( + 'text' => sanitize_text_field( $button['text'] ?? 'Learn More' ), + 'url' => esc_url_raw( $button['url'] ?? '' ), + ); + } + } + } + + // Ensure at least one button + if ( empty( $sanitized ) ) { + $sanitized[] = array( 'text' => 'Learn More', 'url' => '' ); + } + + return array_slice( $sanitized, 0, 5 ); // Max 5 buttons + } + + /** + * Validate slide data + * + * @since 1.0.0 + * @param array $data Sanitized slide data + * @return bool Is valid + */ + private function validate_slide_data( $data ) { + error_log( 'Validating slide data: ' . print_r( $data, true ) ); + + // Required fields + if ( empty( $data['title'] ) ) { + error_log( 'Validation failed: Missing title' ); + return false; + } + + // Validate target sites + if ( ! empty( $data['target_sites'] ) && is_array( $data['target_sites'] ) ) { + // Check if each site slug exists as a taxonomy term + foreach ( $data['target_sites'] as $site_slug ) { + $term = get_term_by( 'slug', $site_slug, 'hssm_site' ); + if ( ! $term || is_wp_error( $term ) ) { + error_log( 'Validation failed: Invalid site slug (not a taxonomy term): ' . $site_slug ); + return false; + } + } + } else { + error_log( 'Validation failed: No target sites selected or target_sites is not an array' ); + error_log( 'target_sites value: ' . print_r( $data['target_sites'] ?? 'NOT SET', true ) ); + return false; + } + + error_log( 'Validation passed' ); + return true; + } + + /** + * Save slide meta data + * + * @since 1.0.0 + * @param int $slide_id Slide ID + * @param array $data Slide data + */ + private function save_slide_meta( $slide_id, $data ) { + $meta_fields = array( + '_hssm_subtitle' => $data['subtitle'], + '_hssm_buttons' => $data['buttons'], + '_hssm_start_date' => $data['start_date'], + '_hssm_end_date' => $data['end_date'], + '_hssm_auto_publish' => $data['auto_publish'] ? '1' : '0', + '_hssm_auto_unpublish' => $data['auto_unpublish'] ? '1' : '0', + '_hssm_order' => $data['order'], + '_hssm_hide_all_text' => $data['hide_all_text'] ? '1' : '0', + ); + + foreach ( $meta_fields as $key => $value ) { + update_post_meta( $slide_id, $key, $value ); + } + } + + /** + * Save slide taxonomies + * + * @since 1.0.0 + * @param int $slide_id Slide ID + * @param array $data Slide data + */ + private function save_slide_taxonomies( $slide_id, $data ) { + // Set target sites + if ( ! empty( $data['target_sites'] ) && is_array( $data['target_sites'] ) ) { + error_log( 'Saving target sites for slide ' . $slide_id . ': ' . print_r( $data['target_sites'], true ) ); + + // Ensure we're using term slugs, not IDs + $term_slugs = array(); + foreach ( $data['target_sites'] as $site_slug ) { + // Check if term exists, if not create it + $term = get_term_by( 'slug', $site_slug, 'hssm_site' ); + if ( ! $term || is_wp_error( $term ) ) { + // Try to create the term if it doesn't exist + $term_result = wp_insert_term( $site_slug, 'hssm_site', array( 'slug' => $site_slug ) ); + if ( ! is_wp_error( $term_result ) ) { + $term_slugs[] = $site_slug; + } + } else { + $term_slugs[] = $site_slug; + } + } + + if ( ! empty( $term_slugs ) ) { + $result = wp_set_post_terms( $slide_id, $term_slugs, 'hssm_site' ); + error_log( 'wp_set_post_terms result: ' . print_r( $result, true ) ); + } else { + error_log( 'No valid term slugs to save' ); + } + } else { + error_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' ); + } + } + + /** + * Format slide data for frontend display + * + * @since 1.0.0 + * @param int $slide_id Slide ID + * @return array Formatted slide data + */ + private function format_slide_for_frontend( $slide_id ) { + $slide = get_post( $slide_id ); + + 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, '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, + ); + } + + /** + * AJAX handler for getting slides + * + * @since 1.0.0 + */ + public function ajax_get_slides() { + // Get pagination parameters + $page = isset( $_POST['page'] ) ? intval( $_POST['page'] ) : 1; + $per_page = isset( $_POST['per_page'] ) ? intval( $_POST['per_page'] ) : 25; + $offset = ( $page - 1 ) * $per_page; + + // Get filter parameters + $status_filter = isset( $_POST['status_filter'] ) ? sanitize_text_field( $_POST['status_filter'] ) : ''; + $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; + + // Build query args + $args = array( + 'post_type' => 'hssm_slide', + 'post_status' => array( 'publish', 'draft', 'pending', 'future' ), // Include all statuses + 'posts_per_page' => $per_page, + 'offset' => $offset, + 'orderby' => 'date', + 'order' => 'DESC', + 'tax_query' => array(), + 'date_query' => array() + ); + + // Add status filter + if ( ! empty( $status_filter ) ) { + $args['post_status'] = array( $status_filter ); + } + + // Add site filter using taxonomy query + if ( ! empty( $site_filter ) ) { + // Get site term by slug + $site_term = get_term_by( 'slug', $site_filter, 'hssm_site' ); + if ( $site_term && ! is_wp_error( $site_term ) ) { + $args['tax_query'][] = array( + 'taxonomy' => 'hssm_site', + 'field' => 'term_id', + 'terms' => $site_term->term_id, + ); + } + } + + // 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 + $date_parts = explode( '-', $month_filter ); + if ( count( $date_parts ) === 2 && strlen( $date_parts[0] ) === 4 && strlen( $date_parts[1] ) === 2 ) { + $year = intval( $date_parts[0] ); + $month = intval( $date_parts[1] ); + if ( $year > 0 && $month >= 1 && $month <= 12 ) { + $args['date_query'][] = array( + 'year' => $year, + 'month' => $month, + ); + } + } + } elseif ( $year_filter > 0 ) { + // If only year is specified + $args['date_query'][] = array( + 'year' => $year_filter, + ); + } + + // Set relation if we have multiple taxonomy queries + if ( count( $args['tax_query'] ) > 1 ) { + $args['tax_query']['relation'] = 'AND'; + } elseif ( empty( $args['tax_query'] ) ) { + // Remove empty tax_query to avoid unnecessary query conditions + unset( $args['tax_query'] ); + } + + // Remove empty date_query if no date filters were applied + if ( empty( $args['date_query'] ) ) { + unset( $args['date_query'] ); + } + + // Get total count for pagination + $count_args = $args; + $count_args['posts_per_page'] = -1; + $count_args['fields'] = 'ids'; + unset( $count_args['offset'] ); + $total_slides = count( get_posts( $count_args ) ); + + // Get slides + $slides = get_posts( $args ); + $formatted_slides = array(); + + foreach ( $slides as $slide ) { + $slide_meta = get_post_meta( $slide->ID ); + + // Get selected sites from taxonomy terms + $site_terms = wp_get_post_terms( $slide->ID, 'hssm_site', array( 'fields' => 'all' ) ); + $site_names = array(); + if ( ! empty( $site_terms ) && ! is_wp_error( $site_terms ) ) { + foreach ( $site_terms as $site_term ) { + $site_names[] = $site_term->name; + } + } + + // Get featured image + $featured_image_url = get_the_post_thumbnail_url( $slide->ID, 'thumbnail' ); + + // Get schedule info + $start_date = isset( $slide_meta['_hssm_start_date'][0] ) ? $slide_meta['_hssm_start_date'][0] : ''; + $end_date = isset( $slide_meta['_hssm_end_date'][0] ) ? $slide_meta['_hssm_end_date'][0] : ''; + $auto_publish = isset( $slide_meta['_hssm_auto_publish'][0] ) ? $slide_meta['_hssm_auto_publish'][0] : ''; + $auto_unpublish = isset( $slide_meta['_hssm_auto_unpublish'][0] ) ? $slide_meta['_hssm_auto_unpublish'][0] : ''; + + // Format status for display + $status_display = ucfirst( $slide->post_status ); + if ( $slide->post_status === 'pending' ) { + $status_display = 'Pending Approval'; + } elseif ( $slide->post_status === 'future' ) { + $status_display = 'Scheduled'; + } + + $formatted_slides[] = array( + 'id' => $slide->ID, + 'title' => $slide->post_title, + 'subtitle' => isset( $slide_meta['_hssm_subtitle'][0] ) ? $slide_meta['_hssm_subtitle'][0] : '', + 'content' => $slide->post_content, + 'status' => $slide->post_status, + 'status_display' => $status_display, + 'sites' => $site_names, + 'client' => $this->get_slide_client_name( $slide->ID ), + 'start_date' => $start_date, + 'end_date' => $end_date, + 'auto_publish' => $auto_publish, + 'auto_unpublish' => $auto_unpublish, + 'featured_image_url' => $featured_image_url, + 'created' => $slide->post_date, + 'modified' => $slide->post_modified, + 'buttons' => isset( $slide_meta['_hssm_buttons'][0] ) ? maybe_unserialize( $slide_meta['_hssm_buttons'][0] ) : array(), + 'hide_all_text' => isset( $slide_meta['_hssm_hide_all_text'][0] ) && $slide_meta['_hssm_hide_all_text'][0] === '1' + ); + } + + wp_send_json_success( array( + 'slides' => $formatted_slides, + 'total' => $total_slides, + 'page' => $page, + 'per_page' => $per_page, + 'total_pages' => ceil( $total_slides / $per_page ), + 'has_more' => ( $page * $per_page ) < $total_slides + ) ); + } + + /** + * AJAX handler for approving slides + * + * @since 1.0.0 + */ + public function ajax_approve_slide() { + error_log( 'Approve slide AJAX called' ); + + if ( empty( $_POST['slide_id'] ) ) { + wp_send_json_error( array( 'message' => 'Slide ID is required' ) ); + return; + } + + $slide_id = intval( $_POST['slide_id'] ); + $approve_action = isset( $_POST['approve_action'] ) ? sanitize_text_field( $_POST['approve_action'] ) : 'publish'; + + // Check if slide exists + $slide = get_post( $slide_id ); + if ( ! $slide || $slide->post_type !== 'hssm_slide' ) { + wp_send_json_error( array( 'message' => 'Slide not found' ) ); + return; + } + + // Check permissions + if ( ! current_user_can( 'edit_posts' ) ) { + wp_send_json_error( array( 'message' => 'Permission denied' ) ); + return; + } + + $slide_meta = get_post_meta( $slide_id ); + $start_date = isset( $slide_meta['_hssm_start_date'][0] ) ? $slide_meta['_hssm_start_date'][0] : ''; + + if ( $approve_action === 'schedule' && ! empty( $start_date ) ) { + // Schedule for future publication + $post_data = array( + 'ID' => $slide_id, + 'post_status' => 'future', + 'post_date' => $start_date . ' 00:00:00' + ); + } else { + // Publish immediately + $post_data = array( + 'ID' => $slide_id, + 'post_status' => 'publish' + ); + } + + $result = wp_update_post( $post_data ); + + if ( $result ) { + error_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 ); + + wp_send_json_success( array( + 'message' => 'Slide approved successfully', + 'new_status' => $post_data['post_status'] + ) ); + } else { + wp_send_json_error( array( 'message' => 'Failed to approve slide' ) ); + } + } + + /** + * Test AJAX handler to verify AJAX is working + * + * @since 1.0.0 + */ + public function ajax_test() { + error_log( 'AJAX TEST HANDLER CALLED - SUCCESS!' ); + wp_send_json_success( array( 'message' => 'AJAX is working!' ) ); + } + + /** + * AJAX handler for deleting slides + * + * @since 1.0.0 + */ + public function ajax_delete_slide() { + if ( empty( $_POST['slide_id'] ) ) { + wp_send_json_error( array( 'message' => 'Slide ID is required' ) ); + } + + $slide_id = intval( $_POST['slide_id'] ); + + // Check if slide exists and user can delete it + $slide = get_post( $slide_id ); + if ( ! $slide || $slide->post_type !== 'hssm_slide' ) { + wp_send_json_error( array( 'message' => 'Slide not found' ) ); + } + + if ( ! current_user_can( 'delete_post', $slide_id ) ) { + wp_send_json_error( array( 'message' => 'Insufficient permissions' ) ); + } + + // Delete the slide + $result = wp_delete_post( $slide_id, true ); + + if ( $result ) { + wp_send_json_success( array( 'message' => 'Slide deleted successfully!' ) ); + } else { + wp_send_json_error( array( 'message' => 'Failed to delete slide' ) ); + } + } + + /** + * AJAX handler for updating slides + * + * @since 1.0.0 + */ + public function ajax_update_slide() { + if ( empty( $_POST['slide_id'] ) ) { + wp_send_json_error( array( 'message' => 'Slide ID is required' ) ); + } + + $slide_id = intval( $_POST['slide_id'] ); + + // Check if slide exists and user can edit it + $slide = get_post( $slide_id ); + if ( ! $slide || $slide->post_type !== 'hssm_slide' ) { + wp_send_json_error( array( 'message' => 'Slide not found' ) ); + } + + if ( ! current_user_can( 'edit_post', $slide_id ) ) { + wp_send_json_error( array( 'message' => 'Insufficient permissions' ) ); + } + + // 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' ) ); + } + + // Get post status from request (default to draft if not specified) + $post_status = isset( $_POST['post_status'] ) ? sanitize_text_field( $_POST['post_status'] ) : 'draft'; + + // Validate post status + $allowed_statuses = array( 'draft', 'pending', 'publish', 'future' ); + if ( ! in_array( $post_status, $allowed_statuses, true ) ) { + $post_status = 'draft'; + } + + // Update the slide post + $post_args = array( + 'ID' => $slide_id, + 'post_title' => $slide_data['title'], + 'post_content' => $slide_data['content'], + 'post_excerpt' => $slide_data['content'], + 'post_status' => $post_status, + ); + + // If scheduling for future and start date is set, use that as post_date + if ( $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'] ); + } + + $result = wp_update_post( $post_args ); + + if ( is_wp_error( $result ) ) { + wp_send_json_error( array( 'message' => 'Failed to update slide' ) ); + } + + // Update meta data + $this->save_slide_meta( $slide_id, $slide_data ); + + // Update taxonomies + $this->save_slide_taxonomies( $slide_id, $slide_data ); + + // Update featured image if provided + if ( ! empty( $slide_data['featured_image_id'] ) ) { + set_post_thumbnail( $slide_id, $slide_data['featured_image_id'] ); + } + + // Send approval notification if status is pending + if ( $post_status === 'pending' ) { + // Trigger 1 (variant): Notification to Editor when updated slide is scheduled for approval + $notifications = new HSSM_Notifications(); + $notifications->notify_editor_slide_created( $slide_id, 'scheduled' ); + } + + $status_message = ( $post_status === 'pending' ) + ? 'Slide updated and scheduled for approval!' + : 'Slide updated successfully!'; + + wp_send_json_success( array( + 'message' => $status_message, + 'slide_data' => $this->format_slide_for_frontend( $slide_id ) + ) ); + } + + /** + * AJAX handler for scheduling a slide for approval + * + * @since 1.0.0 + */ + public function ajax_schedule_slide() { + error_log( 'Schedule slide AJAX called' ); + error_log( 'POST data: ' . print_r( $_POST, true ) ); + + // Check nonce + if ( ! check_ajax_referer( 'hssm_dashboard_nonce', 'nonce', false ) ) { + error_log( 'Nonce verification failed' ); + wp_send_json_error( array( 'message' => 'Security check failed' ) ); + } + + // Check user permissions + if ( ! current_user_can( 'edit_posts' ) ) { + error_log( 'User permissions check failed' ); + wp_send_json_error( array( 'message' => 'Insufficient permissions' ) ); + } + + // Sanitize and validate input + $slide_data = $this->sanitize_slide_data( $_POST ); + + if ( ! $this->validate_slide_data( $slide_data ) ) { + error_log( 'Slide data validation failed' ); + wp_send_json_error( array( 'message' => 'Invalid slide data' ) ); + } + + // Create slide with 'pending' status for approval + $post_args = array( + 'post_title' => $slide_data['title'], + 'post_content' => $slide_data['content'], + 'post_excerpt' => $slide_data['content'], + 'post_type' => 'hssm_slide', + 'post_status' => 'pending', + 'meta_input' => array( + '_hssm_scheduled_for_approval' => current_time( 'timestamp' ), + '_hssm_submitted_by' => get_current_user_id(), + ) + ); + + $slide_id = wp_insert_post( $post_args ); + + if ( is_wp_error( $slide_id ) || ! $slide_id ) { + wp_send_json_error( array( 'message' => 'Failed to schedule slide' ) ); + } + + // Save meta data and taxonomies + $this->save_slide_meta( $slide_id, $slide_data ); + $this->save_slide_taxonomies( $slide_id, $slide_data ); + + // Set featured image + if ( ! empty( $slide_data['featured_image_id'] ) ) { + set_post_thumbnail( $slide_id, $slide_data['featured_image_id'] ); + } + + // Send approval email notifications + // Trigger 1 (variant): Notification to Editor when slide is scheduled for approval + $notifications = new HSSM_Notifications(); + $notifications->notify_editor_slide_created( $slide_id, 'scheduled' ); + + wp_send_json_success( array( + 'message' => 'Slide scheduled for approval. Email notifications sent.', + 'slide_id' => $slide_id + ) ); + } + + /** + * AJAX handler for publishing a slide immediately + * + * @since 1.0.0 + */ + public function ajax_publish_slide() { + // Check nonce + if ( ! check_ajax_referer( 'hssm_dashboard_nonce', 'nonce', false ) ) { + wp_send_json_error( array( 'message' => 'Security check failed' ) ); + } + + // Check user permissions + if ( ! current_user_can( 'publish_posts' ) ) { + wp_send_json_error( array( 'message' => 'Insufficient permissions' ) ); + } + + // 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 slide with 'publish' status + $post_args = array( + 'post_title' => $slide_data['title'], + 'post_content' => $slide_data['content'], + 'post_excerpt' => $slide_data['content'], + 'post_type' => 'hssm_slide', + 'post_status' => 'publish', + 'meta_input' => array( + '_hssm_published_immediately' => current_time( 'timestamp' ), + '_hssm_published_by' => get_current_user_id(), + ) + ); + + $slide_id = wp_insert_post( $post_args ); + + if ( is_wp_error( $slide_id ) || ! $slide_id ) { + wp_send_json_error( array( 'message' => 'Failed to publish slide' ) ); + } + + // Save meta data and taxonomies + $this->save_slide_meta( $slide_id, $slide_data ); + $this->save_slide_taxonomies( $slide_id, $slide_data ); + + // Set featured image + if ( ! empty( $slide_data['featured_image_id'] ) ) { + set_post_thumbnail( $slide_id, $slide_data['featured_image_id'] ); + } + + // Send approval/notification emails + // Trigger 1: Notification to Editor when slide is scheduled or published + $notifications = new HSSM_Notifications(); + $notifications->notify_editor_slide_created( $slide_id, 'published' ); + + wp_send_json_success( array( + 'message' => 'Slide published successfully. Approval notifications sent.', + 'slide_id' => $slide_id + ) ); + } + + /** + * Send email notifications for slide approval workflow + * + * @since 1.0.0 + * @param int $slide_id The slide ID + * @param string $action The action taken ('scheduled' or 'published') + */ + private function send_approval_notification( $slide_id, $action ) { + // Use the new Notification class + $notifications = new HSSM_Notifications(); + $notifications->notify_editor_slide_created( $slide_id, $action ); + } + + /** + * Get formatted text of target sites for a slide + * + * @since 1.0.0 + * @param int $slide_id The slide ID + * @return string Formatted target sites text + */ + private function get_slide_target_sites_text( $slide_id ) { + $target_sites = get_post_meta( $slide_id, '_hssm_target_sites', true ); + + if ( empty( $target_sites ) || ! is_array( $target_sites ) ) { + return 'None selected'; + } + + $site_names = array(); + foreach ( $target_sites as $site_id ) { + // Get site name - this could be enhanced to use actual site data + $site_names[] = ucfirst( str_replace( '-', ' ', $site_id ) ); + } + + 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 + * + * @since 1.0.0 + * @return array Available sites data + */ + private function get_available_sites() { + // Use centralized method from HSSM_Main if available + if ( class_exists( 'HSSM_Main' ) && method_exists( 'HSSM_Main', 'get_companion_sites' ) ) { + $sites = HSSM_Main::get_companion_sites(); + $formatted_sites = array(); + + foreach ( $sites as $site ) { + $formatted_sites[] = array( + 'id' => $site['slug'], + 'name' => $site['name'] + ); + } + + return $formatted_sites; + } + + // Fallback for safety + return array( + array( + 'id' => 'hi-school', + 'name' => 'Hi-School Pharmacy' + ), + array( + 'id' => 'one-stop', + 'name' => 'One Stop Hardware' + ), + array( + 'id' => 'ace', + 'name' => 'Ace Hardware' + ) + ); + } + + /** + * AJAX handler for duplicating slides + * + * @since 1.0.0 + */ + public function ajax_duplicate_slide() { + if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'hssm_dashboard_nonce' ) || ! $this->check_dashboard_access() ) { + wp_send_json_error( array( 'message' => 'Unauthorized' ) ); + } + + if ( empty( $_POST['slide_id'] ) ) { + wp_send_json_error( array( 'message' => 'Slide ID is required' ) ); + } + + $original_id = intval( $_POST['slide_id'] ); + $original_post = get_post( $original_id ); + + if ( ! $original_post || $original_post->post_type !== 'hssm_slide' ) { + wp_send_json_error( array( 'message' => 'Slide not found' ) ); + } + + // Create new post + $new_post_data = array( + 'post_title' => $original_post->post_title . ' (Copy)', + 'post_content' => $original_post->post_content, + 'post_status' => 'draft', + 'post_type' => 'hssm_slide', + 'post_author' => get_current_user_id(), + ); + + $new_id = wp_insert_post( $new_post_data ); + + if ( is_wp_error( $new_id ) ) { + wp_send_json_error( array( 'message' => 'Failed to duplicate slide' ) ); + } + + // Copy all meta fields + $meta_fields = get_post_meta( $original_id ); + foreach ( $meta_fields as $key => $values ) { + foreach ( $values as $value ) { + add_post_meta( $new_id, $key, maybe_unserialize( $value ) ); + } + } + + wp_send_json_success( array( 'message' => 'Slide duplicated successfully', 'new_id' => $new_id ) ); + } + + /** + * AJAX handler for toggling slide status + * + * @since 1.0.0 + */ + public function ajax_toggle_slide_status() { + if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'hssm_dashboard_nonce' ) || ! $this->check_dashboard_access() ) { + wp_send_json_error( array( 'message' => 'Unauthorized' ) ); + } + + if ( empty( $_POST['slide_id'] ) ) { + wp_send_json_error( array( 'message' => 'Slide ID is required' ) ); + } + + $slide_id = intval( $_POST['slide_id'] ); + $post = get_post( $slide_id ); + + if ( ! $post || $post->post_type !== 'hssm_slide' ) { + wp_send_json_error( array( 'message' => 'Slide not found' ) ); + } + + // Toggle status + $new_status = ( $post->post_status === 'publish' ) ? 'draft' : 'publish'; + + $updated = wp_update_post( array( + 'ID' => $slide_id, + 'post_status' => $new_status + ) ); + + 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', + 'new_status' => $new_status + ) ); + } + + /** + * AJAX handler to get notification settings + */ + public function ajax_get_notification_settings() { + if ( ! check_ajax_referer( 'hssm_dashboard_nonce', 'nonce', false ) || ! $this->check_dashboard_access() ) { + wp_send_json_error( array( 'message' => 'Unauthorized' ) ); + } + + $settings = HSSM_Notifications::get_settings(); + wp_send_json_success( $settings ); + } + + /** + * AJAX handler to save notification settings + */ + public function ajax_save_notification_settings() { + if ( ! check_ajax_referer( 'hssm_dashboard_nonce', 'nonce', false ) || ! $this->check_dashboard_access() ) { + wp_send_json_error( array( 'message' => 'Unauthorized' ) ); + } + + // Pass raw values to the class method which handles sanitization + $settings = array( + 'editor_emails' => isset($_POST['editor_emails']) ? wp_unslash( $_POST['editor_emails'] ) : '', + 'itc_emails' => isset($_POST['itc_emails']) ? wp_unslash( $_POST['itc_emails'] ) : '' + ); + + if ( HSSM_Notifications::save_settings( $settings ) ) { + wp_send_json_success( array( 'message' => 'Settings saved' ) ); + } else { + // It might fail if no changes were made, which is fine, but we'll return success anyway or handle accordingly + wp_send_json_success( array( 'message' => 'Settings saved (or no changes)' ) ); + } + } + + /** + * Filter Slides Query on Preview Page + * We use pre_get_posts because it's more reliable at catching all queries. + * + * @since 1.0.0 + * @param WP_Query $query The query object. + */ + public function filter_preview_slides_query( $query ) { + if ( is_admin() ) { + return; + } + + // --- DEBUG --- + // error_log('HSSM Query Filter Check: is_main=' . ($query->is_main_query()?'Y':'N') . ' post_type=' . print_r($query->get('post_type'), true)); + + // Only on preview page + $obj_id = get_queried_object_id(); + if ( ! $obj_id ) { + global $post; + $obj_id = $post->ID ?? 0; + } + + $template = get_page_template_slug( $obj_id ); + if ( $template !== 'page-slide-preview.php' ) { + return; + } + + // Don't filter the main query (the page itself) + if ( $query->is_main_query() ) { + return; + } + + // Check if this query is intended for our slides. + // We look for queries that either already target hssm_slide + // OR queries from Divi loops that might need redirection. + $post_type = $query->get( 'post_type' ); + + // If it's a menu or something else we recognize, bail + if ( $query->get( 'post_type' ) === 'nav_menu_item' ) { + return; + } + + // Force the query to slides for the preview + $query->set( 'post_type', 'hssm_slide' ); + $query->set( 'posts_per_page', -1 ); + $query->set( 'post_status', array( 'publish', 'future', 'pending' ) ); + + // Filter parameters from URL + $date_param = isset( $_GET['hssm_date'] ) ? sanitize_text_field( $_GET['hssm_date'] ) : current_time( 'Y-m-d' ); + $site_param = isset( $_GET['hssm_site'] ) ? sanitize_text_field( $_GET['hssm_site'] ) : ''; + + // Apply Meta Query for Date + $meta_query = array( + 'relation' => 'AND', + array( + 'key' => '_hssm_start_date', + 'value' => $date_param, + 'compare' => '<=', + ), + array( + 'relation' => 'OR', + array( + 'key' => '_hssm_end_date', + 'value' => $date_param, + 'compare' => '>=', + ), + array( + 'key' => '_hssm_end_date', + 'compare' => 'NOT EXISTS', + ), + array( + 'key' => '_hssm_end_date', + 'value' => '', + 'compare' => '=', + ), + ), + ); + $query->set( 'meta_query', $meta_query ); + + // Apply Tax Query for Site + if ( ! empty( $site_param ) ) { + $tax_query = array( + array( + 'taxonomy' => 'hssm_site', + 'field' => 'slug', + 'terms' => $site_param, + ) + ); + $query->set( 'tax_query', $tax_query ); + } + + // Ensure ordering matches + $query->set( 'orderby', 'meta_value_num' ); + $query->set( 'meta_key', '_hssm_order' ); + $query->set( 'order', 'ASC' ); + $query->set( 'ignore_sticky_posts', true ); + } + +} \ No newline at end of file diff --git a/includes/class-hssm-main.php b/includes/class-hssm-main.php new file mode 100644 index 0000000..e08c2a1 --- /dev/null +++ b/includes/class-hssm-main.php @@ -0,0 +1,520 @@ +init_components(); + + // Hook into WordPress + $this->init_hooks(); + + // Load text domain for translations + $this->load_textdomain(); + } + + /** + * Initialize plugin components + * + * @since 1.0.0 + */ + private function init_components() { + // Manually require class files (since they don't follow exact autoloader pattern) + require_once HSSM_PLUGIN_DIR . 'includes/class-hssm-slides-cpt.php'; + require_once HSSM_PLUGIN_DIR . 'includes/class-hssm-rest-api.php'; + require_once HSSM_PLUGIN_DIR . 'includes/class-hssm-admin.php'; + require_once HSSM_PLUGIN_DIR . 'includes/class-hssm-frontend.php'; + require_once HSSM_PLUGIN_DIR . 'includes/class-hssm-notifications.php'; + require_once HSSM_PLUGIN_DIR . 'includes/class-hssm-api-client.php'; + require_once HSSM_PLUGIN_DIR . 'includes/class-hssm-slider.php'; + require_once HSSM_PLUGIN_DIR . 'includes/class-hssm-shortcode.php'; + + // Initialize slides custom post type + $this->slides_cpt = new HSSM_Slides_CPT(); + + // Initialize REST API endpoints + $this->rest_api = new HSSM_REST_API(); + + // Initialize admin functionality + $this->admin = new HSSM_Admin(); + + // Initialize frontend functionality + $this->frontend = new HSSM_Frontend(); + + // Initialize shortcode + $this->shortcode = new HSSM_Shortcode(); + + // Initialize email notifications + $this->email_notifications = new HSSM_Notifications(); + } + + /** + * Initialize WordPress hooks + * + * @since 1.0.0 + */ + private function init_hooks() { + $plugin_mode = get_option( 'hssm_plugin_mode', 'manager' ); + + // Core WordPress hooks + if ( 'manager' === $plugin_mode ) { + add_action( 'init', array( $this, 'register_post_types' ) ); + add_action( 'init', array( $this, 'register_taxonomies' ) ); + add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) ); + } + + // Frontend hooks + add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_assets' ), 20 ); + add_action( 'template_redirect', array( $this, 'handle_frontend_pages' ) ); + + // Admin hooks + add_action( 'admin_init', array( $this, 'admin_init' ) ); + add_action( 'admin_menu', array( $this, 'add_admin_menus' ) ); + add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) ); + + // AJAX hooks for authenticated users + // Note: Main AJAX handlers are in HSSM_Frontend class + add_action( 'wp_ajax_hssm_upload_image', array( $this, 'ajax_upload_image' ) ); + add_action( 'wp_ajax_hssm_preview_slides', array( $this, 'ajax_preview_slides' ) ); + add_action( 'wp_ajax_hssm_send_preview_email', array( $this, 'ajax_send_preview_email' ) ); + + // Cron hooks for scheduled slides + add_action( 'hssm_check_scheduled_slides', array( $this, 'check_scheduled_slides' ) ); + } + + /** + * Load plugin text domain + * + * @since 1.0.0 + */ + private function load_textdomain() { + load_plugin_textdomain( + 'hi-school-slide-manager', + false, + dirname( HSSM_PLUGIN_BASENAME ) . '/languages' + ); + } + + /** + * Register custom post types + * + * @since 1.0.0 + */ + public function register_post_types() { + if ( $this->slides_cpt ) { + $this->slides_cpt->register(); + } + } + + /** + * Register custom taxonomies + * + * @since 1.0.0 + */ + public function register_taxonomies() { + if ( $this->slides_cpt ) { + $this->slides_cpt->register_taxonomies(); + } + } + + /** + * Register REST API routes + * + * @since 1.0.0 + */ + public function register_rest_routes() { + if ( $this->rest_api ) { + $this->rest_api->register_routes(); + } + } + + /** + * Enqueue frontend assets + * + * @since 1.0.0 + */ + 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') ) { + // These pages will load their own specific styles (admin-slides-dashboard.css) or rely on explicit shortcode enqueueing. + return; + } + + // Enqueue slider assets globally so they are available for the shortcode on regular frontend pages + wp_enqueue_style( + 'hssm-slider', + HSSM_PLUGIN_URL . 'assets/css/slider.css', + array(), + HSSM_VERSION + ); + + wp_enqueue_script( + 'hssm-slider', + HSSM_PLUGIN_URL . 'assets/js/slider.js', + array( 'jquery' ), + HSSM_VERSION, + true + ); + + if ( $this->frontend ) { + $this->frontend->enqueue_assets(); + } + } + + /** + * Handle frontend page routing + * + * @since 1.0.0 + */ + public function handle_frontend_pages() { + if ( $this->frontend ) { + $this->frontend->handle_page_routing(); + } + } + + /** + * Initialize admin functionality + * + * @since 1.0.0 + */ + public function admin_init() { + if ( $this->admin ) { + $this->admin->init(); + } + } + + /** + * Add admin menus + * + * @since 1.0.0 + */ + public function add_admin_menus() { + if ( $this->admin ) { + $this->admin->add_menus(); + } + } + + /** + * Enqueue admin assets + * + * @since 1.0.0 + * @param string $hook Current admin page hook + */ + public function enqueue_admin_assets( $hook ) { + if ( $this->admin ) { + $this->admin->enqueue_assets( $hook ); + } + } + + /** + * Get companion sites configuration + * + * Centralized source of truth for companion sites + * + * @since 1.0.0 + * @return array List of companion sites + */ + public static function get_companion_sites() { + return array( + array( + 'slug' => 'hi-school', + 'name' => 'Hi-School Pharmacy', + 'description' => 'Hi-School Pharmacy companion site' + ), + array( + 'slug' => 'one-stop', + 'name' => 'One Stop Hardware', + 'description' => 'One Stop Hardware companion site' + ), + array( + 'slug' => 'ace', + 'name' => 'Ace Hardware', + 'description' => 'Ace Hardware companion site' + ) + ); + } + + /** + * Check for scheduled slides that need to be activated/deactivated + * + * @since 1.0.0 + */ + public function check_scheduled_slides() { + if ( $this->slides_cpt ) { + $this->slides_cpt->check_scheduled_slides(); + } + } + + /** + * Plugin activation + * + * @since 1.0.0 + */ + public static function activate() { + // Set up cron jobs + self::setup_cron_jobs(); + + // Flush rewrite rules + flush_rewrite_rules(); + + // Set default options + self::set_default_options(); + + // Create default taxonomy terms + self::create_default_terms(); + } + + /** + * Plugin deactivation + * + * @since 1.0.0 + */ + public static function deactivate() { + // Clear cron jobs + wp_clear_scheduled_hook( 'hssm_check_scheduled_slides' ); + + // Flush rewrite rules + flush_rewrite_rules(); + } + + /** + * Plugin uninstall + * + * @since 1.0.0 + */ + public static function uninstall() { + // Remove plugin options + self::remove_plugin_options(); + + // Remove user meta + self::remove_user_meta(); + + // Remove taxonomy terms and slides + self::remove_plugin_data(); + } + + /** + * Create default taxonomy terms for sites + * + * @since 1.0.0 + */ + private static function create_default_terms() { + // Load the CPT class manually during activation + require_once HSSM_PLUGIN_DIR . 'includes/class-hssm-slides-cpt.php'; + + // Create a temporary instance to set up default terms + $slides_cpt = new HSSM_Slides_CPT(); + + // Register the taxonomies first + $slides_cpt->register_taxonomies(); + + // Default terms are created automatically in the register_taxonomies method + } + + /** + * Setup cron jobs + * + * @since 1.0.0 + */ + private static function setup_cron_jobs() { + if ( ! wp_next_scheduled( 'hssm_check_scheduled_slides' ) ) { + wp_schedule_event( time(), 'hourly', 'hssm_check_scheduled_slides' ); + } + } + + /** + * Set default plugin options + * + * @since 1.0.0 + */ + private static function set_default_options() { + $default_options = array( + 'hssm_plugin_mode' => 'manager', + 'hssm_api_url' => '', + 'hssm_client_site_slug' => '', + 'hssm_notification_emails' => array( get_option( 'admin_email' ) ), + 'hssm_default_slide_duration' => 30, // days + 'hssm_cache_duration' => 3600, // 1 hour + 'hssm_api_version' => 'v1', + 'hssm_preview_page_slug' => 'slide-preview', + 'hssm_manager_page_slug' => 'slide-manager', + 'hssm_api_keys' => array( wp_generate_password( 32, false ) ), // Generate initial API key + ); + + foreach ( $default_options as $option_name => $option_value ) { + if ( ! get_option( $option_name ) ) { + add_option( $option_name, $option_value ); + } + } + } + + /** + * Remove plugin options + * + * @since 1.0.0 + */ + private static function remove_plugin_options() { + $options = array( + 'hssm_notification_emails', + 'hssm_default_slide_duration', + 'hssm_cache_duration', + 'hssm_api_version', + 'hssm_preview_page_slug', + 'hssm_manager_page_slug', + 'hssm_api_keys', + ); + + foreach ( $options as $option ) { + delete_option( $option ); + } + } + + /** + * Remove user meta + * + * @since 1.0.0 + */ + private static function remove_user_meta() { + global $wpdb; + + $wpdb->delete( + $wpdb->usermeta, + array( 'meta_key' => 'hssm_user_preferences' ) + ); + } + + /** + * Remove plugin data including slides and taxonomy terms + * + * @since 1.0.0 + */ + private static function remove_plugin_data() { + // Get all slide posts + $slides = get_posts( array( + 'post_type' => 'hssm_slide', + 'numberposts' => -1, + 'post_status' => 'any' + ) ); + + // Delete all slides + foreach ( $slides as $slide ) { + wp_delete_post( $slide->ID, true ); + } + + // Remove taxonomy terms + $taxonomies = array( 'hssm_site', 'hssm_client' ); + foreach ( $taxonomies as $taxonomy ) { + $terms = get_terms( array( + 'taxonomy' => $taxonomy, + 'hide_empty' => false, + ) ); + + if ( ! is_wp_error( $terms ) ) { + foreach ( $terms as $term ) { + wp_delete_term( $term->term_id, $taxonomy ); + } + } + } + } +} diff --git a/includes/class-hssm-notifications.php b/includes/class-hssm-notifications.php new file mode 100644 index 0000000..4f822d2 --- /dev/null +++ b/includes/class-hssm-notifications.php @@ -0,0 +1,274 @@ + '', + 'itc_emails' => '' + ); + + $settings = get_option( 'hssm_notification_settings', $defaults ); + + // Ensure we have an array and it contains our expected keys + if ( ! is_array( $settings ) ) { + return $defaults; + } + + return wp_parse_args( $settings, $defaults ); + } + + /** + * Save notification settings + * + * @param array $settings Array of settings + * @return bool True if saved successfully + */ + public static function save_settings( $settings ) { + $clean_settings = array( + 'editor_emails' => sanitize_textarea_field( $settings['editor_emails'] ), + 'itc_emails' => sanitize_textarea_field( $settings['itc_emails'] ) + ); + + return update_option( 'hssm_notification_settings', $clean_settings ); + } + + /** + * Helper to get emails as array from string + * + * @param string $emails_string Comma or newline separated emails + * @return array Array of valid email addresses + */ + private function get_emails_array( $emails_string ) { + if ( empty( $emails_string ) ) { + return array(); + } + + // Replace newlines with commas + $emails_string = str_replace( array( "\r", "\n" ), ',', $emails_string ); + $emails = explode( ',', $emails_string ); + + $clean_emails = array(); + foreach ( $emails as $email ) { + $email = sanitize_email( trim( $email ) ); + if ( is_email( $email ) ) { + $clean_emails[] = $email; + } + } + + return array_unique( $clean_emails ); + } + + /** + * Send notification to Editor when slide is Scheduled or Published Immediately + * + * @param int $slide_id Slide ID + * @param string $status 'scheduled' or 'published' + */ + public function notify_editor_slide_created( $slide_id, $status ) { + $settings = self::get_settings(); + $emails = $this->get_emails_array( $settings['editor_emails'] ); + + if ( empty( $emails ) ) { + error_log( 'HSSM: No Editor emails configured for notification.' ); + return; + } + + $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 ); + } + + $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 ); + + $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."; + + foreach ( $emails as $email ) { + wp_mail( $email, $subject, $message ); + } + } + + /** + * Send notification to Creator (Author) when slide is Approved + * + * @param int $slide_id Slide ID + */ + public function notify_creator_slide_approved( $slide_id ) { + $slide = get_post( $slide_id ); + if ( ! $slide ) return; + + $author_email = get_the_author_meta( 'user_email', $slide->post_author ); + + 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."; + + wp_mail( $author_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 ) { + $settings = self::get_settings(); + $emails = $this->get_emails_array( $settings['itc_emails'] ); + + if ( empty( $emails ) ) return; + + $slide = get_post( $slide_id ); + 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 .= "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; + + foreach ( $emails as $email ) { + wp_mail( $email, $subject, $message ); + } + } + + /** + * Send FINAL notification to Editor and Author (triggered by ITC confirmation) + * + * @param int $slide_id Slide ID + */ + public function notify_all_confirmed_live( $slide_id ) { + $slide = get_post( $slide_id ); + if ( ! $slide ) return; + + // Editor Emails + $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 ) ); + + if ( empty( $recipients ) ) return; + + $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."; + + foreach ( $recipients as $email ) { + wp_mail( $email, $subject, $message ); + } + } +} diff --git a/includes/class-hssm-rest-api.php b/includes/class-hssm-rest-api.php new file mode 100644 index 0000000..701b14f --- /dev/null +++ b/includes/class-hssm-rest-api.php @@ -0,0 +1,532 @@ +[a-zA-Z0-9-]+)', array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_slides_for_site' ), + 'permission_callback' => array( $this, 'check_api_permissions' ), + 'args' => array( + 'site' => array( + 'required' => true, + 'validate_callback' => array( $this, 'validate_site_slug' ), + 'sanitize_callback' => 'sanitize_text_field', + 'description' => 'Site slug (hi-school, one-stop, ace)', + ), + 'date' => array( + 'required' => false, + 'validate_callback' => array( $this, 'validate_date' ), + 'sanitize_callback' => 'sanitize_text_field', + 'description' => 'Date to get slides for (YYYY-MM-DD format)', + ), + 'limit' => array( + 'required' => false, + 'default' => 10, + 'validate_callback' => array( $this, 'validate_limit' ), + 'sanitize_callback' => 'absint', + 'description' => 'Maximum number of slides to return (1-50)', + ), + 'status' => array( + 'required' => false, + 'default' => 'active', + 'validate_callback' => array( $this, 'validate_status' ), + 'sanitize_callback' => 'sanitize_text_field', + 'description' => 'Slide status: active, scheduled, all', + ), + ), + ) ); + + // Get all available sites + register_rest_route( self::NAMESPACE, '/sites', array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_available_sites' ), + 'permission_callback' => array( $this, 'check_api_permissions' ), + ) ); + + // Get slide by ID + register_rest_route( self::NAMESPACE, '/slide/(?P\d+)', array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_slide_by_id' ), + 'permission_callback' => array( $this, 'check_api_permissions' ), + 'args' => array( + 'id' => array( + 'required' => true, + 'validate_callback' => array( $this, 'validate_slide_id' ), + 'sanitize_callback' => 'absint', + 'description' => 'Slide ID', + ), + ), + ) ); + + // Health check endpoint + register_rest_route( self::NAMESPACE, '/health', array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'health_check' ), + 'permission_callback' => '__return_true', // Public endpoint + ) ); + } + + /** + * Get slides for a specific site + * + * @since 1.0.0 + * @param WP_REST_Request $request Request object + * @return WP_REST_Response|WP_Error Response object + */ + public function get_slides_for_site( $request ) { + $site_slug = $request->get_param( 'site' ); + $date = $request->get_param( 'date' ); + $limit = $request->get_param( 'limit' ); + $status = $request->get_param( 'status' ); + + // Get the site term + $site_term = get_term_by( 'slug', $site_slug, 'hssm_site' ); + if ( ! $site_term ) { + return new WP_Error( 'invalid_site', 'Invalid site slug', array( 'status' => 404 ) ); + } + + // Build query args + $query_args = array( + 'post_type' => 'hssm_slide', + 'posts_per_page' => $limit, + 'post_status' => 'publish', + 'meta_key' => '_hssm_order', + 'orderby' => 'meta_value_num', + 'order' => 'ASC', + 'tax_query' => array( + array( + 'taxonomy' => 'hssm_site', + 'field' => 'term_id', + 'terms' => $site_term->term_id, + ), + ), + ); + + // Add date filtering if specified + if ( $date ) { + $target_date = $date . ' 23:59:59'; + $query_args['meta_query'] = array( + 'relation' => 'AND', + array( + 'relation' => 'OR', + array( + 'key' => '_hssm_start_date', + 'value' => '', + 'compare' => '=' + ), + array( + 'key' => '_hssm_start_date', + 'value' => $target_date, + 'compare' => '<=' + ), + array( + 'key' => '_hssm_start_date', + 'compare' => 'NOT EXISTS' + ) + ), + array( + 'relation' => 'OR', + array( + 'key' => '_hssm_end_date', + 'value' => '', + 'compare' => '=' + ), + array( + 'key' => '_hssm_end_date', + 'value' => $target_date, + 'compare' => '>=' + ), + array( + 'key' => '_hssm_end_date', + 'compare' => 'NOT EXISTS' + ) + ) + ); + } + + // Handle status filtering + if ( $status === 'scheduled' ) { + $current_time = current_time( 'mysql' ); + $query_args['meta_query'] = array( + array( + 'key' => '_hssm_start_date', + 'value' => $current_time, + 'compare' => '>' + ) + ); + } elseif ( $status === 'all' ) { + $query_args['post_status'] = array( 'publish', 'future', 'draft' ); + } + + $slides_query = new WP_Query( $query_args ); + $slides_data = array(); + + if ( $slides_query->have_posts() ) { + while ( $slides_query->have_posts() ) { + $slides_query->the_post(); + $slides_data[] = $this->format_slide_data( get_post() ); + } + wp_reset_postdata(); + } + + $response_data = array( + 'slides' => $slides_data, + 'site' => array( + 'slug' => $site_slug, + 'name' => $site_term->name, + 'description' => $site_term->description, + ), + 'meta' => array( + 'total_slides' => count( $slides_data ), + 'query_date' => $date ?: current_time( 'Y-m-d' ), + 'status_filter' => $status, + 'timestamp' => current_time( 'c' ), + 'cache_duration' => get_option( 'hssm_cache_duration', 3600 ), + ), + ); + + return rest_ensure_response( $response_data ); + } + + /** + * Get all available sites + * + * @since 1.0.0 + * @param WP_REST_Request $request Request object + * @return WP_REST_Response Response object + */ + public function get_available_sites( $request ) { + $sites = get_terms( array( + 'taxonomy' => 'hssm_site', + 'hide_empty' => false, + ) ); + + $sites_data = array(); + if ( ! is_wp_error( $sites ) ) { + foreach ( $sites as $site ) { + $sites_data[] = array( + 'slug' => $site->slug, + 'name' => $site->name, + 'description' => $site->description, + 'slide_count' => $this->get_site_slide_count( $site->term_id ), + ); + } + } + + return rest_ensure_response( array( + 'sites' => $sites_data, + 'total' => count( $sites_data ), + ) ); + } + + /** + * Get slide by ID + * + * @since 1.0.0 + * @param WP_REST_Request $request Request object + * @return WP_REST_Response|WP_Error Response object + */ + public function get_slide_by_id( $request ) { + $slide_id = $request->get_param( 'id' ); + $slide = get_post( $slide_id ); + + if ( ! $slide || $slide->post_type !== 'hssm_slide' ) { + return new WP_Error( 'slide_not_found', 'Slide not found', array( 'status' => 404 ) ); + } + + return rest_ensure_response( $this->format_slide_data( $slide ) ); + } + + /** + * Health check endpoint + * + * @since 1.0.0 + * @param WP_REST_Request $request Request object + * @return WP_REST_Response Response object + */ + public function health_check( $request ) { + return rest_ensure_response( array( + 'status' => 'healthy', + 'plugin' => 'Hi-School Slide Manager', + 'version' => HSSM_VERSION, + 'timestamp' => current_time( 'c' ), + 'endpoints' => array( + 'slides' => rest_url( self::NAMESPACE . '/slides/{site}' ), + 'sites' => rest_url( self::NAMESPACE . '/sites' ), + 'slide' => rest_url( self::NAMESPACE . '/slide/{id}' ), + ), + ) ); + } + + /** + * Check API permissions + * + * @since 1.0.0 + * @param WP_REST_Request $request Request object + * @return bool|WP_Error Permission check result + */ + public function check_api_permissions( $request ) { + // Get the API key from header or query parameter + $api_key = $request->get_header( 'X-HSSM-API-Key' ); + if ( ! $api_key ) { + $api_key = $request->get_param( 'api_key' ); + } + + if ( ! $api_key ) { + return new WP_Error( 'missing_api_key', 'API key required', array( 'status' => 401 ) ); + } + + // Validate API key + $valid_keys = get_option( 'hssm_api_keys', array() ); + if ( ! in_array( $api_key, $valid_keys ) ) { + return new WP_Error( 'invalid_api_key', 'Invalid API key', array( 'status' => 403 ) ); + } + + return true; + } + + /** + * Format slide data for API response + * + * @since 1.0.0 + * @param WP_Post $slide Slide post object + * @return array Formatted slide data + */ + private function format_slide_data( $slide ) { + $slide_data = array( + 'id' => $slide->ID, + 'title' => $slide->post_title, + 'subtitle' => get_post_meta( $slide->ID, '_hssm_subtitle', true ), + 'content' => $slide->post_content, + 'excerpt' => $slide->post_excerpt, + 'buttons' => get_post_meta( $slide->ID, '_hssm_buttons', true ) ?: array(), + 'order' => (int) get_post_meta( $slide->ID, '_hssm_order', true ), + 'background_image' => $this->get_slide_background_image( $slide->ID ), + 'schedule' => array( + 'start_date' => get_post_meta( $slide->ID, '_hssm_start_date', true ), + 'end_date' => get_post_meta( $slide->ID, '_hssm_end_date', true ), + 'auto_publish' => get_post_meta( $slide->ID, '_hssm_auto_publish', true ) === '1', + '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, + ); + + return $slide_data; + } + + /** + * Get slide background image data + * + * @since 1.0.0 + * @param int $slide_id Slide ID + * @return array|null Background image data + */ + private function get_slide_background_image( $slide_id ) { + $thumbnail_id = get_post_thumbnail_id( $slide_id ); + if ( ! $thumbnail_id ) { + return null; + } + + $image_data = wp_get_attachment_image_src( $thumbnail_id, 'full' ); + if ( ! $image_data ) { + return null; + } + + return array( + 'id' => $thumbnail_id, + 'url' => $image_data[0], + 'width' => $image_data[1], + 'height' => $image_data[2], + 'alt' => get_post_meta( $thumbnail_id, '_wp_attachment_image_alt', true ), + 'sizes' => array( + 'full' => wp_get_attachment_image_src( $thumbnail_id, 'full' )[0], + 'large' => wp_get_attachment_image_src( $thumbnail_id, 'large' )[0], + 'medium' => wp_get_attachment_image_src( $thumbnail_id, 'medium' )[0], + 'thumbnail' => wp_get_attachment_image_src( $thumbnail_id, 'thumbnail' )[0], + ), + ); + } + + /** + * Get slide target sites + * + * @since 1.0.0 + * @param int $slide_id Slide ID + * @return array Target sites data + */ + private function get_slide_target_sites( $slide_id ) { + $sites = wp_get_post_terms( $slide_id, 'hssm_site' ); + $sites_data = array(); + + if ( ! is_wp_error( $sites ) ) { + foreach ( $sites as $site ) { + $sites_data[] = array( + 'slug' => $site->slug, + 'name' => $site->name, + ); + } + } + + 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 + * + * @since 1.0.0 + * @param int $site_term_id Site term ID + * @return int Slide count + */ + private function get_site_slide_count( $site_term_id ) { + $query = new WP_Query( array( + 'post_type' => 'hssm_slide', + 'post_status' => 'publish', + 'posts_per_page' => -1, + 'fields' => 'ids', + 'tax_query' => array( + array( + 'taxonomy' => 'hssm_site', + 'field' => 'term_id', + 'terms' => $site_term_id, + ), + ), + ) ); + + return $query->found_posts; + } + + /** + * Validate site slug parameter + * + * @since 1.0.0 + * @param string $value Site slug + * @param WP_REST_Request $request Request object + * @param string $param Parameter name + * @return bool Validation result + */ + public function validate_site_slug( $value, $request, $param ) { + $companion_sites = HSSM_Main::get_companion_sites(); + $valid_sites = wp_list_pluck( $companion_sites, 'slug' ); + return in_array( $value, $valid_sites ); + } + + /** + * Validate date parameter + * + * @since 1.0.0 + * @param string $value Date string + * @param WP_REST_Request $request Request object + * @param string $param Parameter name + * @return bool Validation result + */ + public function validate_date( $value, $request, $param ) { + return (bool) strtotime( $value ); + } + + /** + * Validate limit parameter + * + * @since 1.0.0 + * @param int $value Limit value + * @param WP_REST_Request $request Request object + * @param string $param Parameter name + * @return bool Validation result + */ + public function validate_limit( $value, $request, $param ) { + return is_numeric( $value ) && $value >= 1 && $value <= 50; + } + + /** + * Validate status parameter + * + * @since 1.0.0 + * @param string $value Status value + * @param WP_REST_Request $request Request object + * @param string $param Parameter name + * @return bool Validation result + */ + public function validate_status( $value, $request, $param ) { + $valid_statuses = array( 'active', 'scheduled', 'all' ); + return in_array( $value, $valid_statuses ); + } + + /** + * Validate slide ID parameter + * + * @since 1.0.0 + * @param int $value Slide ID + * @param WP_REST_Request $request Request object + * @param string $param Parameter name + * @return bool Validation result + */ + public function validate_slide_id( $value, $request, $param ) { + return is_numeric( $value ) && $value > 0; + } +} \ No newline at end of file diff --git a/includes/class-hssm-shortcode.php b/includes/class-hssm-shortcode.php new file mode 100644 index 0000000..9cc4a38 --- /dev/null +++ b/includes/class-hssm-shortcode.php @@ -0,0 +1,121 @@ +get_slides(); + + if ( is_wp_error( $result ) ) { + if ( current_user_can( 'manage_options' ) ) { + return '
' . sprintf( __( 'HSSM API Error: %s', 'hi-school-slide-manager' ), $result->get_error_message() ) . '
'; + } + return ''; + } + $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' ); + + // 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' ), + 'meta_key' => '_hssm_order', + 'orderby' => 'meta_value_num', + 'order' => 'ASC', + ); + + if ( ! empty( $site_slug ) ) { + $args['tax_query'] = array( + array( + 'taxonomy' => 'hssm_site', + 'field' => 'slug', + 'terms' => $site_slug, + ), + ); + } + + // Add date filtering + $target_date = $date . ' 23:59:59'; + $args['meta_query'] = array( + 'relation' => 'AND', + array( + 'relation' => 'OR', + array( 'key' => '_hssm_start_date', 'value' => '', 'compare' => '=' ), + array( 'key' => '_hssm_start_date', 'value' => $target_date, 'compare' => '<=' ), + array( 'key' => '_hssm_start_date', 'compare' => 'NOT EXISTS' ) + ), + array( + 'relation' => 'OR', + array( 'key' => '_hssm_end_date', 'value' => '', 'compare' => '=' ), + array( 'key' => '_hssm_end_date', 'value' => $target_date, 'compare' => '>=' ), + array( 'key' => '_hssm_end_date', 'compare' => 'NOT EXISTS' ) + ) + ); + + $query = new WP_Query( $args ); + 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', + ); + } + wp_reset_postdata(); + } + } + + if ( empty( $slides ) ) { + return ''; + } + + $slider = new HSSM_Slider(); + return $slider->render( $slides, $atts ); + } +} diff --git a/includes/class-hssm-slider.php b/includes/class-hssm-slider.php new file mode 100644 index 0000000..011a964 --- /dev/null +++ b/includes/class-hssm-slider.php @@ -0,0 +1,133 @@ + 'hssm-slider-' . uniqid(), + 'class' => '', + 'autoplay' => 'true', + 'interval' => '5000', + ), $atts ); + + ob_start(); + $this->enqueue_assets(); + ?> +
+
+
+
+ $slide ) : ?> + +
+
+
+
+ +

+ + + +

+ + + +
+ +
+ + + + + +
+ + + + + +
+ + +
+
+
+
+ +
+
+ + 1 ) : ?> + + + +
+
+ _x( 'Slides', 'Post type general name', 'hi-school-slide-manager' ), + 'singular_name' => _x( 'Slide', 'Post type singular name', 'hi-school-slide-manager' ), + 'menu_name' => _x( 'Slides', 'Admin Menu text', 'hi-school-slide-manager' ), + 'name_admin_bar' => _x( 'Slide', 'Add New on Toolbar', 'hi-school-slide-manager' ), + 'add_new' => __( 'Add New', 'hi-school-slide-manager' ), + 'add_new_item' => __( 'Add New Slide', 'hi-school-slide-manager' ), + 'new_item' => __( 'New Slide', 'hi-school-slide-manager' ), + 'edit_item' => __( 'Edit Slide', 'hi-school-slide-manager' ), + 'view_item' => __( 'View Slide', 'hi-school-slide-manager' ), + 'all_items' => __( 'All Slides', 'hi-school-slide-manager' ), + 'search_items' => __( 'Search Slides', 'hi-school-slide-manager' ), + 'parent_item_colon' => __( 'Parent Slides:', 'hi-school-slide-manager' ), + 'not_found' => __( 'No slides found.', 'hi-school-slide-manager' ), + 'not_found_in_trash' => __( 'No slides found in Trash.', 'hi-school-slide-manager' ), + 'featured_image' => _x( 'Background Image', 'Overrides the "Featured Image" phrase', 'hi-school-slide-manager' ), + 'set_featured_image' => _x( 'Set background image', 'Overrides the "Set featured image" phrase', 'hi-school-slide-manager' ), + 'remove_featured_image' => _x( 'Remove background image', 'Overrides the "Remove featured image" phrase', 'hi-school-slide-manager' ), + 'use_featured_image' => _x( 'Use as background image', 'Overrides the "Use as featured image" phrase', 'hi-school-slide-manager' ), + 'archives' => _x( 'Slide archives', 'The post type archive label used in nav menus', 'hi-school-slide-manager' ), + 'insert_into_item' => _x( 'Insert into slide', 'Overrides the "Insert into post" phrase', 'hi-school-slide-manager' ), + 'uploaded_to_this_item' => _x( 'Uploaded to this slide', 'Overrides the "Uploaded to this post" phrase', 'hi-school-slide-manager' ), + 'filter_items_list' => _x( 'Filter slides list', 'Screen reader text for the filter links', 'hi-school-slide-manager' ), + 'items_list_navigation' => _x( 'Slides list navigation', 'Screen reader text for the pagination', 'hi-school-slide-manager' ), + 'items_list' => _x( 'Slides list', 'Screen reader text for the items list', 'hi-school-slide-manager' ), + ); + + $args = array( + 'labels' => $labels, + 'public' => true, + 'publicly_queryable' => true, + 'show_ui' => true, + 'show_in_menu' => true, + 'query_var' => true, + 'rewrite' => array( 'slug' => 'slide' ), + 'capability_type' => 'post', + 'has_archive' => true, + 'hierarchical' => false, + 'menu_position' => 20, + 'menu_icon' => 'dashicons-slides', + 'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt' ), + 'show_in_rest' => true, + 'rest_base' => 'slides', + 'rest_controller_class' => 'HSSM_REST_Slides_Controller', + ); + + register_post_type( self::POST_TYPE, $args ); + } + + /** + * Register custom taxonomies + * + * @since 1.0.0 + */ + public function register_taxonomies() { + // Site taxonomy for organizing slides by target site + $site_labels = array( + 'name' => _x( 'Target Sites', 'taxonomy general name', 'hi-school-slide-manager' ), + 'singular_name' => _x( 'Target Site', 'taxonomy singular name', 'hi-school-slide-manager' ), + 'search_items' => __( 'Search Sites', 'hi-school-slide-manager' ), + 'all_items' => __( 'All Sites', 'hi-school-slide-manager' ), + 'parent_item' => __( 'Parent Site', 'hi-school-slide-manager' ), + 'parent_item_colon' => __( 'Parent Site:', 'hi-school-slide-manager' ), + 'edit_item' => __( 'Edit Site', 'hi-school-slide-manager' ), + 'update_item' => __( 'Update Site', 'hi-school-slide-manager' ), + 'add_new_item' => __( 'Add New Site', 'hi-school-slide-manager' ), + 'new_item_name' => __( 'New Site Name', 'hi-school-slide-manager' ), + 'menu_name' => __( 'Target Sites', 'hi-school-slide-manager' ), + ); + + $site_args = array( + 'hierarchical' => false, + 'labels' => $site_labels, + 'show_ui' => true, + 'show_admin_column' => true, + 'query_var' => true, + 'rewrite' => array( 'slug' => 'slide-site' ), + 'show_in_rest' => true, + ); + + 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(); + } + + /** + * Create default site terms for the three companion sites + * + * @since 1.0.0 + */ + private function create_default_site_terms() { + $default_sites = array(); + + // Use centralized method from HSSM_Main if available + if ( class_exists( 'HSSM_Main' ) && method_exists( 'HSSM_Main', 'get_companion_sites' ) ) { + $default_sites = HSSM_Main::get_companion_sites(); + } else { + // Fallback + $default_sites = array( + array( + 'slug' => 'hi-school', + 'name' => 'Hi-School Pharmacy', + 'description' => 'Hi-School Pharmacy companion site' + ), + array( + 'slug' => 'one-stop', + 'name' => 'One Stop Hardware', + 'description' => 'One Stop Hardware companion site' + ), + array( + 'slug' => 'ace', + 'name' => 'Ace Hardware', + 'description' => 'Ace Hardware companion site' + ) + ); + } + + foreach ( $default_sites as $site ) { + if ( ! term_exists( $site['slug'], self::SITE_TAXONOMY ) ) { + wp_insert_term( + $site['name'], + self::SITE_TAXONOMY, + array( + 'slug' => $site['slug'], + 'description' => $site['description'] + ) + ); + } + } + } + + /** + * Add meta boxes for slide fields + * + * @since 1.0.0 + */ + public function add_meta_boxes() { + add_meta_box( + 'hssm_slide_details', + __( 'Slide Details', 'hi-school-slide-manager' ), + array( $this, 'slide_details_meta_box' ), + self::POST_TYPE, + 'normal', + 'high' + ); + + add_meta_box( + 'hssm_background_image_help', + __( 'Background Image Guidelines', 'hi-school-slide-manager' ), + array( $this, 'background_image_help_meta_box' ), + self::POST_TYPE, + 'side', + 'high' + ); + + add_meta_box( + 'hssm_slide_scheduling', + __( 'Slide Scheduling', 'hi-school-slide-manager' ), + array( $this, 'slide_scheduling_meta_box' ), + self::POST_TYPE, + 'side', + 'default' + ); + + add_meta_box( + 'hssm_slide_targeting', + __( 'Site Targeting', 'hi-school-slide-manager' ), + array( $this, 'slide_targeting_meta_box' ), + self::POST_TYPE, + 'side', + 'default' + ); + } + + /** + * Render slide details meta box + * + * @since 1.0.0 + * @param WP_Post $post Current post object + */ + public function slide_details_meta_box( $post ) { + wp_nonce_field( 'hssm_save_slide_meta', 'hssm_slide_meta_nonce' ); + + $subtitle = get_post_meta( $post->ID, '_hssm_subtitle', true ); + $buttons = get_post_meta( $post->ID, '_hssm_buttons', true ); + $order = get_post_meta( $post->ID, '_hssm_order', true ); + $hide_all_text = get_post_meta( $post->ID, '_hssm_hide_all_text', true ); + + // Initialize buttons array if empty + if ( ! is_array( $buttons ) || empty( $buttons ) ) { + $buttons = array( + array( 'text' => 'Learn More', 'url' => '' ) + ); + } + + ?> + + + + + + + + + + + + + + + + + +
+ + + +

+
+ + +
+ $button ) : ?> +
+
+ + + + + + + +
+
+ +
+ +

+ + +

+ +

+
+ + + +

+
+ + + +

+
+ + + + + +
+

+
    +
  • +
  • +
  • +
  • +
+ +

+
    +
  • +
  • +
  • +
+ +

+
+ + + ID, '_hssm_start_date', true ); + $end_date = get_post_meta( $post->ID, '_hssm_end_date', true ); + $auto_publish = get_post_meta( $post->ID, '_hssm_auto_publish', true ); + $auto_unpublish = get_post_meta( $post->ID, '_hssm_auto_unpublish', true ); + + ?> +

+
+ +

+

+
+ +

+

+ +

+

+ +

+ 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( + 'taxonomy' => self::SITE_TAXONOMY, + 'hide_empty' => false, + ) ); + + // Get all available client terms + $client_terms = get_terms( array( + 'taxonomy' => self::CLIENT_TAXONOMY, + 'hide_empty' => false, + ) ); + + ?> +
+

+ + +

+ + description ) : ?> +
description ); ?> + +

+ + +

+ +
+ +
+

+ + + +

+ + + + +

+ +
+ + + 'sanitize_text_field', + 'hssm_order' => 'intval', + 'hssm_start_date' => 'sanitize_text_field', + 'hssm_end_date' => 'sanitize_text_field', + ); + + foreach ( $meta_fields as $field => $sanitize_function ) { + if ( isset( $_POST[ $field ] ) ) { + $value = call_user_func( $sanitize_function, $_POST[ $field ] ); + update_post_meta( $post_id, '_' . $field, $value ); + } + } + + // Save buttons array + if ( isset( $_POST['hssm_buttons'] ) && is_array( $_POST['hssm_buttons'] ) ) { + $buttons = array(); + foreach ( $_POST['hssm_buttons'] as $button_data ) { + if ( ! empty( $button_data['text'] ) || ! empty( $button_data['url'] ) ) { + $buttons[] = array( + 'text' => sanitize_text_field( $button_data['text'] ), + 'url' => esc_url_raw( $button_data['url'] ) + ); + } + } + // Limit to 5 buttons maximum + $buttons = array_slice( $buttons, 0, 5 ); + update_post_meta( $post_id, '_hssm_buttons', $buttons ); + } else { + // If no buttons, set default + update_post_meta( $post_id, '_hssm_buttons', array( + array( 'text' => 'Learn More', 'url' => '' ) + ) ); + } + + // Save checkbox fields + $checkbox_fields = array( 'hssm_auto_publish', 'hssm_auto_unpublish', 'hssm_hide_all_text' ); + foreach ( $checkbox_fields as $field ) { + $value = isset( $_POST[ $field ] ) ? '1' : '0'; + update_post_meta( $post_id, '_' . $field, $value ); + } + + // Save target sites taxonomy + if ( isset( $_POST['hssm_target_sites'] ) && is_array( $_POST['hssm_target_sites'] ) ) { + $target_sites = array_map( 'intval', $_POST['hssm_target_sites'] ); + wp_set_post_terms( $post_id, $target_sites, self::SITE_TAXONOMY ); + } else { + wp_set_post_terms( $post_id, array(), self::SITE_TAXONOMY ); + } + + // Save client taxonomy + if ( isset( $_POST['hssm_client'] ) && ! empty( $_POST['hssm_client'] ) ) { + $client_id = intval( $_POST['hssm_client'] ); + wp_set_post_terms( $post_id, array( $client_id ), self::CLIENT_TAXONOMY ); + } else { + wp_set_post_terms( $post_id, array(), self::CLIENT_TAXONOMY ); + } + + // Send email notification for new slides + if ( get_post_status( $post_id ) === 'publish' && ! get_post_meta( $post_id, '_hssm_notification_sent', true ) ) { + $this->send_new_slide_notification( $post_id ); + update_post_meta( $post_id, '_hssm_notification_sent', '1' ); + } + } + + /** + * Add custom columns to the slides list table + * + * @since 1.0.0 + * @param array $columns Current columns + * @return array Modified columns + */ + public function add_custom_columns( $columns ) { + $new_columns = array(); + $new_columns['cb'] = $columns['cb']; + $new_columns['title'] = $columns['title']; + $new_columns['target_sites'] = __( 'Target Sites', 'hi-school-slide-manager' ); + $new_columns['client'] = __( 'Client', 'hi-school-slide-manager' ); + $new_columns['schedule'] = __( 'Schedule', 'hi-school-slide-manager' ); + $new_columns['order'] = __( 'Order', 'hi-school-slide-manager' ); + $new_columns['date'] = $columns['date']; + + return $new_columns; + } + + /** + * Display custom column content + * + * @since 1.0.0 + * @param string $column Column name + * @param int $post_id Post ID + */ + public function custom_column_content( $column, $post_id ) { + switch ( $column ) { + case 'title': + // Add drag handle to title column + echo ''; + break; + + case 'target_sites': + $site_terms = wp_get_post_terms( $post_id, self::SITE_TAXONOMY ); + if ( ! empty( $site_terms ) && ! is_wp_error( $site_terms ) ) { + $site_names = wp_list_pluck( $site_terms, 'name' ); + echo esc_html( implode( ', ', $site_names ) ); + } else { + echo '' . __( 'No sites selected', 'hi-school-slide-manager' ) . ''; + } + break; + + case 'client': + $client_terms = wp_get_post_terms( $post_id, self::CLIENT_TAXONOMY ); + if ( ! empty( $client_terms ) && ! is_wp_error( $client_terms ) ) { + echo esc_html( $client_terms[0]->name ); + } else { + echo '' . __( 'No client', 'hi-school-slide-manager' ) . ''; + } + break; + + case 'schedule': + $start_date = get_post_meta( $post_id, '_hssm_start_date', true ); + $end_date = get_post_meta( $post_id, '_hssm_end_date', true ); + + if ( $start_date || $end_date ) { + echo '
'; + if ( $start_date ) { + echo '' . __( 'Start:', 'hi-school-slide-manager' ) . ' ' . esc_html( date( 'M j, Y g:i A', strtotime( $start_date ) ) ) . '
'; + } + if ( $end_date ) { + echo '' . __( 'End:', 'hi-school-slide-manager' ) . ' ' . esc_html( date( 'M j, Y g:i A', strtotime( $end_date ) ) ); + } + echo '
'; + } else { + echo '' . __( 'No schedule set', 'hi-school-slide-manager' ) . ''; + } + break; + + case 'order': + $order = get_post_meta( $post_id, '_hssm_order', true ); + echo esc_html( $order ?: '0' ); + break; + } + } + + /** + * Send email notification for new slide + * + * @since 1.0.0 + * @param int $post_id Post ID + */ + private function send_new_slide_notification( $post_id ) { + $notification_emails = get_option( 'hssm_notification_emails', array() ); + + if ( empty( $notification_emails ) ) { + return; + } + + $post = get_post( $post_id ); + $subject = sprintf( __( 'New Slide Created: %s', 'hi-school-slide-manager' ), $post->post_title ); + + $message = sprintf( + __( 'A new slide has been created and published on %s.' . "\n\n" . + 'Title: %s' . "\n" . + 'Content: %s' . "\n" . + 'Edit URL: %s', 'hi-school-slide-manager' ), + get_bloginfo( 'name' ), + $post->post_title, + wp_strip_all_tags( $post->post_content ), + admin_url( 'post.php?post=' . $post_id . '&action=edit' ) + ); + + $headers = array( 'Content-Type: text/plain; charset=UTF-8' ); + + foreach ( $notification_emails as $email ) { + wp_mail( $email, $subject, $message, $headers ); + } + } + + /** + * Check for scheduled slides that need status updates + * + * @since 1.0.0 + */ + public function check_scheduled_slides() { + $current_time = current_time( 'mysql' ); + + // Auto-publish slides that should start now + $slides_to_publish = get_posts( array( + 'post_type' => self::POST_TYPE, + 'post_status' => 'draft', + 'meta_query' => array( + 'relation' => 'AND', + array( + 'key' => '_hssm_auto_publish', + 'value' => '1', + 'compare' => '=' + ), + array( + 'key' => '_hssm_start_date', + 'value' => $current_time, + 'compare' => '<=' + ) + ), + 'numberposts' => -1 + ) ); + + foreach ( $slides_to_publish as $slide ) { + wp_update_post( array( + 'ID' => $slide->ID, + 'post_status' => 'publish' + ) ); + } + + // Auto-unpublish slides that should end now + $slides_to_unpublish = get_posts( array( + 'post_type' => self::POST_TYPE, + 'post_status' => 'publish', + 'meta_query' => array( + 'relation' => 'AND', + array( + 'key' => '_hssm_auto_unpublish', + 'value' => '1', + 'compare' => '=' + ), + array( + 'key' => '_hssm_end_date', + 'value' => $current_time, + 'compare' => '<=' + ) + ), + 'numberposts' => -1 + ) ); + + foreach ( $slides_to_unpublish as $slide ) { + wp_update_post( array( + 'ID' => $slide->ID, + 'post_status' => 'draft' + ) ); + } + } + + /** + * Enqueue admin scripts for drag-and-drop functionality + * + * @since 1.0.0 + * @param string $hook Current admin page hook + */ + public function enqueue_admin_scripts( $hook ) { + global $post_type; + + if ( $hook === 'edit.php' && $post_type === self::POST_TYPE ) { + wp_enqueue_script( 'jquery-ui-sortable' ); + wp_enqueue_script( + 'hssm-admin-sortable', + HSSM_PLUGIN_URL . 'assets/js/admin-sortable.js', + array( 'jquery', 'jquery-ui-sortable' ), + HSSM_VERSION, + true + ); + + wp_localize_script( 'hssm-admin-sortable', 'hssmAjax', array( + 'ajaxurl' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'hssm_update_slide_order' ), + 'postType' => self::POST_TYPE + ) ); + + // Add drag handle styles + wp_add_inline_style( 'wp-admin', ' + .wp-list-table .hssm-drag-handle { + cursor: move; + color: #666; + margin-right: 5px; + } + .wp-list-table .hssm-drag-handle:hover { + color: #000; + } + .wp-list-table tbody tr.ui-sortable-helper { + background: #f0f0f0; + border: 2px dashed #0073aa; + } + .wp-list-table tbody tr.ui-sortable-placeholder { + background: #e0e0e0; + height: 40px; + } + ' ); + } + } + + /** + * AJAX handler for updating slide order + * + * @since 1.0.0 + */ + public function ajax_update_slide_order() { + // Verify nonce and permissions + if ( ! wp_verify_nonce( $_POST['nonce'], 'hssm_update_slide_order' ) || ! current_user_can( 'edit_posts' ) ) { + wp_die( 'Unauthorized' ); + } + + if ( isset( $_POST['slide_ids'] ) && is_array( $_POST['slide_ids'] ) ) { + $slide_ids = array_map( 'intval', $_POST['slide_ids'] ); + + foreach ( $slide_ids as $index => $slide_id ) { + update_post_meta( $slide_id, '_hssm_order', $index ); + } + + wp_send_json_success( array( 'message' => __( 'Slide order updated successfully', 'hi-school-slide-manager' ) ) ); + } + + wp_send_json_error( array( 'message' => __( 'Failed to update slide order', 'hi-school-slide-manager' ) ) ); + } +} \ No newline at end of file diff --git a/mimic-carousel.html b/mimic-carousel.html new file mode 100644 index 0000000..e188be4 --- /dev/null +++ b/mimic-carousel.html @@ -0,0 +1,214 @@ + + + + + Carousel Mimic Test + + + + + +

Carousel Mimic for Testing

+ + + + + + diff --git a/mop-up.md b/mop-up.md new file mode 100644 index 0000000..196e6d8 --- /dev/null +++ b/mop-up.md @@ -0,0 +1,7 @@ +# Emergent Tasks + +- Nest pages into slide-dashboard as parent +- Preview banners to preview slides +- check all "Coming Soon!" functionality +- conditional show for empty header fields +- \ No newline at end of file diff --git a/prompt-builder-reference-and-notes.md b/prompt-builder-reference-and-notes.md new file mode 100644 index 0000000..ef7ffc3 --- /dev/null +++ b/prompt-builder-reference-and-notes.md @@ -0,0 +1,19 @@ +# Dynamic Query for Our Slider on the Preview Page + +I need to dynamically filter and override the Divi 5 Group/Carousel module on the "Slide Preview" page based on our URL parameters, or the filters used from our preview dashboard. + +We need to intercept divi's loop query and create our own. The header Currently shows all published slides as intended, so it should be just a matter of finding and replacing the query. + +# Task: Please write a PHP filter in class-hssm-frontend.php to intercept the query args. + +1. **Identify the Hook:** Look for a filter like divi_query_args, et_pb_blog_query_args, or use pre_get_posts as a fallback. Since this is Divi 5, we need to be careful not to break the visual builder, so ensure !is_admin() or !et_fb_is_enabled() checks are used if we go the pre_get_posts route. + +2. **Targeting:** The filter must ONLY apply if: + - We are on the Preview Dashboard page. + - The $_GET parameters (like hssm_filter) are present in the URL. +3. **The Logic:** + - Sanitize the GET parameter. + - Modify the query's tax_query (if filtering by Slide Group) or meta_query (if filtering by Store ID) to match the parameter. +4. **Implementation:** + - Add the code to class-hssm-frontend.php. + - If you are unsure of the exact Divi 5 hook, start by creating a standard pre_get_posts function that targets the main query of that specific page ID. \ No newline at end of file diff --git a/templates/page-slide-confirmation.php b/templates/page-slide-confirmation.php new file mode 100644 index 0000000..c514ee1 --- /dev/null +++ b/templates/page-slide-confirmation.php @@ -0,0 +1,112 @@ +post_type === 'hssm_slide' ); +$confirmed = false; +$error_message = ''; + +// Handle confirmation action +if ( $is_valid_slide && isset( $_POST['confirm_live_action'] ) && $_POST['confirm_live_action'] === 'confirm' ) { + if ( wp_verify_nonce( $_POST['hssm_live_nonce'], 'hssm_confirm_live_action' ) ) { + // Trigger the final notification + if ( class_exists( 'HSSM_Notifications' ) ) { + $notifications = new HSSM_Notifications(); + $notifications->notify_all_confirmed_live( $slide_id ); + + // Mark as confirmed in metadata + update_post_meta( $slide_id, '_hssm_live_confirmed', current_time( 'timestamp' ) ); + $confirmed = true; + } else { + $error_message = 'Notification system not available.'; + } + } else { + $error_message = 'Security check failed. Please refresh and try again.'; + } +} +?> + +
+
+
+ +
+ + +
+

Invalid Slide

+

The slide you are trying to confirm could not be found.

+ Return Home +
+ + +
+

Confirmation Sent!

+

+

Thank you!

+

Notifications have been sent to the Editor and Creator confirming that "post_title ); ?>" is Live.

+ Return Home +
+ + +
+

Confirm Slide is Live

+ + +
+ +
+ + +
+

post_title ); ?>

+ 'names' ) ); + if ( ! empty( $target_sites ) ) { + echo '

Target Sites: ' . esc_html( implode( ', ', $target_sites ) ) . '

'; + } + ?> +
+ +
+
+ +

I confirm that the slide "post_title ); ?>" is Live and displaying correctly on the target sites.

+ +
+ + + + +
+
+ + +
+ +
+
+
+ + diff --git a/templates/page-slide-dashboard.php b/templates/page-slide-dashboard.php new file mode 100644 index 0000000..96476b7 --- /dev/null +++ b/templates/page-slide-dashboard.php @@ -0,0 +1,369 @@ + + +
+
+
+ + +
+
+
+

+ Slide Management Dashboard +

+
+ + +
+
+ + +
+ + +
+ + + + + +
+
+

Create New Slides

+
+ + +
+
+ +
+ +
+ +
+ + + +
+
+ + +
+
+

Manage Existing Slides

+
+ + + + + +
+
+ +
+ +
+
+ + +
+
+

Slide Analytics

+
+ + +
+
+ +
+
+
+

Total Slides Created

+
--
+
+
+

Active Campaigns

+
--
+
+
+

Scheduled Slides

+
--
+
+
+
+
+ + +
+
+

Notification Settings

+
+ +
+
+
+

Editor Notifications

+

Recipients for slide scheduling and approval requests.

+
+ + +
+
+ +
+

I.T.C.Guys Notifications

+

Recipients for LIVE confirmation checks.

+
+ + +
+
+ +
+ +
+
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/templates/page-slide-preview.php b/templates/page-slide-preview.php new file mode 100644 index 0000000..1a8ce99 --- /dev/null +++ b/templates/page-slide-preview.php @@ -0,0 +1,498 @@ +ID, 'full' ); + if ( $img_url ) { + // We use the title to match the slide on the frontend + $bg_data[] = array( + 'title' => html_entity_decode(get_the_title($slide)), + 'bg' => $img_url + ); + } + } + ?> + + + format( 'Y-m-d' ); + $site_param = isset( $_GET['hssm_site'] ) ? sanitize_text_field( wp_unslash( $_GET['hssm_site'] ) ) : ''; + $show_past = isset( $_GET['hssm_past'] ) && $_GET['hssm_past'] === '1'; + + // Validate date (YYYY-MM-DD) + $date_obj = DateTime::createFromFormat( 'Y-m-d', $date_param ); + if ( ! $date_obj ) { + $date_obj = $today; + } + + return array( + 'date' => $date_obj->format( 'Y-m-d' ), + 'site_slug' => $site_param, + 'show_past' => $show_past, + ); +} + +/** + * Get available target sites from the hssm_site taxonomy. + * + * @return array + */ +function hssm_get_preview_sites() { + // We specifically want only these three sites to appear, to match the main dashboard. + // Also helps avoid duplicates if the taxonomy has similar terms. + $allowed_slugs = array( 'hi-school', 'ace', 'one-stop' ); + + $terms = get_terms( + array( + 'taxonomy' => HSSM_Slides_CPT::SITE_TAXONOMY, + 'hide_empty' => false, + 'slug' => $allowed_slugs, + ) + ); + + if ( is_wp_error( $terms ) || empty( $terms ) ) { + return array(); + } + + $sites = array(); + $added_slugs = array(); + + foreach ( $terms as $term ) { + // Double check against duplicates just in case + if ( in_array( $term->slug, $added_slugs, true ) ) { + continue; + } + + $sites[] = array( + 'slug' => $term->slug, + 'name' => $term->name, + ); + $added_slugs[] = $term->slug; + } + + return $sites; +} + +/** + * Get calendar events (start/end dates) for highlighting. + * + * @param string $site_slug Optional site slug to filter by. + * @return array Map of date => ['start', 'end'] + */ +function hssm_get_calendar_events( $site_slug = '' ) { + $meta_query = array( + 'relation' => 'OR', + array( + 'key' => '_hssm_start_date', + 'compare' => 'EXISTS', + ), + array( + 'key' => '_hssm_end_date', + 'compare' => 'EXISTS', + ), + ); + + $tax_query = array(); + + if ( ! empty( $site_slug ) ) { + $site_term = get_term_by( 'slug', $site_slug, HSSM_Slides_CPT::SITE_TAXONOMY ); + if ( $site_term && ! is_wp_error( $site_term ) ) { + $tax_query[] = array( + 'taxonomy' => HSSM_Slides_CPT::SITE_TAXONOMY, + 'field' => 'term_id', + 'terms' => $site_term->term_id, + ); + } + } + + if ( count( $tax_query ) > 1 ) { + $tax_query['relation'] = 'AND'; + } + + $args = array( + 'post_type' => HSSM_Slides_CPT::POST_TYPE, + 'post_status' => array( 'publish', 'future', 'pending' ), + 'posts_per_page' => -1, + 'fields' => 'ids', + 'meta_query' => $meta_query, + ); + + if ( ! empty( $tax_query ) ) { + $args['tax_query'] = $tax_query; + } + + $slide_ids = get_posts( $args ); + $events = array(); + + foreach ( $slide_ids as $slide_id ) { + $start_raw = get_post_meta( $slide_id, '_hssm_start_date', true ); + $end_raw = get_post_meta( $slide_id, '_hssm_end_date', true ); + + if ( ! empty( $start_raw ) ) { + $d = substr( $start_raw, 0, 10 ); + if ( ! isset( $events[ $d ] ) ) $events[ $d ] = array(); + if ( ! in_array( 'start', $events[ $d ], true ) ) $events[ $d ][] = 'start'; + } + + if ( ! empty( $end_raw ) ) { + $d = substr( $end_raw, 0, 10 ); + if ( ! isset( $events[ $d ] ) ) $events[ $d ] = array(); + if ( ! in_array( 'end', $events[ $d ], true ) ) $events[ $d ][] = 'end'; + } + } + + return $events; +} + +/** + * Query slides for a given date and optional site. + * + * @param string $date Date in YYYY-MM-DD format. + * @param string $site_slug Optional site slug. + * @return WP_Post[] + */ +function hssm_get_preview_slides_for_date( $date, $site_slug = '' ) { + $meta_query = array( + 'relation' => 'AND', + array( + 'key' => '_hssm_start_date', + 'value' => $date, + 'compare' => '<=', + ), + 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 ) ) { + $site_term = get_term_by( 'slug', $site_slug, HSSM_Slides_CPT::SITE_TAXONOMY ); + if ( $site_term && ! is_wp_error( $site_term ) ) { + $tax_query[] = array( + 'taxonomy' => HSSM_Slides_CPT::SITE_TAXONOMY, + 'field' => 'term_id', + 'terms' => $site_term->term_id, + ); + } + } + + if ( count( $tax_query ) > 1 ) { + $tax_query['relation'] = 'AND'; + } + + $args = array( + 'post_type' => HSSM_Slides_CPT::POST_TYPE, + 'post_status' => array( 'publish', 'future', 'pending' ), + 'posts_per_page' => -1, + 'orderby' => 'meta_value_num', + 'meta_key' => '_hssm_order', + 'order' => 'ASC', + 'meta_query' => $meta_query, + ); + + if ( ! empty( $tax_query ) ) { + $args['tax_query'] = $tax_query; + } + + return get_posts( $args ); +} + +$request = hssm_get_preview_request(); +$sites = hssm_get_preview_sites(); +$events = hssm_get_calendar_events( $request['site_slug'] ); + +$current_site_name = ''; +if ( ! empty( $request['site_slug'] ) ) { + foreach ( $sites as $site ) { + if ( $site['slug'] === $request['site_slug'] ) { + $current_site_name = $site['name']; + break; + } + } +} + +if ( empty( $current_site_name ) && ! empty( $sites ) ) { + $current_site_name = __( 'All Sites', 'hi-school-slide-manager' ); +} +?> + + + +
+
+
+ + + +
+
+

+ +

+
+ +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+ +
+ +
+
+

+

+
+ ID, '_hssm_subtitle', true ); + $start_date = get_post_meta( $slide->ID, '_hssm_start_date', true ); + $end_date = get_post_meta( $slide->ID, '_hssm_end_date', true ); + $sites_terms = wp_get_post_terms( $slide->ID, HSSM_Slides_CPT::SITE_TAXONOMY ); + $site_names = array(); + if ( ! is_wp_error( $sites_terms ) && ! empty( $sites_terms ) ) { + foreach ( $sites_terms as $term ) { + $site_names[] = $term->name; + } + } + $thumb_url = get_the_post_thumbnail_url( $slide->ID, 'thumbnail' ); + ?> +
+
+
+ +
+ +
+ +
+ + +
+ post_status === 'pending' ) : ?> +
+ + + +
+ + + + + +
+ + + + + + + +
+ + post_status === 'pending' && current_user_can( 'edit_others_posts' ) ) : ?> +
+ +
+ +
+ + +
+   +
+ +
+ +
+ +
+ + + +
+
+ +
+
+
+ +