Restore to commit 74e578279624c6045ca440a3459ebfa1f8d54191

This commit is contained in:
southseact-3d
2026-02-07 20:32:41 +00:00
commit ed67b7741b
252 changed files with 99814 additions and 0 deletions

View File

@@ -0,0 +1,125 @@
# PC Announcements 274
A comprehensive WordPress plugin for creating and managing announcements that display at the top of public pages with scheduling capabilities.
## Features
- **Admin Management**: Create, edit, delete, and schedule announcements through an intuitive admin interface
- **Scheduling**: Set start and end dates for announcements with automatic activation/deactivation
- **Modern Frontend Display**: Responsive, accessible announcement banners with smooth animations
- **Security**: Built with WordPress security best practices including nonce verification and capability checks
- **Customizable**: Easily styled with CSS custom properties and modern design patterns
- **Mobile Responsive**: Optimized for all screen sizes with mobile-first approach
## Installation
1. Upload the `pc-announcements-274` folder to your WordPress `/wp-content/plugins/` directory
2. Activate the plugin through the WordPress admin "Plugins" menu
3. Navigate to "Announcements" in the WordPress admin to create your first announcement
## Usage
### Creating Announcements
1. Go to **Announcements → Add New** in the WordPress admin
2. Enter a title and message for your announcement
3. Optionally add an image URL
4. Set scheduling dates (start/end) if needed
5. Set the status (active, inactive, or scheduled)
6. Click "Create Announcement"
### Managing Announcements
- View all announcements at **Announcements → All Announcements**
- Edit existing announcements by clicking the "Edit" button
- Delete announcements using the "Delete" button with confirmation
- See real-time status indicators for active announcements
### Frontend Display
Announcements automatically appear at the top of all public pages when:
- The announcement status is set to "Active"
- The current time is within the scheduled start and end dates
- The announcement hasn't been dismissed by the user
## Customization
### CSS Custom Properties
You can customize the appearance using these CSS variables:
```css
:root {
--pc-announcements-274-bg-primary: #0d47a1;
--pc-announcements-274-bg-secondary: #1565c0;
--pc-announcements-274-text-primary: #ffffff;
--pc-announcements-274-text-secondary: rgba(255, 255, 255, 0.9);
--pc-announcements-274-border-radius: 8px;
--pc-announcements-274-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto;
}
```
### Theme Variations
Add these classes to customize appearance:
- `.pc-announcements-274-success` - Green theme
- `.pc-announcements-274-warning` - Orange theme
- `.pc-announcements-274-error` - Red theme
- `.pc-announcements-274-info` - Light blue theme
- `.pc-announcements-274-compact` - Smaller, compact version
- `.pc-announcements-274-no-image` - Hide image and optimize layout
## Security Features
- **Capability Checks**: Only administrators can manage announcements
- **Nonce Verification**: All AJAX requests protected with WordPress nonces
- **Input Sanitization**: All user inputs properly sanitized and escaped
- **Database Security**: Prepared statements used for all database operations
- **CSRF Protection**: Built-in CSRF protection for form submissions
## Accessibility
- WCAG 2.1 AA compliant design
- Keyboard navigation support
- Screen reader compatibility
- High contrast mode support
- Reduced motion preferences respected
- Focus management for dynamic content
## Browser Support
- Chrome 60+
- Firefox 55+
- Safari 12+
- Edge 79+
- Mobile browsers (iOS Safari 12+, Android Chrome 60+)
## Technical Details
- **PHP Version**: 7.4+
- **WordPress Version**: 5.0+
- **Database**: Uses custom table with proper indexing
- **Performance**: Optimized queries with caching considerations
- **Memory**: Minimal memory footprint
- **Standards**: Follows WordPress coding standards and best practices
## Changelog
### Version 1.0.0
- Initial release
- Core announcement management functionality
- Admin interface with CRUD operations
- Frontend display with responsive design
- Scheduling and status management
- Security and accessibility features
## Support
For support, documentation, and updates:
- Plugin URL: https://plugincompass.com/plugins/pc-announcements-274
- Author: Plugin Compass
- Author URI: https://plugincompass.com
## License
This plugin is licensed under the GPL-2.0-or-later license.

View File

@@ -0,0 +1,238 @@
<?php
/**
* Admin class for managing announcements in WordPress admin
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
class PC_Announcements_274_Admin {
/**
* Constructor
*/
public function __construct() {
add_action('admin_menu', array($this, 'add_admin_menu'));
add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts'));
add_action('wp_ajax_pc_announcements_274_action', array($this, 'handle_ajax_requests'));
}
/**
* Add admin menu items
*/
public function add_admin_menu() {
add_menu_page(
__('Announcements', 'pc-announcements-274'),
__('Announcements', 'pc-announcements-274'),
'manage_options',
'pc-announcements-274',
array($this, 'render_announcements_page'),
'dashicons-megaphone',
25
);
add_submenu_page(
'pc-announcements-274',
__('All Announcements', 'pc-announcements-274'),
__('All Announcements', 'pc-announcements-274'),
'manage_options',
'pc-announcements-274',
array($this, 'render_announcements_page')
);
add_submenu_page(
'pc-announcements-274',
__('Add New', 'pc-announcements-274'),
__('Add New', 'pc-announcements-274'),
'manage_options',
'pc-announcements-274-add',
array($this, 'render_add_announcement_page')
);
}
/**
* Enqueue admin scripts and styles
*/
public function enqueue_admin_scripts($hook) {
if (strpos($hook, 'pc-announcements-274') === false) {
return;
}
wp_enqueue_style('pc-announcements-274-admin-style', PC_ANNOUNCEMENTS_274_PLUGIN_URL . 'admin/css/admin-style.css', array('wp-admin'), PC_ANNOUNCEMENTS_274_VERSION);
wp_enqueue_media();
wp_enqueue_script('pc-announcements-274-admin-script', PC_ANNOUNCEMENTS_274_PLUGIN_URL . 'admin/js/admin-script.js', array('jquery'), PC_ANNOUNCEMENTS_274_VERSION, true);
wp_localize_script('pc-announcements-274-admin-script', 'pc_announcements_274_ajax', array(
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('pc_announcements_274_nonce'),
'i18n' => array(
'error_occurred' => __('An error occurred. Please try again.', 'pc-announcements-274'),
'choose_image' => __('Choose Image', 'pc-announcements-274'),
'preview' => __('Preview', 'pc-announcements-274'),
'end_date_warning' => __('End date should be after start date.', 'pc-announcements-274')
)
));
}
/**
* Render main announcements page
*/
public function render_announcements_page() {
$current_page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1;
$per_page = 20;
$offset = ($current_page - 1) * $per_page;
global $wpdb;
$table_name = PC_Announcements_274_Install::get_table_name();
if (empty($table_name)) {
include PC_ANNOUNCEMENTS_274_PLUGIN_DIR . 'admin/templates/error-page.php';
return;
}
// Get total count
$total = $wpdb->get_var("SELECT COUNT(*) FROM $table_name");
if ($total === null) {
$total = 0;
}
$total_pages = ceil($total / $per_page);
// Get announcements
$announcements = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM $table_name ORDER BY created_at DESC LIMIT %d OFFSET %d",
$per_page, $offset
));
if ($announcements === null) {
$announcements = array();
}
include PC_ANNOUNCEMENTS_274_PLUGIN_DIR . 'admin/templates/list-page.php';
}
/**
* Render add new announcement page
*/
public function render_add_announcement_page() {
$announcement_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
$announcement = null;
if ($announcement_id > 0) {
global $wpdb;
$table_name = PC_Announcements_274_Install::get_table_name();
if (!empty($table_name)) {
$announcement = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table_name WHERE id = %d", $announcement_id));
}
}
include PC_ANNOUNCEMENTS_274_PLUGIN_DIR . 'admin/templates/edit-page.php';
}
/**
* Handle AJAX requests
*/
public function handle_ajax_requests() {
check_ajax_referer('pc_announcements_274_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die(__('You do not have sufficient permissions.', 'pc-announcements-274'));
}
$action = isset($_POST['sub_action']) ? sanitize_text_field($_POST['sub_action']) : '';
switch ($action) {
case 'save_announcement':
$this->save_announcement();
break;
case 'delete_announcement':
$this->delete_announcement();
break;
default:
wp_send_json_error(array('message' => __('Invalid action.', 'pc-announcements-274')));
}
}
/**
* Save announcement
*/
private function save_announcement() {
global $wpdb;
$table_name = PC_Announcements_274_Install::get_table_name();
$id = isset($_POST['id']) ? intval($_POST['id']) : 0;
$title = sanitize_text_field($_POST['title']);
$message = wp_kses_post($_POST['message']);
$banner_color = sanitize_hex_color($_POST['banner_color']);
$link_url = esc_url_raw($_POST['link_url']);
$image_url = esc_url_raw($_POST['image_url']);
$start_date = !empty($_POST['start_date']) ? sanitize_text_field($_POST['start_date']) : null;
$end_date = !empty($_POST['end_date']) ? sanitize_text_field($_POST['end_date']) : null;
$status = sanitize_text_field($_POST['status']);
if (empty($title)) {
wp_send_json_error(array('message' => __('Title is required.', 'pc-announcements-274')));
}
if (empty($banner_color)) {
$banner_color = '#0d47a1';
}
$data = array(
'title' => $title,
'message' => $message,
'banner_color' => $banner_color,
'link_url' => $link_url,
'image_url' => $image_url,
'start_date' => $start_date,
'end_date' => $end_date,
'status' => $status,
'updated_at' => current_time('mysql')
);
$format = array('%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s');
if ($id > 0) {
$result = $wpdb->update($table_name, $data, array('id' => $id), $format, array('%d'));
} else {
$data['created_at'] = current_time('mysql');
$data['created_by'] = get_current_user_id();
$format = array('%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s');
$result = $wpdb->insert($table_name, $data, $format);
$id = $wpdb->insert_id;
}
if ($result === false) {
wp_send_json_error(array('message' => __('Failed to save announcement.', 'pc-announcements-274')));
}
wp_send_json_success(array(
'message' => $id > 0 && isset($_POST['id']) ? __('Announcement updated successfully!', 'pc-announcements-274') : __('Announcement created successfully!', 'pc-announcements-274'),
'id' => $id
));
}
/**
* Delete announcement
*/
private function delete_announcement() {
global $wpdb;
$table_name = PC_Announcements_274_Install::get_table_name();
$id = intval($_POST['id']);
if ($id <= 0) {
wp_send_json_error(array('message' => __('Invalid announcement ID.', 'pc-announcements-274')));
}
$result = $wpdb->delete($table_name, array('id' => $id), array('%d'));
if ($result === false) {
wp_send_json_error(array('message' => __('Failed to delete announcement.', 'pc-announcements-274')));
}
wp_send_json_success(array('message' => __('Announcement deleted successfully!', 'pc-announcements-274')));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,199 @@
jQuery(document).ready(function($) {
'use strict';
// Form submission
$('#pc-announcements-274-form').on('submit', function(e) {
e.preventDefault();
var $form = $(this);
var $submitBtn = $form.find('button[type="submit"]');
var originalText = $submitBtn.text();
// Show loading state
$submitBtn.prop('disabled', true).text('Saving...');
$.ajax({
url: pc_announcements_274_ajax.ajax_url,
type: 'POST',
data: $form.serialize(),
dataType: 'json',
success: function(response) {
if (response.success) {
// Show success message
$('<div class="notice notice-success is-dismissible"><p>' + response.data.message + '</p></div>')
.insertAfter('.wp-header-end')
.delay(3000)
.fadeOut(function() {
$(this).remove();
});
// Redirect to list page
setTimeout(function() {
window.location.href = 'admin.php?page=pc-announcements-274&message=success';
}, 1000);
} else {
// Show error message
$('<div class="notice notice-error is-dismissible"><p>' + response.data.message + '</p></div>')
.insertAfter('.wp-header-end');
}
},
error: function() {
$('<div class="notice notice-error is-dismissible"><p>' + pc_announcements_274_ajax.i18n.error_occurred + '</p></div>')
.insertAfter('.wp-header-end');
},
complete: function() {
// Restore button state
$submitBtn.prop('disabled', false).text(originalText);
}
});
});
// Delete confirmation
$('.pc-announcements-274-delete-btn').on('click', function(e) {
e.preventDefault();
var announcementId = $(this).data('id');
var $modal = $('#pc-announcements-274-delete-modal');
// Show modal
$modal.show();
// Handle delete confirmation
$modal.find('.pc-announcements-274-confirm-delete').off('click').on('click', function() {
$.ajax({
url: pc_announcements_274_ajax.ajax_url,
type: 'POST',
data: {
action: 'pc_announcements_274_action',
sub_action: 'delete_announcement',
id: announcementId,
nonce: pc_announcements_274_ajax.nonce
},
dataType: 'json',
success: function(response) {
if (response.success) {
// Remove the row from table
$('button[data-id="' + announcementId + '"]').closest('tr').fadeOut(function() {
$(this).remove();
// Show empty state if no items left
if ($('.pc-announcements-274-wrap .wp-list-table tbody tr').length === 0) {
location.reload();
}
});
// Show success message
$('<div class="notice notice-success is-dismissible"><p>' + response.data.message + '</p></div>')
.insertAfter('.wp-header-end')
.delay(3000)
.fadeOut(function() {
$(this).remove();
});
} else {
// Show error message
$('<div class="notice notice-error is-dismissible"><p>' + response.data.message + '</p></div>')
.insertAfter('.wp-header-end');
}
},
error: function() {
$('<div class="notice notice-error is-dismissible"><p>' + pc_announcements_274_ajax.i18n.error_occurred + '</p></div>')
.insertAfter('.wp-header-end');
},
complete: function() {
// Hide modal
$modal.hide();
}
});
});
});
// Close modal handlers
$('#pc-announcements-274-delete-modal').on('click', '.pc-announcements-274-cancel-delete, .pc-announcements-274-modal-backdrop', function() {
$('#pc-announcements-274-delete-modal').hide();
});
// Close modal with Escape key
$(document).on('keydown', function(e) {
if (e.keyCode === 27) { // Escape key
$('#pc-announcements-274-delete-modal').hide();
}
});
// Media upload functionality
var customUploader;
$('.pc-announcements-274-upload-image-btn').on('click', function(e) {
e.preventDefault();
var $button = $(this);
var $inputField = $button.siblings('input[type="url"]');
// If the uploader object has already been created, reopen the dialog
if (customUploader) {
customUploader.open();
return;
}
// Extend the wp.media object
customUploader = wp.media.frames.file_frame = wp.media({
title: pc_announcements_274_ajax.i18n.choose_image,
button: {
text: pc_announcements_274_ajax.i18n.choose_image
},
multiple: false
});
// When a file is selected, grab the URL and set it as the text field's value
customUploader.on('select', function() {
var attachment = customUploader.state().get('selection').first().toJSON();
$inputField.val(attachment.url);
// Update preview if exists
var $preview = $inputField.siblings('.pc-announcements-274-image-preview');
if ($preview.length === 0) {
$preview = $('<div class="pc-announcements-274-image-preview"></div>').insertAfter($inputField.parent());
}
$preview.html('<img src="' + attachment.url + '" alt="' + pc_announcements_274_ajax.i18n.preview + '" style="max-width: 200px; height: auto;">');
});
// Open the uploader dialog
customUploader.open();
});
// Auto-hide notices
$('.notice.is-dismissible').on('click', '.notice-dismiss', function() {
$(this).closest('.notice').fadeOut(function() {
$(this).remove();
});
});
// Image URL field change handler
$('input[name="image_url"]').on('input', function() {
var url = $(this).val();
var $preview = $(this).siblings('.pc-announcements-274-image-preview');
if (url) {
if ($preview.length === 0) {
$preview = $('<div class="pc-announcements-274-image-preview"></div>').insertAfter($(this).parent());
}
$preview.html('<img src="' + url + '" alt="' + pc_announcements_274_ajax.i18n.preview + '" style="max-width: 200px; height: auto;" onerror="this.style.display=\'none\'">');
} else if ($preview.length > 0) {
$preview.empty();
}
});
// Date/time validation
$('#start_date, #end_date').on('change', function() {
var startDate = $('#start_date').val();
var endDate = $('#end_date').val();
if (startDate && endDate && new Date(startDate) >= new Date(endDate)) {
$('<div class="notice notice-warning is-dismissible"><p>' + pc_announcements_274_ajax.i18n.end_date_warning + '</p></div>')
.insertAfter('.wp-header-end')
.delay(5000)
.fadeOut(function() {
$(this).remove();
});
}
});
});

View File

@@ -0,0 +1,197 @@
<?php
/**
* Edit/Add announcement page template
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
$is_edit = !empty($announcement) && isset($announcement->id) && $announcement->id > 0;
$page_title = $is_edit ? __('Edit Announcement', 'pc-announcements-274') : __('Add New Announcement', 'pc-announcements-274');
?>
<div class="wrap pc-announcements-274-wrap">
<h1 class="wp-heading-inline">
<?php echo esc_html($page_title); ?>
</h1>
<a href="<?php echo admin_url('admin.php?page=pc-announcements-274'); ?>" class="page-title-action">
<?php _e('Back to List', 'pc-announcements-274'); ?>
</a>
<hr class="wp-header-end">
<form id="pc-announcements-274-form" class="pc-announcements-274-form">
<div class="pc-announcements-274-main-content">
<div class="pc-announcements-274-card">
<div class="pc-announcements-274-card-header">
<h2><?php _e('Announcement Details', 'pc-announcements-274'); ?></h2>
</div>
<div class="pc-announcements-274-card-body">
<div class="pc-announcements-274-form-row">
<div class="pc-announcements-274-form-group">
<label for="title" class="pc-announcements-274-label">
<?php _e('Title', 'pc-announcements-274'); ?> <span class="required">*</span>
</label>
<input type="text" id="title" name="title" class="regular-text" required
value="<?php echo $is_edit ? esc_attr($announcement->title) : ''; ?>"
placeholder="<?php _e('Enter announcement title', 'pc-announcements-274'); ?>">
</div>
</div>
<div class="pc-announcements-274-form-row">
<div class="pc-announcements-274-form-group">
<label for="banner_color" class="pc-announcements-274-label">
<?php _e('Banner Color', 'pc-announcements-274'); ?>
</label>
<div class="pc-announcements-274-color-picker">
<input type="color" id="banner_color" name="banner_color"
value="<?php echo $is_edit ? esc_attr($announcement->banner_color) : '#0d47a1'; ?>"
style="width: 60px; height: 40px; padding: 2px; cursor: pointer;">
<input type="text" id="banner_color_text" class="regular-text"
value="<?php echo $is_edit ? esc_attr($announcement->banner_color) : '#0d47a1'; ?>"
placeholder="#0d47a1">
</div>
<p class="description">
<?php _e('Choose the background color for the announcement banner', 'pc-announcements-274'); ?>
</p>
</div>
</div>
<div class="pc-announcements-274-form-row">
<div class="pc-announcements-274-form-group">
<label for="link_url" class="pc-announcements-274-label">
<?php _e('Link URL', 'pc-announcements-274'); ?>
</label>
<input type="url" id="link_url" name="link_url" class="regular-text"
value="<?php echo $is_edit ? esc_url($announcement->link_url) : ''; ?>"
placeholder="<?php _e('https://example.com', 'pc-announcements-274'); ?>">
<p class="description">
<?php _e('Enter a URL to make the announcement banner clickable', 'pc-announcements-274'); ?>
</p>
</div>
</div>
<div class="pc-announcements-274-form-row">
<div class="pc-announcements-274-form-group">
<label for="image_url" class="pc-announcements-274-label">
<?php _e('Image URL', 'pc-announcements-274'); ?>
</label>
<div class="pc-announcements-274-media-upload">
<input type="url" id="image_url" name="image_url" class="regular-text"
value="<?php echo $is_edit ? esc_url($announcement->image_url) : ''; ?>"
placeholder="<?php _e('https://example.com/image.jpg', 'pc-announcements-274'); ?>">
<button type="button" class="button pc-announcements-274-upload-image-btn">
<?php _e('Upload Image', 'pc-announcements-274'); ?>
</button>
</div>
<?php if ($is_edit && $announcement->image_url): ?>
<div class="pc-announcements-274-image-preview">
<img src="<?php echo esc_url($announcement->image_url); ?>" alt="<?php _e('Preview', 'pc-announcements-274'); ?>" style="max-width: 200px; height: auto;">
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<div class="pc-announcements-274-card">
<div class="pc-announcements-274-card-header">
<h2><?php _e('Scheduling', 'pc-announcements-274'); ?></h2>
</div>
<div class="pc-announcements-274-card-body">
<div class="pc-announcements-274-form-row">
<div class="pc-announcements-274-form-group">
<label for="status" class="pc-announcements-274-label">
<?php _e('Status', 'pc-announcements-274'); ?>
</label>
<select id="status" name="status" class="regular-text">
<option value="active" <?php echo ($is_edit && $announcement->status === 'active') ? 'selected' : ''; ?>>
<?php _e('Active', 'pc-announcements-274'); ?>
</option>
<option value="inactive" <?php echo ($is_edit && $announcement->status === 'inactive') ? 'selected' : ''; ?>>
<?php _e('Inactive', 'pc-announcements-274'); ?>
</option>
<option value="scheduled" <?php echo ($is_edit && $announcement->status === 'scheduled') ? 'selected' : ''; ?>>
<?php _e('Scheduled', 'pc-announcements-274'); ?>
</option>
</select>
</div>
</div>
<div class="pc-announcements-274-form-row">
<div class="pc-announcements-274-form-group">
<label for="start_date" class="pc-announcements-274-label">
<?php _e('Start Date', 'pc-announcements-274'); ?>
</label>
<input type="datetime-local" id="start_date" name="start_date"
value="<?php echo $is_edit && $announcement->start_date ? date('Y-m-d\TH:i', strtotime($announcement->start_date)) : ''; ?>">
<p class="description">
<?php _e('Leave empty to start immediately', 'pc-announcements-274'); ?>
</p>
</div>
<div class="pc-announcements-274-form-group">
<label for="end_date" class="pc-announcements-274-label">
<?php _e('End Date', 'pc-announcements-274'); ?>
</label>
<input type="datetime-local" id="end_date" name="end_date"
value="<?php echo $is_edit && $announcement->end_date ? date('Y-m-d\TH:i', strtotime($announcement->end_date)) : ''; ?>">
<p class="description">
<?php _e('Leave empty to show indefinitely', 'pc-announcements-274'); ?>
</p>
</div>
</div>
</div>
</div>
</div>
<div class="pc-announcements-274-sidebar">
<div class="pc-announcements-274-card">
<div class="pc-announcements-274-card-header">
<h2><?php _e('Publish', 'pc-announcements-274'); ?></h2>
</div>
<div class="pc-announcements-274-card-body">
<input type="hidden" name="id" value="<?php echo $is_edit ? $announcement->id : 0; ?>">
<input type="hidden" name="action" value="pc_announcements_274_action">
<input type="hidden" name="sub_action" value="save_announcement">
<?php wp_nonce_field('pc_announcements_274_nonce', 'nonce'); ?>
<div class="pc-announcements-274-publish-actions">
<button type="submit" class="button button-primary button-large">
<?php echo $is_edit ? __('Update Announcement', 'pc-announcements-274') : __('Create Announcement', 'pc-announcements-274'); ?>
</button>
<a href="<?php echo admin_url('admin.php?page=pc-announcements-274'); ?>" class="button">
<?php _e('Cancel', 'pc-announcements-274'); ?>
</a>
</div>
<?php if ($is_edit): ?>
<div class="pc-announcements-274-form-info">
<p><strong><?php _e('Created:', 'pc-announcements-274'); ?></strong> <?php echo date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($announcement->created_at)); ?></p>
<?php if ($announcement->updated_at !== $announcement->created_at): ?>
<p><strong><?php _e('Last Updated:', 'pc-announcements-274'); ?></strong> <?php echo date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($announcement->updated_at)); ?></p>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</div>
</div>
</form>
</div>
<div id="pc-announcements-274-delete-modal" class="pc-announcements-274-modal" style="display: none;">
<div class="pc-announcements-274-modal-backdrop"></div>
<div class="pc-announcements-274-modal-content">
<div class="pc-announcements-274-modal-header">
<h3><?php _e('Delete Announcement', 'pc-announcements-274'); ?></h3>
</div>
<div class="pc-announcements-274-modal-body">
<p><?php _e('Are you sure you want to delete this announcement? This action cannot be undone.', 'pc-announcements-274'); ?></p>
</div>
<div class="pc-announcements-274-modal-footer">
<button type="button" class="button pc-announcements-274-cancel-delete"><?php _e('Cancel', 'pc-announcements-274'); ?></button>
<button type="button" class="button button-danger pc-announcements-274-confirm-delete"><?php _e('Delete', 'pc-announcements-274'); ?></button>
</div>
</div>
</div>

View File

@@ -0,0 +1,30 @@
<?php
/**
* Error page template for database table issues
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
?>
<div class="wrap pc-announcements-274-wrap">
<h1 class="wp-heading-inline">
<?php _e('Announcements', 'pc-announcements-274'); ?>
</h1>
<hr class="wp-header-end">
<div class="pc-announcements-274-card">
<div class="pc-announcements-274-card-header">
<h2><?php _e('Database Error', 'pc-announcements-274'); ?></h2>
</div>
<div class="pc-announcements-274-card-body">
<div class="pc-announcements-274-empty-state">
<div class="pc-announcements-274-empty-icon">⚠️</div>
<h3><?php _e('Database table not found', 'pc-announcements-274'); ?></h3>
<p><?php _e('The announcements database table has not been created yet. Please try deactivating and reactivating the plugin.', 'pc-announcements-274'); ?></p>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,133 @@
<?php
/**
* List page template for announcements
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
?>
<div class="wrap pc-announcements-274-wrap">
<h1 class="wp-heading-inline">
<?php _e('Announcements', 'pc-announcements-274'); ?>
</h1>
<a href="<?php echo admin_url('admin.php?page=pc-announcements-274-add'); ?>" class="page-title-action">
<?php _e('Add New', 'pc-announcements-274'); ?>
</a>
<hr class="wp-header-end">
<?php if (isset($_GET['message']) && $_GET['message'] === 'success'): ?>
<div class="notice notice-success is-dismissible">
<p><?php _e('Announcement saved successfully!', 'pc-announcements-274'); ?></p>
</div>
<?php endif; ?>
<div class="pc-announcements-274-card">
<div class="pc-announcements-274-card-header">
<h2><?php _e('All Announcements', 'pc-announcements-274'); ?></h2>
</div>
<div class="pc-announcements-274-card-body">
<?php if (!empty($announcements) && is_array($announcements)): ?>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th scope="col" class="manage-column column-title"><?php _e('Title', 'pc-announcements-274'); ?></th>
<th scope="col" class="manage-column column-status"><?php _e('Status', 'pc-announcements-274'); ?></th>
<th scope="col" class="manage-column column-date"><?php _e('Schedule', 'pc-announcements-274'); ?></th>
<th scope="col" class="manage-column column-date"><?php _e('Created', 'pc-announcements-274'); ?></th>
<th scope="col" class="manage-column column-actions"><?php _e('Actions', 'pc-announcements-274'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($announcements as $announcement): ?>
<?php
$current_time = current_time('timestamp');
$start_timestamp = $announcement->start_date ? strtotime($announcement->start_date) : 0;
$end_timestamp = $announcement->end_date ? strtotime($announcement->end_date) : 9999999999;
$is_active = $announcement->status === 'active' &&
(!$start_timestamp || $current_time >= $start_timestamp) &&
(!$end_timestamp || $current_time <= $end_timestamp);
?>
<tr>
<td class="column-title">
<strong>
<a href="<?php echo admin_url('admin.php?page=pc-announcements-274-add&id=' . $announcement->id); ?>">
<?php echo esc_html($announcement->title); ?>
</a>
</strong>
<?php if ($is_active): ?>
<span class="pc-announcements-274-active-badge"><?php _e('Active', 'pc-announcements-274'); ?></span>
<?php endif; ?>
</td>
<td class="column-status">
<span class="pc-announcements-274-status pc-announcements-274-status-<?php echo esc_attr($announcement->status); ?>">
<?php
$status_labels = array(
'active' => __('Active', 'pc-announcements-274'),
'inactive' => __('Inactive', 'pc-announcements-274'),
'scheduled' => __('Scheduled', 'pc-announcements-274')
);
echo esc_html($status_labels[$announcement->status] ?? $announcement->status);
?>
</span>
</td>
<td class="column-date">
<?php if ($announcement->start_date): ?>
<?php _e('From', 'pc-announcements-274'); ?> <?php echo date_i18n(get_option('date_format'), strtotime($announcement->start_date)); ?><br>
<?php endif; ?>
<?php if ($announcement->end_date): ?>
<?php _e('Until', 'pc-announcements-274'); ?> <?php echo date_i18n(get_option('date_format'), strtotime($announcement->end_date)); ?>
<?php endif; ?>
<?php if (!$announcement->start_date && !$announcement->end_date): ?>
<?php _e('Always', 'pc-announcements-274'); ?>
<?php endif; ?>
</td>
<td class="column-date">
<?php echo date_i18n(get_option('date_format'), strtotime($announcement->created_at)); ?>
</td>
<td class="column-actions">
<a href="<?php echo admin_url('admin.php?page=pc-announcements-274-add&id=' . $announcement->id); ?>" class="button">
<?php _e('Edit', 'pc-announcements-274'); ?>
</a>
<button class="button button-danger pc-announcements-274-delete-btn" data-id="<?php echo $announcement->id; ?>">
<?php _e('Delete', 'pc-announcements-274'); ?>
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php if (!empty($total_pages) && $total_pages > 1): ?>
<div class="tablenav bottom">
<div class="tablenav-pages">
<?php
$current_url = admin_url('admin.php?page=pc-announcements-274');
echo paginate_links(array(
'base' => add_query_arg('paged', '%#%', $current_url),
'format' => '',
'prev_text' => __('&laquo; Previous', 'pc-announcements-274'),
'next_text' => __('Next &raquo;', 'pc-announcements-274'),
'total' => $total_pages,
'current' => $current_page,
));
?>
</div>
</div>
<?php endif; ?>
<?php else: ?>
<div class="pc-announcements-274-empty-state">
<div class="pc-announcements-274-empty-icon">📢</div>
<h3><?php _e('No announcements found', 'pc-announcements-274'); ?></h3>
<p><?php _e('Create your first announcement to get started.', 'pc-announcements-274'); ?></p>
<a href="<?php echo admin_url('admin.php?page=pc-announcements-274-add'); ?>" class="button button-primary">
<?php _e('Add New Announcement', 'pc-announcements-274'); ?>
</a>
</div>
<?php endif; ?>
</div>
</div>
</div>

View File

@@ -0,0 +1,74 @@
<?php
/**
* Installation class for database setup and cleanup
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
class PC_Announcements_274_Install {
/**
* Install plugin - create database tables and set default options
*/
public static function install() {
global $wpdb;
// Ensure plugin constants are defined
if (!defined('PC_ANNOUNCEMENTS_274_VERSION')) {
define('PC_ANNOUNCEMENTS_274_VERSION', '1.0.0');
}
if (!function_exists('dbDelta')) {
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
}
$table_name = $wpdb->prefix . 'pc_announcements_274';
$charset_collate = '';
if (method_exists($wpdb, 'get_charset_collate')) {
$charset_collate = $wpdb->get_charset_collate();
}
if (empty($charset_collate)) {
$charset_collate = 'DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci';
}
$sql = "CREATE TABLE $table_name (
id int(11) NOT NULL AUTO_INCREMENT,
title varchar(255) NOT NULL,
message text NOT NULL,
image_url varchar(500) DEFAULT NULL,
banner_color varchar(7) DEFAULT '#0d47a1',
link_url varchar(500) DEFAULT NULL,
start_date datetime DEFAULT NULL,
end_date datetime DEFAULT NULL,
status varchar(20) DEFAULT 'active',
created_at datetime DEFAULT CURRENT_TIMESTAMP,
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
created_by int(11) NOT NULL,
PRIMARY KEY (id),
KEY status (status),
KEY start_date (start_date),
KEY end_date (end_date)
) $charset_collate;";
dbDelta($sql);
add_option('pc_announcements_274_version', PC_ANNOUNCEMENTS_274_VERSION);
add_option('pc_announcements_274_db_version', '1.0');
}
/**
* Get table name
*/
public static function get_table_name() {
global $wpdb;
if (!isset($wpdb)) {
return null;
}
return $wpdb->prefix . 'pc_announcements_274';
}
}

View File

@@ -0,0 +1,126 @@
<?php
/**
* Plugin Name: Announcements Manager
* Plugin URI: https://plugincompass.com/plugins/pc-announcements-274
* Description: Create and manage announcements that display at the top of public pages with scheduling capabilities.
* Version: 1.0.0
* Author: Plugin Compass
* Author URI: https://plugincompass.com
* License: GPL-2.0-or-later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: pc-announcements-274
* Domain Path: /languages
* Requires at least: 5.0
* Requires PHP: 7.4
* Update URI: false
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
// Define plugin constants
define('PC_ANNOUNCEMENTS_274_VERSION', '1.0.0');
define('PC_ANNOUNCEMENTS_274_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('PC_ANNOUNCEMENTS_274_PLUGIN_URL', plugin_dir_url(__FILE__));
define('PC_ANNOUNCEMENTS_274_PLUGIN_BASENAME', plugin_basename(__FILE__));
// Prevent WordPress.org update checks
add_filter('site_transient_update_plugins', function($value) {
$plugin_file = plugin_basename(__FILE__);
if (isset($value->response[$plugin_file])) {
unset($value->response[$plugin_file]);
}
return $value;
});
/**
* Main plugin class
*/
class PC_Announcements_274 {
/**
* Single instance of the class
*/
private static $instance = null;
/**
* Get single instance
*/
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
add_action('plugins_loaded', array($this, 'init'));
}
/**
* Initialize plugin
*/
public function init() {
// Load text domain
load_plugin_textdomain('pc-announcements-274', false, dirname(PC_ANNOUNCEMENTS_274_PLUGIN_BASENAME) . '/languages');
// Include required files
$this->includes();
// Initialize admin
if (is_admin()) {
new PC_Announcements_274_Admin();
}
// Initialize frontend
new PC_Announcements_274_Frontend();
}
/**
* Include required files
*/
private function includes() {
$files = array(
PC_ANNOUNCEMENTS_274_PLUGIN_DIR . 'includes/class-install.php',
PC_ANNOUNCEMENTS_274_PLUGIN_DIR . 'admin/class-admin.php',
PC_ANNOUNCEMENTS_274_PLUGIN_DIR . 'public/class-frontend.php'
);
foreach ($files as $file_path) {
if (file_exists($file_path)) {
require_once $file_path;
}
}
}
/**
* Activate plugin
*/
public static function activate() {
// Ensure install class is available during activation (plugins_loaded hasn't run yet)
if (!class_exists('PC_Announcements_274_Install')) {
require_once PC_ANNOUNCEMENTS_274_PLUGIN_DIR . 'includes/class-install.php';
}
PC_Announcements_274_Install::install();
flush_rewrite_rules();
}
/**
* Deactivate plugin
*/
public static function deactivate() {
flush_rewrite_rules();
}
}
// Initialize plugin
PC_Announcements_274::get_instance();
// Register activation and deactivation hooks
register_activation_hook(__FILE__, array('PC_Announcements_274', 'activate'));
register_deactivation_hook(__FILE__, array('PC_Announcements_274', 'deactivate'));

View File

@@ -0,0 +1,239 @@
<?php
/**
* Frontend class for displaying announcements on public pages
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
class PC_Announcements_274_Frontend {
/**
* Constructor
*/
public function __construct() {
add_action('wp_enqueue_scripts', array($this, 'enqueue_frontend_scripts'));
add_action('wp_head', array($this, 'display_announcements'), 1);
add_shortcode('pc_announcements_274', array($this, 'get_announcements_for_shortcode'));
}
/**
* Enqueue frontend scripts and styles
*/
public function enqueue_frontend_scripts() {
wp_enqueue_style('pc-announcements-274-public-style', PC_ANNOUNCEMENTS_274_PLUGIN_URL . 'public/css/public-style.css', array(), PC_ANNOUNCEMENTS_274_VERSION);
wp_enqueue_script('pc-announcements-274-public-script', PC_ANNOUNCEMENTS_274_PLUGIN_URL . 'public/js/public-script.js', array('jquery'), PC_ANNOUNCEMENTS_274_VERSION, true);
}
/**
* Get active announcements
*/
private function get_active_announcements() {
global $wpdb;
$table_name = PC_Announcements_274_Install::get_table_name();
if (empty($table_name)) {
return array();
}
$current_time = current_time('mysql');
$announcements = $wpdb->get_results($wpdb->prepare("
SELECT * FROM $table_name
WHERE status = %s
AND (start_date IS NULL OR start_date <= %s)
AND (end_date IS NULL OR end_date >= %s)
ORDER BY created_at DESC
", 'active', $current_time, $current_time));
if ($announcements === null) {
return array();
}
return $announcements;
}
/**
* Display announcements at the top of pages
*/
public function display_announcements() {
// Don't show on admin pages or in WordPress admin
if (is_admin()) {
return;
}
$announcements = $this->get_active_announcements();
if (empty($announcements)) {
return;
}
// Display only the most recent active announcement
$announcement = $announcements[0];
$this->render_announcement($announcement);
}
/**
* Render single announcement
*/
private function render_announcement($announcement) {
$title = esc_html($announcement->title);
$message = wpautop(wp_kses_post($announcement->message));
$image_url = esc_url($announcement->image_url);
$banner_color = esc_attr($announcement->banner_color);
$link_url = esc_url($announcement->link_url);
$close_text = __('Close', 'pc-announcements-274');
$banner_style = '';
if (!empty($banner_color) && $banner_color !== '#0d47a1') {
$banner_style = 'background: ' . $banner_color . ';';
}
$has_link = !empty($link_url);
$link_attrs = $has_link ? 'href="' . $link_url . '" target="_blank" rel="noopener noreferrer"' : '';
$close_button = $has_link ? '' : '<button class="pc-announcements-274-close" aria-label="' . esc_attr($close_text) . '">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6L6 18M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>';
?>
<div class="pc-announcements-274-announcement" data-announcement-id="<?php echo $announcement->id; ?>" style="<?php echo $banner_style; ?>">
<div class="pc-announcements-274-container">
<div class="pc-announcements-274-content">
<?php if ($has_link): ?>
<a class="pc-announcements-274-link" <?php echo $link_attrs; ?>>
<?php endif; ?>
<?php if (!empty($image_url)): ?>
<div class="pc-announcements-274-image">
<img src="<?php echo $image_url; ?>" alt="<?php echo esc_attr($title); ?>" loading="lazy">
</div>
<?php endif; ?>
<div class="pc-announcements-274-text">
<?php if (!empty($title)): ?>
<h3 class="pc-announcements-274-title"><?php echo $title; ?></h3>
<?php endif; ?>
<?php if (!empty(trim($message))): ?>
<div class="pc-announcements-274-message">
<?php echo $message; ?>
</div>
<?php endif; ?>
</div>
<?php if ($has_link): ?>
</a>
<?php endif; ?>
</div>
<?php echo $close_button; ?>
</div>
</div>
<?php
}
/**
* Get announcements for shortcode
*/
public function get_announcements_for_shortcode($atts) {
$atts = shortcode_atts(array(
'count' => 1,
'show_image' => true,
'show_close' => true,
'class' => ''
), $atts, 'pc_announcements_274');
$announcements = $this->get_active_announcements();
if (empty($announcements)) {
return '';
}
$output = '';
$count = min(intval($atts['count']), count($announcements));
$show_image = filter_var($atts['show_image'], FILTER_VALIDATE_BOOLEAN);
$show_close = filter_var($atts['show_close'], FILTER_VALIDATE_BOOLEAN);
$custom_class = sanitize_html_class($atts['class']);
for ($i = 0; $i < $count; $i++) {
$announcement = $announcements[$i];
$output .= $this->render_announcement_html($announcement, $show_image, $show_close, $custom_class);
}
return $output;
}
/**
* Render announcement as HTML string
*/
private function render_announcement_html($announcement, $show_image = true, $show_close = true, $custom_class = '') {
$title = esc_html($announcement->title);
$message = wpautop(wp_kses_post($announcement->message));
$image_url = esc_url($announcement->image_url);
$banner_color = esc_attr($announcement->banner_color);
$link_url = esc_url($announcement->link_url);
$close_text = __('Close', 'pc-announcements-274');
$class_names = array('pc-announcements-274-announcement');
if (!empty($custom_class)) {
$class_names[] = $custom_class;
}
$banner_style = '';
if (!empty($banner_color) && $banner_color !== '#0d47a1') {
$banner_style = 'style="background: ' . $banner_color . ';"';
}
$has_link = !empty($link_url);
$link_attrs = $has_link ? 'href="' . $link_url . '" target="_blank" rel="noopener noreferrer"' : '';
$html = '<div class="' . implode(' ', $class_names) . '" data-announcement-id="' . $announcement->id . '" ' . $banner_style . '>';
$html .= '<div class="pc-announcements-274-container">';
$html .= '<div class="pc-announcements-274-content">';
if ($has_link) {
$html .= '<a class="pc-announcements-274-link" ' . $link_attrs . '>';
}
if ($show_image && !empty($image_url)) {
$html .= '<div class="pc-announcements-274-image">';
$html .= '<img src="' . $image_url . '" alt="' . esc_attr($title) . '" loading="lazy">';
$html .= '</div>';
}
$html .= '<div class="pc-announcements-274-text">';
if (!empty($title)) {
$html .= '<h3 class="pc-announcements-274-title">' . $title . '</h3>';
}
if (!empty(trim($message))) {
$html .= '<div class="pc-announcements-274-message">' . $message . '</div>';
}
$html .= '</div>';
if ($has_link) {
$html .= '</a>';
}
$html .= '</div>';
if ($show_close && !$has_link) {
$html .= '<button class="pc-announcements-274-close" aria-label="' . esc_attr($close_text) . '">';
$html .= '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">';
$html .= '<path d="M18 6L6 18M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
$html .= '</svg>';
$html .= '</button>';
}
$html .= '</div>';
$html .= '</div>';
return $html;
}
}

View File

@@ -0,0 +1,424 @@
/* PC Announcements 274 - Public Styles */
/* CSS Custom Properties */
:root {
--pc-announcements-274-bg-primary: #0d47a1;
--pc-announcements-274-bg-secondary: #1565c0;
--pc-announcements-274-text-primary: #ffffff;
--pc-announcements-274-text-secondary: rgba(255, 255, 255, 0.9);
--pc-announcements-274-border-color: rgba(255, 255, 255, 0.2);
--pc-announcements-274-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
--pc-announcements-274-shadow-hover: 0 4px 8px rgba(0, 0, 0, 0.15);
--pc-announcements-274-close-color: rgba(255, 255, 255, 0.8);
--pc-announcements-274-close-hover: #ffffff;
--pc-announcements-274-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
--pc-announcements-274-font-size: 16px;
--pc-announcements-274-line-height: 1.5;
--pc-announcements-274-border-radius: 8px;
--pc-announcements-274-z-index: 9999;
--pc-announcements-274-transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Main Announcement Container */
.pc-announcements-274-announcement {
position: relative;
width: 100%;
background: linear-gradient(135deg, var(--pc-announcements-274-bg-primary), var(--pc-announcements-274-bg-secondary));
border-bottom: 1px solid var(--pc-announcements-274-border-color);
box-shadow: var(--pc-announcements-274-shadow);
font-family: var(--pc-announcements-274-font-family);
font-size: var(--pc-announcements-274-font-size);
line-height: var(--pc-announcements-274-line-height);
color: var(--pc-announcements-274-text-primary);
z-index: var(--pc-announcements-274-z-index);
transition: var(--pc-announcements-274-transition);
}
.pc-announcements-274-announcement:hover {
box-shadow: var(--pc-announcements-274-shadow-hover);
}
/* Content Container */
.pc-announcements-274-container {
max-width: 1200px;
margin: 0 auto;
padding: 16px 20px;
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
}
/* Content Layout */
.pc-announcements-274-content {
flex: 1;
display: flex;
align-items: center;
gap: 16px;
min-width: 0;
}
/* Image Styles */
.pc-announcements-274-image {
flex-shrink: 0;
width: 60px;
height: 60px;
border-radius: var(--pc-announcements-274-border-radius);
overflow: hidden;
border: 2px solid var(--pc-announcements-274-border-color);
box-shadow: var(--pc-announcements-274-shadow);
}
.pc-announcements-274-image img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
/* Text Content */
.pc-announcements-274-text {
flex: 1;
min-width: 0;
}
.pc-announcements-274-title {
margin: 0 0 4px 0;
font-size: 18px;
font-weight: 600;
line-height: 1.3;
color: var(--pc-announcements-274-text-primary);
letter-spacing: -0.02em;
}
.pc-announcements-274-message {
margin: 0;
font-size: 14px;
line-height: 1.4;
color: var(--pc-announcements-274-text-secondary);
font-weight: 400;
}
.pc-announcements-274-message p {
margin: 0;
}
.pc-announcements-274-message p:last-child {
margin-bottom: 0;
}
/* Link Styles */
.pc-announcements-274-link {
display: flex;
align-items: center;
gap: 16px;
flex: 1;
min-width: 0;
text-decoration: none;
color: inherit;
transition: var(--pc-announcements-274-transition);
}
.pc-announcements-274-link:hover {
opacity: 0.9;
}
.pc-announcements-274-link:focus {
outline: 2px solid var(--pc-announcements-274-close-hover);
outline-offset: 2px;
}
/* Close Button */
.pc-announcements-274-close {
flex-shrink: 0;
width: 40px;
height: 40px;
border: none;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
color: var(--pc-announcements-274-close-color);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: var(--pc-announcements-274-transition);
padding: 0;
margin-left: 16px;
}
.pc-announcements-274-close:hover {
background: rgba(255, 255, 255, 0.2);
color: var(--pc-announcements-274-close-hover);
transform: scale(1.1);
}
.pc-announcements-274-close:focus {
outline: 2px solid var(--pc-announcements-274-close-hover);
outline-offset: 2px;
}
.pc-announcements-274-close svg {
width: 16px;
height: 16px;
stroke-width: 2;
}
/* Animations */
.pc-announcements-274-announcement.pc-announcements-274-hidden {
animation: pc-announcements-274-slide-up 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
.pc-announcements-274-announcement.pc-announcements-274-show {
animation: pc-announcements-274-slide-down 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
@keyframes pc-announcements-274-slide-up {
0% {
transform: translateY(0);
opacity: 1;
max-height: 200px;
}
100% {
transform: translateY(-100%);
opacity: 0;
max-height: 0;
}
}
@keyframes pc-announcements-274-slide-down {
0% {
transform: translateY(-100%);
opacity: 0;
max-height: 0;
}
100% {
transform: translateY(0);
opacity: 1;
max-height: 200px;
}
}
/* Responsive Design */
@media (max-width: 768px) {
.pc-announcements-274-container {
padding: 12px 16px;
}
.pc-announcements-274-content {
flex-direction: column;
align-items: flex-start;
gap: 12px;
text-align: center;
}
.pc-announcements-274-image {
width: 50px;
height: 50px;
}
.pc-announcements-274-title {
font-size: 16px;
}
.pc-announcements-274-message {
font-size: 13px;
}
.pc-announcements-274-close {
position: absolute;
top: 8px;
right: 8px;
margin-left: 0;
}
}
@media (max-width: 480px) {
.pc-announcements-274-container {
padding: 10px 12px;
}
.pc-announcements-274-image {
width: 40px;
height: 40px;
}
.pc-announcements-274-title {
font-size: 15px;
}
.pc-announcements-274-message {
font-size: 12px;
}
.pc-announcements-274-close {
width: 32px;
height: 32px;
}
.pc-announcements-274-close svg {
width: 14px;
height: 14px;
}
}
/* High Contrast Mode Support */
@media (prefers-contrast: high) {
.pc-announcements-274-announcement {
border-bottom: 2px solid #000;
}
.pc-announcements-274-image {
border: 2px solid #000;
}
.pc-announcements-274-close {
border: 2px solid var(--pc-announcements-274-text-primary);
}
}
/* Reduced Motion Support */
@media (prefers-reduced-motion: reduce) {
.pc-announcements-274-announcement,
.pc-announcements-274-close,
.pc-announcements-274-announcement:hover {
transition: none;
}
.pc-announcements-274-close:hover {
transform: none;
}
.pc-announcements-274-announcement.pc-announcements-274-hidden,
.pc-announcements-274-announcement.pc-announcements-274-show {
animation: none;
}
.pc-announcements-274-announcement.pc-announcements-274-hidden {
display: none;
}
}
/* Dark Mode Support */
@media (prefers-color-scheme: dark) {
:root {
--pc-announcements-274-bg-primary: #1a237e;
--pc-announcements-274-bg-secondary: #283593;
--pc-announcements-274-text-primary: #ffffff;
--pc-announcements-274-text-secondary: rgba(255, 255, 255, 0.85);
--pc-announcements-274-border-color: rgba(255, 255, 255, 0.1);
--pc-announcements-274-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
--pc-announcements-274-shadow-hover: 0 4px 8px rgba(0, 0, 0, 0.4);
}
}
/* Print Styles */
@media print {
.pc-announcements-274-announcement {
display: none !important;
}
}
/* RTL Support */
[dir="rtl"] .pc-announcements-274-content {
text-align: right;
}
[dir="rtl"] .pc-announcements-274-close {
margin-left: 0;
margin-right: 16px;
}
[dir="rtl"] .pc-announcements-274-close:only-child {
margin-right: 0;
}
/* Custom Theme Variations */
.pc-announcements-274-announcement.pc-announcements-274-success {
--pc-announcements-274-bg-primary: #2e7d32;
--pc-announcements-274-bg-secondary: #388e3c;
}
.pc-announcements-274-announcement.pc-announcements-274-warning {
--pc-announcements-274-bg-primary: #f57c00;
--pc-announcements-274-bg-secondary: #fb8c00;
}
.pc-announcements-274-announcement.pc-announcements-274-error {
--pc-announcements-274-bg-primary: #c62828;
--pc-announcements-274-bg-secondary: #d32f2f;
}
.pc-announcements-274-announcement.pc-announcements-274-info {
--pc-announcements-274-bg-primary: #0277bd;
--pc-announcements-274-bg-secondary: #0288d1;
}
/* Compact Version */
.pc-announcements-274-announcement.pc-announcements-274-compact {
--pc-announcements-274-font-size: 14px;
}
.pc-announcements-274-announcement.pc-announcements-274-compact .pc-announcements-274-container {
padding: 12px 16px;
}
.pc-announcements-274-announcement.pc-announcements-274-compact .pc-announcements-274-image {
width: 40px;
height: 40px;
}
.pc-announcements-274-announcement.pc-announcements-274-compact .pc-announcements-274-title {
font-size: 16px;
}
.pc-announcements-274-announcement.pc-announcements-274-compact .pc-announcements-274-message {
font-size: 13px;
}
/* No Image Variant */
.pc-announcements-274-announcement.pc-announcements-274-no-image .pc-announcements-274-content {
gap: 0;
}
.pc-announcements-274-announcement.pc-announcements-274-no-image .pc-announcements-274-text {
margin-right: 16px;
}
/* Hidden State - Animation */
.pc-announcements-274-announcement[aria-hidden="true"] {
display: none;
}
/* Focus Management */
.pc-announcements-274-announcement:focus-within {
outline: 2px solid var(--pc-announcements-274-close-hover);
outline-offset: -2px;
}
/* Accessibility Improvements */
.pc-announcements-274-announcement[role="banner"] {
max-height: none;
}
.pc-announcements-274-announcement[role="banner"].pc-announcements-274-hidden {
max-height: 0;
overflow: hidden;
}
/* Browser-specific fixes */
@supports (-webkit-appearance: none) {
.pc-announcements-274-close {
-webkit-appearance: none;
border-radius: 50%;
}
}
@supports not (display: grid) {
.pc-announcements-274-content {
display: flex;
}
.pc-announcements-274-text {
flex: 1;
}
}

View File

@@ -0,0 +1,219 @@
jQuery(document).ready(function($) {
'use strict';
// Close announcement functionality
$('.pc-announcements-274-close').on('click', function(e) {
e.preventDefault();
e.stopPropagation();
var $announcement = $(this).closest('.pc-announcements-274-announcement');
var announcementId = $announcement.data('announcement-id');
// Add hiding class for animation
$announcement.addClass('pc-announcements-274-hidden');
// Store dismissal in localStorage for this session
if (typeof(Storage) !== "undefined" && announcementId) {
var dismissed = localStorage.getItem('pc_announcements_274_dismissed') || '[]';
var dismissedArray = JSON.parse(dismissed);
if (dismissedArray.indexOf(announcementId) === -1) {
dismissedArray.push(announcementId);
localStorage.setItem('pc_announcements_274_dismissed', JSON.stringify(dismissedArray));
}
}
// Remove from DOM after animation
setTimeout(function() {
$announcement.attr('aria-hidden', 'true').hide();
// Adjust body padding if needed
adjustBodyPadding();
}, 400);
});
// Adjust body padding to prevent content jump when announcement is hidden
function adjustBodyPadding() {
var $announcement = $('.pc-announcements-274-announcement');
if ($announcement.length === 0) {
$('body').css('padding-top', '');
return;
}
if ($announcement.is(':visible')) {
var announcementHeight = $announcement.outerHeight();
var currentPadding = parseInt($('body').css('padding-top')) || 0;
if (currentPadding < announcementHeight) {
$('body').css('padding-top', announcementHeight + 'px');
}
}
}
// Initialize padding adjustment
$(window).on('load', function() {
adjustBodyPadding();
});
// Handle window resize
$(window).on('resize', function() {
adjustBodyPadding();
});
// Check for dismissed announcements on page load
function checkDismissedAnnouncements() {
if (typeof(Storage) !== "undefined") {
var dismissed = localStorage.getItem('pc_announcements_274_dismissed') || '[]';
var dismissedArray = JSON.parse(dismissed);
$('.pc-announcements-274-announcement').each(function() {
var announcementId = $(this).data('announcement-id');
if (announcementId && dismissedArray.indexOf(announcementId) !== -1) {
$(this).attr('aria-hidden', 'true').hide();
}
});
}
}
checkDismissedAnnouncements();
// Re-check padding after checking dismissed announcements
setTimeout(function() {
adjustBodyPadding();
}, 100);
// Keyboard navigation
$('.pc-announcements-274-close').on('keydown', function(e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
$(this).click();
}
});
// Escape key to close announcement
$(document).on('keydown', function(e) {
if (e.key === 'Escape') {
var $visibleAnnouncement = $('.pc-announcements-274-announcement:visible');
if ($visibleAnnouncement.length > 0) {
$visibleAnnouncement.find('.pc-announcements-274-close').first().focus();
}
}
});
// Auto-hide functionality (optional - could be enabled with a data attribute)
function initAutoHide() {
$('.pc-announcements-274-announcement[data-auto-hide]').each(function() {
var $announcement = $(this);
var autoHideTime = parseInt($announcement.data('auto-hide')) * 1000;
if (autoHideTime > 0) {
setTimeout(function() {
if ($announcement.is(':visible')) {
$announcement.find('.pc-announcements-274-close').click();
}
}, autoHideTime);
}
});
}
initAutoHide();
// Add animation classes on initial load
$('.pc-announcements-274-announcement:visible').addClass('pc-announcements-274-show');
// Handle dynamic content loading (if announcements are loaded via AJAX)
function reinitializeAnnouncements() {
adjustBodyPadding();
checkDismissedAnnouncements();
initAutoHide();
$('.pc-announcements-274-announcement:visible').addClass('pc-announcements-274-show');
}
// Expose reinitialize function for global use
window.pcAnnouncements274Reinitialize = reinitializeAnnouncements;
// Smooth scroll to top when announcement appears (optional)
function smoothScrollToTop() {
if ($('.pc-announcements-274-announcement:visible').length > 0) {
$('html, body').animate({
scrollTop: 0
}, 300);
}
}
// Only scroll to top on initial page load if announcement is present
if (performance.navigation.type === 0) { // First page load
setTimeout(function() {
if ($('.pc-announcements-274-announcement:visible').length > 0) {
var $announcement = $('.pc-announcements-274-announcement:visible');
var announcementId = $announcement.data('announcement-id');
// Don't scroll if it was just dismissed
if (typeof(Storage) !== "undefined" && announcementId) {
var dismissed = localStorage.getItem('pc_announcements_274_dismissed') || '[]';
var dismissedArray = JSON.parse(dismissed);
if (dismissedArray.indexOf(announcementId) === -1) {
smoothScrollToTop();
}
} else {
smoothScrollToTop();
}
}
}, 100);
}
// Handle announcement stacking if multiple are shown
function handleStacking() {
var $announcements = $('.pc-announcements-274-announcement:visible');
var offset = 0;
$announcements.each(function(index) {
$(this).css('top', offset + 'px');
offset += $(this).outerHeight();
});
}
handleStacking();
// Re-handle stacking on window resize
$(window).on('resize', function() {
handleStacking();
});
// Accessibility: Focus management
function manageFocus() {
$('.pc-announcements-274-announcement').attr('role', 'banner');
$('.pc-announcements-274-close').attr('tabindex', '0');
}
manageFocus();
// Performance: Debounce resize events
function debounce(func, wait) {
var timeout;
return function executedFunction() {
var context = this;
var args = arguments;
var later = function() {
timeout = null;
func.apply(context, args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
var debouncedResize = debounce(function() {
adjustBodyPadding();
handleStacking();
}, 250);
$(window).on('resize', debouncedResize);
// Log for debugging (remove in production)
if (window.console && window.console.log && false) { // Set to true for debugging
console.log('PC Announcements 274: Initialized');
}
});

View File

@@ -0,0 +1,162 @@
#!/bin/bash
# WordPress Plugin Validation Script
# This script validates the PHP syntax and structure of the PC Announcements plugin
echo "🔍 Validating PC Announcements Plugin..."
echo "========================================"
PLUGIN_DIR="/home/web/data/apps/c7f9e5c6-e7c2-4258-a583-ccffcf9791c8/announcements-v274"
# Check if plugin directory exists
if [ ! -d "$PLUGIN_DIR" ]; then
echo "❌ Plugin directory not found: $PLUGIN_DIR"
exit 1
fi
# Navigate to plugin directory
cd "$PLUGIN_DIR" || exit 1
echo "✅ Plugin directory found: $PLUGIN_DIR"
# Check main plugin file
echo ""
echo "📝 Checking main plugin file..."
MAIN_FILE="pc-announcements-274.php"
if [ ! -f "$MAIN_FILE" ]; then
echo "❌ Main plugin file not found: $MAIN_FILE"
exit 1
fi
echo "✅ Main plugin file found: $MAIN_FILE"
# Check PHP syntax for main file
echo ""
echo "🔍 Checking PHP syntax for main file..."
if php -l "$MAIN_FILE"; then
echo "✅ Main plugin file has valid PHP syntax"
else
echo "❌ Main plugin file has PHP syntax errors"
exit 1
fi
# Check all PHP files for syntax errors
echo ""
echo "🔍 Checking all PHP files for syntax errors..."
PHP_FILES=$(find . -name "*.php" -type f)
if [ -z "$PHP_FILES" ]; then
echo "❌ No PHP files found"
exit 1
fi
echo "Found PHP files:"
echo "$PHP_FILES"
echo ""
SYNTAX_ERRORS=0
for file in $PHP_FILES; do
echo "Checking: $file"
if php -l "$file"; then
echo "✅ Valid syntax"
else
echo "❌ Syntax errors found"
SYNTAX_ERRORS=$((SYNTAX_ERRORS + 1))
fi
echo ""
done
if [ $SYNTAX_ERRORS -eq 0 ]; then
echo "✅ All PHP files have valid syntax"
else
echo "❌ Found $SYNTAX_ERRORS PHP files with syntax errors"
exit 1
fi
# Check plugin header
echo ""
echo "🔍 Checking plugin header..."
HEADER_CHECK=$(grep -q "Plugin Name:" "$MAIN_FILE" && grep -q "Plugin URI:" "$MAIN_FILE" && grep -q "Version:" "$MAIN_FILE" && grep -q "Requires PHP:" "$MAIN_FILE" && echo "✅ Plugin header found" || echo "❌ Plugin header incomplete")
if [[ "$HEADER_CHECK" == *"❌"* ]]; then
echo "❌ Plugin header is incomplete"
exit 1
else
echo "$HEADER_CHECK"
fi
# Check required files and directories
echo ""
echo "🔍 Checking required files and directories..."
REQUIRED_FILES=(
"includes/class-install.php"
"admin/class-admin.php"
"public/class-frontend.php"
"uninstall.php"
"admin/templates/list-page.php"
"admin/templates/edit-page.php"
"admin/templates/error-page.php"
)
MISSING_FILES=0
for file in "${REQUIRED_FILES[@]}"; do
if [ -f "$file" ]; then
echo "✅ Found: $file"
else
echo "❌ Missing: $file"
MISSING_FILES=$((MISSING_FILES + 1))
fi
done
if [ $MISSING_FILES -eq 0 ]; then
echo "✅ All required files found"
else
echo "❌ Missing $MISSING_FILES required files"
exit 1
fi
# Check for WordPress coding standards compliance
echo ""
echo "🔍 Checking WordPress coding standards..."
STANDARD_CHECK=$(grep -q "defined('ABSPATH')" "$MAIN_FILE" && echo "✅ ABSPATH check found" || echo "❌ ABSPATH check missing")
if [[ "$STANDARD_CHECK" == *"❌"* ]]; then
echo "❌ WordPress coding standards not met"
exit 1
else
echo "$STANDARD_CHECK"
fi
# Check for security measures
echo ""
echo "🔍 Checking security measures..."
NONCE_CHECK=$(grep -r "wp_verify_nonce\|check_ajax_referer" . --include="*.php" | grep -v "grep" | head -1)
if [ -n "$NONCE_CHECK" ]; then
echo "✅ Nonce validation found in plugin files"
else
echo "❌ Nonce validation missing"
exit 1
fi
# Check for proper file permissions
echo ""
echo "🔍 Checking file permissions..."
PERMISSION_CHECK=$(ls -la "$MAIN_FILE" | grep -q "\-rw\-" && echo "✅ Main file has proper permissions" || echo "❌ Main file has incorrect permissions")
if [[ "$PERMISSION_CHECK" == *"❌"* ]]; then
echo "⚠️ Warning: File permissions may need adjustment"
else
echo "$PERMISSION_CHECK"
fi
echo ""
echo "🎉 Plugin validation completed successfully!"
echo "============================================="
echo "✅ All PHP files have valid syntax"
echo "✅ Plugin header is complete"
echo "✅ All required files are present"
echo "✅ WordPress coding standards are met"
echo "✅ Security measures are in place"
echo ""
echo "The PC Announcements 274 plugin is ready for installation!"

View File

@@ -0,0 +1,31 @@
<?php
/**
* Uninstall script for PC Announcements 274
* This script runs when the plugin is uninstalled from WordPress
*/
// Prevent direct access
if (!defined('WP_UNINSTALL_PLUGIN')) {
exit;
}
// Plugin options to delete
$options_to_delete = array(
'pc_announcements_274_version',
'pc_announcements_274_db_version'
);
// Delete options
foreach ($options_to_delete as $option) {
delete_option($option);
}
// Delete database table
global $wpdb;
if (isset($wpdb)) {
$table_name = $wpdb->prefix . 'pc_announcements_274';
$wpdb->query("DROP TABLE IF EXISTS $table_name");
}
// Clear any cached data
wp_cache_flush();

View File

@@ -0,0 +1,638 @@
/**
* PC Changelog Manager - Admin Styles
*
* @package PCChangelogManager
*/
/* CSS Variables for WordPress Admin Color Scheme Compatibility */
:root {
--pc-clm-admin-primary: #2271b1;
--pc-clm-admin-primary-hover: #135e96;
--pc-clm-admin-secondary: #f6f7f7;
--pc-clm-admin-text: #3c434a;
--pc-clm-admin-text-muted: #646970;
--pc-clm-admin-border: #dcdcde;
--pc-clm-admin-bg: #ffffff;
--pc-clm-admin-bg-alt: #f0f0f1;
--pc-clm-success: #00a32a;
--pc-clm-warning: #dba617;
--pc-clm-error: #dc3232;
--pc-clm-info: #72aee6;
}
/* Dark Mode Support */
@media (prefers-color-scheme: dark) {
:root {
--pc-clm-admin-primary: #2271b1;
--pc-clm-admin-secondary: #1d2327;
--pc-clm-admin-text: #f0f0f1;
--pc-clm-admin-text-muted: #a7aaad;
--pc-clm-admin-border: #3c434a;
--pc-clm-admin-bg: #1d2327;
--pc-clm-admin-bg-alt: #2c3338;
}
}
/* ========================================
Meta Box Styles
======================================== */
/* Entry Details Meta Box */
#pc_clm_entry_details {
background: var(--pc-clm-admin-bg);
}
#pc_clm_entry_details .inside {
padding: 16px 20px;
}
.pc-clm-meta-fields {
display: flex;
flex-direction: column;
gap: 20px;
}
.pc-clm-field-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.pc-clm-field-group label {
font-weight: 600;
font-size: 13px;
color: var(--pc-clm-admin-text);
margin: 0;
}
.pc-clm-field-group input[type="text"],
.pc-clm-field-group input[type="date"],
.pc-clm-field-group select {
max-width: 400px;
width: 100%;
height: 36px;
padding: 0 12px;
font-size: 14px;
border: 1px solid var(--pc-clm-admin-border);
border-radius: 4px;
background-color: var(--pc-clm-admin-bg);
color: var(--pc-clm-admin-text);
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.pc-clm-field-group input[type="text"]:focus,
.pc-clm-field-group input[type="date"]:focus,
.pc-clm-field-group select:focus {
border-color: var(--pc-clm-admin-primary);
box-shadow: 0 0 0 1px var(--pc-clm-admin-primary);
outline: none;
}
.pc-clm-field-group .description {
font-size: 12px;
color: var(--pc-clm-admin-text-muted);
margin: 0;
}
/* Preview Meta Box */
#pc_clm_preview .inside {
padding: 16px 20px;
}
.pc-clm-preview-box {
display: flex;
flex-direction: column;
gap: 12px;
}
.pc-clm-preview-box p {
margin: 0;
font-size: 13px;
color: var(--pc-clm-admin-text-muted);
}
.pc-clm-preview-box .button {
display: inline-flex;
align-items: center;
gap: 6px;
}
/* ========================================
Category Badges
======================================== */
.pc-clm-category-badge {
display: inline-block;
padding: 3px 10px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
border-radius: 12px;
background-color: var(--pc-clm-admin-bg-alt);
color: var(--pc-clm-admin-text);
}
.pc-clm-category-new-features {
background-color: #e3fcef;
color: #006d26;
}
.pc-clm-category-bug-fixes {
background-color: #fbe4e4;
color: #c01e1e;
}
.pc-clm-category-improvements {
background-color: #e3f2fd;
color: #0d4fdd;
}
.pc-clm-category-security {
background-color: #fff3cd;
color: #856404;
}
.pc-clm-category-deprecated {
background-color: #f5f5f5;
color: #616161;
}
.pc-clm-category-removed {
background-color: #e8e8e8;
color: #424242;
}
/* Dark mode category badges */
@media (prefers-color-scheme: dark) {
.pc-clm-category-new-features {
background-color: #004d26;
color: #a7ebc3;
}
.pc-clm-category-bug-fixes {
background-color: #4d1a1a;
color: #f5b5b5;
}
.pc-clm-category-improvements {
background-color: #1a3659;
color: #a5c8ff;
}
.pc-clm-category-security {
background-color: #4d4200;
color: #ffecb3;
}
.pc-clm-category-deprecated {
background-color: #424242;
color: #bdbdbd;
}
.pc-clm-category-removed {
background-color: #616161;
color: #e0e0e0;
}
}
/* ========================================
Admin List Table Styles
======================================== */
.fixed .column-version {
width: 100px;
}
.fixed .column-release_date {
width: 120px;
}
.fixed .column-category {
width: 120px;
}
/* Version column styling */
.column-version {
font-family: 'Menlo', 'Monaco', 'Consolas', monospace;
font-weight: 600;
font-size: 13px;
}
/* ========================================
Post Editor Styles
======================================== */
/* Editor title styling for changelog entries */
#title-prompt-text {
color: var(--pc-clm-admin-text-muted);
}
#post-body-content {
margin-bottom: 0;
}
/* ========================================
Notices and Messages
======================================== */
.pc-clm-notice {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 16px;
margin: 16px 0;
border-radius: 4px;
background-color: var(--pc-clm-admin-bg-alt);
border-left: 4px solid var(--pc-clm-admin-info);
}
.pc-clm-notice.success {
border-left-color: var(--pc-clm-success);
}
.pc-clm-notice.warning {
border-left-color: var(--pc-clm-warning);
}
.pc-clm-notice.error {
border-left-color: var(--pc-clm-error);
}
.pc-clm-notice-icon {
flex-shrink: 0;
width: 20px;
height: 20px;
font-size: 20px;
line-height: 1;
}
.pc-clm-notice-content {
flex: 1;
}
.pc-clm-notice-title {
font-weight: 600;
margin: 0 0 4px 0;
font-size: 14px;
color: var(--pc-clm-admin-text);
}
.pc-clm-notice-message {
margin: 0;
font-size: 13px;
color: var(--pc-clm-admin-text-muted);
}
/* ========================================
Responsive Design
======================================== */
@media screen and (max-width: 782px) {
/* Stack meta fields on mobile */
.pc-clm-meta-fields {
gap: 16px;
}
.pc-clm-field-group input[type="text"],
.pc-clm-field-group input[type="date"],
.pc-clm-field-group select {
max-width: 100%;
}
/* Adjust column widths on mobile */
.fixed .column-version,
.fixed .column-release_date,
.fixed .column-category {
width: auto;
}
/* Hide some columns on mobile */
.column-release_date,
.column-category {
display: none;
}
/* Category badges wrap properly */
.pc-clm-category-badge {
display: inline-flex;
margin: 2px;
}
}
@media screen and (max-width: 480px) {
#pc_clm_entry_details .inside {
padding: 12px 16px;
}
.pc-clm-preview-box {
gap: 10px;
}
.pc-clm-preview-box .button {
width: 100%;
justify-content: center;
}
}
/* ========================================
Print Styles
======================================== */
@media print {
.pc-clm-meta-fields {
display: block;
}
.pc-clm-field-group {
margin-bottom: 16px;
}
.pc-clm-field-group input,
.pc-clm-field-group select {
border: 1px solid #000;
}
.pc-clm-preview-box {
display: none;
}
}
/* ========================================
Accessibility Enhancements
======================================== */
.pc-clm-field-group input:focus,
.pc-clm-field-group select:focus {
outline: 2px solid var(--pc-clm-admin-primary);
outline-offset: 2px;
}
/* High contrast mode support */
@media (prefers-contrast: high) {
.pc-clm-field-group input,
.pc-clm-field-group select {
border-width: 2px;
}
.pc-clm-category-badge {
border: 1px solid currentColor;
}
}
/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
.pc-clm-field-group input,
.pc-clm-field-group select,
.pc-clm-category-badge {
transition: none;
}
}
/* ========================================
Tooltip Styles for Help Text
======================================== */
.pc-clm-tooltip {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
margin-left: 6px;
font-size: 11px;
font-weight: 600;
color: var(--pc-clm-admin-text-muted);
background-color: var(--pc-clm-admin-bg-alt);
border-radius: 50%;
cursor: help;
}
.pc-clm-tooltip:hover::after {
content: attr(data-tooltip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
padding: 8px 12px;
margin-bottom: 8px;
font-size: 12px;
font-weight: 400;
white-space: nowrap;
color: #fff;
background-color: #1d2327;
border-radius: 4px;
z-index: 1000;
}
.pc-clm-tooltip::after {
pointer-events: none;
}
/* ========================================
Animation for Loading States
======================================== */
@keyframes pc-clm-fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.pc-clm-loading {
animation: pc-clm-fade-in 0.3s ease-out;
}
/* ========================================
Empty State Styling
======================================== */
.pc-clm-empty-state {
text-align: center;
padding: 40px 20px;
color: var(--pc-clm-admin-text-muted);
}
.pc-clm-empty-state-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
.pc-clm-empty-state-title {
font-size: 18px;
font-weight: 600;
margin: 0 0 8px 0;
color: var(--pc-clm-admin-text);
}
.pc-clm-empty-state-description {
margin: 0;
font-size: 14px;
}
/* ========================================
Admin View Changelog Page
======================================== */
.pc-clm-admin-view-list {
display: flex;
flex-direction: column;
gap: 16px;
margin-top: 0;
}
.pc-clm-admin-entry {
padding: 20px;
background: #ffffff;
border: 1px solid #dcdcde;
border-radius: 4px;
transition: box-shadow 0.15s ease-in-out, border-color 0.15s ease-in-out;
}
.pc-clm-admin-entry:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
border-color: #2271b1;
}
.pc-clm-admin-entry-header {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.pc-clm-admin-version {
display: inline-block;
padding: 4px 12px;
font-family: 'Menlo', 'Monaco', 'Consolas', monospace;
font-size: 13px;
font-weight: 600;
color: #2271b1;
background-color: #f0f0f1;
border-radius: 4px;
}
.pc-clm-admin-date {
font-size: 13px;
color: #646970;
}
.pc-clm-admin-entry-title {
margin: 0 0 12px 0;
font-size: 18px;
font-weight: 600;
line-height: 1.3;
}
.pc-clm-admin-entry-title a {
color: #1d2327;
text-decoration: none;
transition: color 0.15s ease-in-out;
}
.pc-clm-admin-entry-title a:hover {
color: #2271b1;
}
.pc-clm-admin-entry-content {
margin-bottom: 16px;
font-size: 14px;
line-height: 1.6;
color: #3c434a;
}
.pc-clm-admin-entry-actions {
display: flex;
gap: 8px;
padding-top: 12px;
border-top: 1px solid #dcdcde;
}
/* ========================================
Admin Add New Page
======================================== */
.pc-clm-admin-add-form {
margin-top: 20px;
max-width: 900px;
}
.pc-clm-admin-add-form .form-table th {
width: 200px;
font-weight: 600;
}
.pc-clm-admin-add-form .required {
color: var(--pc-clm-error);
}
/* ========================================
Admin Empty State
======================================== */
.pc-clm-admin-empty {
text-align: center;
padding: 80px 20px;
background: var(--pc-clm-admin-bg-alt);
border: 2px dashed var(--pc-clm-admin-border);
border-radius: 8px;
margin-top: 20px;
}
.pc-clm-admin-empty p {
margin: 0 0 20px 0;
font-size: 16px;
color: var(--pc-clm-admin-text-muted);
}
/* ========================================
Dark Mode for Admin Pages
======================================== */
@media (prefers-color-scheme: dark) {
.pc-clm-admin-entry {
background: #1d2327;
border-color: #3c434a;
}
.pc-clm-admin-entry:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
border-color: #2271b1;
}
.pc-clm-admin-version {
background-color: #2c3338;
color: #72aee6;
}
.pc-clm-admin-entry-title a {
color: #f0f0f1;
}
.pc-clm-admin-entry-title a:hover {
color: #72aee6;
}
.pc-clm-admin-entry-content {
color: #a7aaad;
}
.pc-clm-admin-entry-actions {
border-top-color: #3c434a;
}
.pc-clm-admin-empty {
background-color: #1d2327;
border-color: #3c434a;
}
.pc-clm-admin-empty p {
color: #a7aaad;
}
}

View File

@@ -0,0 +1,590 @@
<?php
/**
* Admin Handler
*
* @package PCChangelogManager
*/
// Prevent direct access to the file.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Handles all admin functionality for the changelog manager.
*/
class PC_CLM_Admin {
/**
* Instance of the post type class.
*
* @var PC_CLM_Post_Type
*/
private $post_type;
/**
* Constructor.
*
* @param PC_CLM_Post_Type $post_type Post type instance.
*/
public function __construct( $post_type ) {
$this->post_type = $post_type;
$this->init_hooks();
}
/**
* Initialize admin hooks.
*/
private function init_hooks() {
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_styles' ) );
add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ) );
add_action( 'save_post_' . $this->post_type->get_post_type_slug(), array( $this, 'save_meta' ), 10, 2 );
add_filter( 'manage_' . $this->post_type->get_post_type_slug() . '_posts_columns', array( $this, 'set_custom_columns' ) );
add_action( 'manage_' . $this->post_type->get_post_type_slug() . '_posts_custom_column', array( $this, 'render_custom_columns' ), 10, 2 );
add_filter( 'post_row_actions', array( $this, 'modify_row_actions' ), 10, 2 );
add_action( 'admin_menu', array( $this, 'add_admin_menu' ) );
add_action( 'parent_file', array( $this, 'highlight_menu' ) );
add_action( 'admin_init', array( $this, 'handle_add_new_form' ) );
}
/**
* Enqueue admin styles.
*/
public function enqueue_styles() {
$screen = get_current_screen();
if ( ! $screen ) {
return;
}
// Only enqueue on changelog post type screens.
if ( $this->post_type->get_post_type_slug() !== $screen->post_type ) {
return;
}
// Enqueue admin stylesheet.
if ( file_exists( PC_CLM_PLUGIN_DIR . 'admin/css/admin-style.css' ) ) {
wp_enqueue_style(
'pc-clm-admin-style',
PC_CLM_PLUGIN_URL . 'admin/css/admin-style.css',
array(),
PC_CLM_VERSION
);
}
}
/**
* Add admin menu items.
*/
public function add_admin_menu() {
add_menu_page(
__( 'Changelog', 'pc-changelog-manager-abc123' ),
__( 'Changelog', 'pc-changelog-manager-abc123' ),
'manage_options',
'pc-clm-view-changelog',
array( $this, 'render_view_changelog_page' ),
'dashicons-backup',
30
);
add_submenu_page(
'pc-clm-view-changelog',
__( 'Add New Changelog Entry', 'pc-changelog-manager-abc123' ),
__( 'Add New', 'pc-changelog-manager-abc123' ),
'manage_options',
'pc-clm-add-new',
array( $this, 'render_add_new_page' )
);
}
/**
* Render the main menu page (no-op).
* The top-level menu now links directly to the post list to avoid redirects
* that may run after headers/styles have already been sent.
*/
public function render_main_menu_page() {
// Intentionally left blank to avoid header() calls after output.
}
/**
* Render the view changelog page - displays changelog entries in admin.
*/
public function render_view_changelog_page() {
$post_type = new PC_CLM_Post_Type();
$query = $post_type->get_entries();
?>
<div class="wrap">
<h1 class="wp-heading-inline"><?php esc_html_e( 'View Changelog', 'pc-changelog-manager-abc123' ); ?></h1>
<a href="<?php echo esc_url( admin_url( 'post-new.php?post_type=' . $post_type->get_post_type_slug() ) ); ?>" class="page-title-action">
<?php esc_html_e( 'Add New Entry', 'pc-changelog-manager-abc123' ); ?>
</a>
<hr class="wp-header-end">
<?php if ( $query->have_posts() ) : ?>
<div class="pc-clm-admin-view-list">
<?php
while ( $query->have_posts() ) :
$query->the_post();
$post_id = get_the_ID();
$version = $post_type->get_meta( $post_id, 'version_number' );
$release_date = $post_type->get_meta( $post_id, 'release_date' );
$category = $post_type->get_meta( $post_id, 'category' );
$categories = $post_type->get_categories();
$category_name = isset( $categories[ $category ] ) ? $categories[ $category ] : $category;
?>
<div class="pc-clm-admin-entry">
<div class="pc-clm-admin-entry-header">
<?php if ( $version ) : ?>
<span class="pc-clm-admin-version"><?php echo esc_html( $version ); ?></span>
<?php endif; ?>
<?php if ( $release_date ) : ?>
<span class="pc-clm-admin-date"><?php echo esc_html( date_i18n( get_option( 'date_format' ), strtotime( $release_date ) ) ); ?></span>
<?php endif; ?>
<?php if ( $category ) : ?>
<span class="pc-clm-admin-category pc-clm-category-<?php echo esc_attr( $category ); ?>"><?php echo esc_html( $category_name ); ?></span>
<?php endif; ?>
</div>
<h3 class="pc-clm-admin-entry-title">
<a href="<?php echo esc_url( get_edit_post_link( $post_id ) ); ?>">
<?php the_title(); ?>
</a>
</h3>
<div class="pc-clm-admin-entry-content">
<?php echo wp_kses_post( wp_trim_words( get_the_content(), 30, '...' ) ); ?>
</div>
<div class="pc-clm-admin-entry-actions">
<a href="<?php echo esc_url( get_edit_post_link( $post_id ) ); ?>" class="button button-small">
<?php esc_html_e( 'Edit', 'pc-changelog-manager-abc123' ); ?>
</a>
<a href="<?php echo esc_url( get_permalink( $post_id ) ); ?>" target="_blank" class="button button-small">
<?php esc_html_e( 'View', 'pc-changelog-manager-abc123' ); ?>
</a>
</div>
</div>
<?php
endwhile;
wp_reset_postdata();
?>
</div>
<?php else : ?>
<div class="pc-clm-admin-empty">
<p>
<span class="dashicons dashicons-backup" style="font-size: 48px; width: 48px; height: 48px; display: block; margin: 0 auto 16px; opacity: 0.5;"></span>
<?php esc_html_e( 'No changelog entries found.', 'pc-changelog-manager-abc123' ); ?>
</p>
<a href="<?php echo esc_url( admin_url( 'post-new.php?post_type=' . $post_type->get_post_type_slug() ) ); ?>" class="button button-primary">
<?php esc_html_e( 'Add Your First Changelog Entry', 'pc-changelog-manager-abc123' ); ?>
</a>
</div>
<?php endif; ?>
</div>
<?php
}
/**
* Handle the add new changelog form submission.
*/
public function handle_add_new_form() {
if ( ! isset( $_POST['pc_clm_add_new_nonce'] ) || ! isset( $_POST['pc_clm_submit_new'] ) ) {
return;
}
if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['pc_clm_add_new_nonce'] ) ), 'pc_clm_add_new' ) ) {
return;
}
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$title = isset( $_POST['pc_clm_title'] ) ? sanitize_text_field( wp_unslash( $_POST['pc_clm_title'] ) ) : '';
$content = isset( $_POST['pc_clm_content'] ) ? wp_kses_post( wp_unslash( $_POST['pc_clm_content'] ) ) : '';
$version = isset( $_POST['pc_clm_version'] ) ? sanitize_text_field( wp_unslash( $_POST['pc_clm_version'] ) ) : '1.0.0';
$release_date = isset( $_POST['pc_clm_release_date'] ) ? sanitize_text_field( wp_unslash( $_POST['pc_clm_release_date'] ) ) : current_time( 'Y-m-d' );
$category = isset( $_POST['pc_clm_category'] ) ? sanitize_text_field( wp_unslash( $_POST['pc_clm_category'] ) ) : 'new-features';
if ( empty( $title ) ) {
return;
}
$post_id = wp_insert_post( array(
'post_title' => $title,
'post_content' => $content,
'post_status' => 'publish',
'post_type' => $this->post_type->get_post_type_slug(),
) );
if ( ! is_wp_error( $post_id ) ) {
$this->post_type->update_meta( $post_id, 'version_number', $version );
$this->post_type->update_meta( $post_id, 'release_date', $release_date );
$this->post_type->update_meta( $post_id, 'category', $category );
add_action( 'admin_notices', function() {
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Changelog entry added successfully!', 'pc-changelog-manager-abc123' ) . '</p></div>';
} );
}
}
/**
* Render the add new changelog page.
*/
public function render_add_new_page() {
$categories = $this->post_type->get_categories();
$default_version = '1.0.0';
$default_date = current_time( 'Y-m-d' );
?>
<div class="wrap">
<h1 class="wp-heading-inline"><?php esc_html_e( 'Add New Changelog Entry', 'pc-changelog-manager-abc123' ); ?></h1>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=pc-clm-view-changelog' ) ); ?>" class="page-title-action">
<?php esc_html_e( 'View All Entries', 'pc-changelog-manager-abc123' ); ?>
</a>
<hr class="wp-header-end">
<div class="pc-clm-admin-add-form">
<form method="post" action="">
<?php wp_nonce_field( 'pc_clm_add_new', 'pc_clm_add_new_nonce' ); ?>
<table class="form-table">
<tr>
<th scope="row">
<label for="pc_clm_title"><?php esc_html_e( 'Title', 'pc-changelog-manager-abc123' ); ?> <span class="required">*</span></label>
</th>
<td>
<input type="text" id="pc_clm_title" name="pc_clm_title" class="regular-text" required>
<p class="description"><?php esc_html_e( 'Enter a title for this changelog entry.', 'pc-changelog-manager-abc123' ); ?></p>
</td>
</tr>
<tr>
<th scope="row">
<label for="pc_clm_version"><?php esc_html_e( 'Version Number', 'pc-changelog-manager-abc123' ); ?></label>
</th>
<td>
<input type="text" id="pc_clm_version" name="pc_clm_version" value="<?php echo esc_attr( $default_version ); ?>" class="regular-text">
<p class="description"><?php esc_html_e( 'The version number (e.g., 1.0.0)', 'pc-changelog-manager-abc123' ); ?></p>
</td>
</tr>
<tr>
<th scope="row">
<label for="pc_clm_release_date"><?php esc_html_e( 'Release Date', 'pc-changelog-manager-abc123' ); ?></label>
</th>
<td>
<input type="date" id="pc_clm_release_date" name="pc_clm_release_date" value="<?php echo esc_attr( $default_date ); ?>">
<p class="description"><?php esc_html_e( 'The release date for this version.', 'pc-changelog-manager-abc123' ); ?></p>
</td>
</tr>
<tr>
<th scope="row">
<label for="pc_clm_category"><?php esc_html_e( 'Category', 'pc-changelog-manager-abc123' ); ?></label>
</th>
<td>
<select id="pc_clm_category" name="pc_clm_category">
<?php foreach ( $categories as $slug => $name ) : ?>
<option value="<?php echo esc_attr( $slug ); ?>">
<?php echo esc_html( $name ); ?>
</option>
<?php endforeach; ?>
</select>
<p class="description"><?php esc_html_e( 'The category for this changelog entry.', 'pc-changelog-manager-abc123' ); ?></p>
</td>
</tr>
<tr>
<th scope="row">
<label for="pc_clm_content"><?php esc_html_e( 'Content', 'pc-changelog-manager-abc123' ); ?></label>
</th>
<td>
<?php
wp_editor( '', 'pc_clm_content', array(
'media_buttons' => true,
'textarea_rows' => 10,
'teeny' => false,
) );
?>
<p class="description"><?php esc_html_e( 'Describe the changes in this version.', 'pc-changelog-manager-abc123' ); ?></p>
</td>
</tr>
</table>
<p class="submit">
<input type="submit" name="pc_clm_submit_new" class="button button-primary" value="<?php esc_attr_e( 'Add Changelog Entry', 'pc-changelog-manager-abc123' ); ?>">
<a href="<?php echo esc_url( admin_url( 'admin.php?page=pc-clm-view-changelog' ) ); ?>" class="button">
<?php esc_html_e( 'Cancel', 'pc-changelog-manager-abc123' ); ?>
</a>
</p>
</form>
</div>
</div>
<?php
}
/**
* Highlight the correct menu item.
*
* @param string $parent_file Parent menu slug.
* @return string
*/
public function highlight_menu( $parent_file ) {
global $submenu_file, $current_screen;
if ( ! isset( $current_screen ) ) {
return $parent_file;
}
// Handle our custom view changelog page.
if ( isset( $current_screen->base ) && 'pc-clm-view-changelog' === $current_screen->base ) {
$parent_file = 'pc-clm-view-changelog';
$submenu_file = 'pc-clm-view-changelog';
}
// Handle our custom add new page.
if ( isset( $current_screen->base ) && 'pc-clm-add-new' === $current_screen->base ) {
$parent_file = 'pc-clm-view-changelog';
$submenu_file = 'pc-clm-add-new';
}
return $parent_file;
}
/**
* Add meta boxes to the changelog entry editor.
*/
public function add_meta_boxes() {
add_meta_box(
'pc_clm_entry_details',
__( 'Entry Details', 'pc-changelog-manager-abc123' ),
array( $this, 'render_entry_details_meta_box' ),
$this->post_type->get_post_type_slug(),
'normal',
'high'
);
add_meta_box(
'pc_clm_preview',
__( 'Preview', 'pc-changelog-manager-abc123' ),
array( $this, 'render_preview_meta_box' ),
$this->post_type->get_post_type_slug(),
'side',
'low'
);
}
/**
* Render the entry details meta box.
*
* @param WP_Post $post Current post object.
*/
public function render_entry_details_meta_box( $post ) {
// Add nonce field.
wp_nonce_field( 'pc_clm_save_meta', 'pc_clm_meta_nonce' );
// Get current values.
$version_number = $this->post_type->get_meta( $post->ID, 'version_number' );
$release_date = $this->post_type->get_meta( $post->ID, 'release_date' );
$category = $this->post_type->get_meta( $post->ID, 'category' );
$categories = $this->post_type->get_categories();
// Set default values for new posts.
if ( empty( $version_number ) ) {
$version_number = '1.0.0';
}
if ( empty( $release_date ) ) {
$release_date = current_time( 'Y-m-d' );
}
?>
<div class="pc-clm-meta-fields">
<div class="pc-clm-field-group">
<label for="pc_clm_version_number"><?php esc_html_e( 'Version Number', 'pc-changelog-manager-abc123' ); ?></label>
<input type="text" id="pc_clm_version_number" name="pc_clm_version_number" value="<?php echo esc_attr( $version_number ); ?>" placeholder="e.g., 1.0.0" />
<p class="description"><?php esc_html_e( 'The version number for this changelog entry (e.g., 1.0.0, 2.1.3).', 'pc-changelog-manager-abc123' ); ?></p>
</div>
<div class="pc-clm-field-group">
<label for="pc_clm_release_date"><?php esc_html_e( 'Release Date', 'pc-changelog-manager-abc123' ); ?></label>
<input type="date" id="pc_clm_release_date" name="pc_clm_release_date" value="<?php echo esc_attr( $release_date ); ?>" />
<p class="description"><?php esc_html_e( 'The release date for this version.', 'pc-changelog-manager-abc123' ); ?></p>
</div>
<div class="pc-clm-field-group">
<label for="pc_clm_category"><?php esc_html_e( 'Category', 'pc-changelog-manager-abc123' ); ?></label>
<select id="pc_clm_category" name="pc_clm_category">
<?php foreach ( $categories as $slug => $name ) : ?>
<option value="<?php echo esc_attr( $slug ); ?>" <?php selected( $category, $slug ); ?>>
<?php echo esc_html( $name ); ?>
</option>
<?php endforeach; ?>
</select>
<p class="description"><?php esc_html_e( 'The category for this changelog entry.', 'pc-changelog-manager-abc123' ); ?></p>
</div>
</div>
<?php
}
/**
* Render the preview meta box.
*
* @param WP_Post $post Current post object.
*/
public function render_preview_meta_box( $post ) {
$view_page_url = pc_clm_get_changelog_page_url();
?>
<div class="pc-clm-preview-box">
<p><?php esc_html_e( 'View your changelog page to see how entries will appear to visitors.', 'pc-changelog-manager-abc123' ); ?></p>
<a href="<?php echo esc_url( $view_page_url ); ?>" target="_blank" class="button button-secondary">
<span class="dashicons dashicons-external" style="margin-top: 3px;"></span>
<?php esc_html_e( 'View Changelog Page', 'pc-changelog-manager-abc123' ); ?>
</a>
</div>
<?php
}
/**
* Save meta box data.
*
* @param int $post_id Post ID.
* @param WP_Post $post Post object.
*/
public function save_meta( $post_id, $post ) {
// Verify nonce.
if ( ! isset( $_POST['pc_clm_meta_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['pc_clm_meta_nonce'] ) ), 'pc_clm_save_meta' ) ) {
return $post_id;
}
// Check if user has permission.
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return $post_id;
}
// Check if autosave.
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return $post_id;
}
// Save version number.
if ( isset( $_POST['pc_clm_version_number'] ) ) {
$this->post_type->update_meta( $post_id, 'version_number', sanitize_text_field( wp_unslash( $_POST['pc_clm_version_number'] ) ) );
}
// Save release date.
if ( isset( $_POST['pc_clm_release_date'] ) ) {
$this->post_type->update_meta( $post_id, 'release_date', sanitize_text_field( wp_unslash( $_POST['pc_clm_release_date'] ) ) );
}
// Save category.
if ( isset( $_POST['pc_clm_category'] ) ) {
$allowed_categories = array_keys( $this->post_type->get_categories() );
$category = sanitize_text_field( wp_unslash( $_POST['pc_clm_category'] ) );
if ( in_array( $category, $allowed_categories, true ) ) {
$this->post_type->update_meta( $post_id, 'category', $category );
}
}
}
/**
* Set custom columns for the admin list view.
*
* @param array $columns Existing columns.
* @return array
*/
public function set_custom_columns( $columns ) {
unset( $columns['date'] );
$new_columns = array(
'version' => __( 'Version', 'pc-changelog-manager-abc123' ),
'release_date' => __( 'Release Date', 'pc-changelog-manager-abc123' ),
'category' => __( 'Category', 'pc-changelog-manager-abc123' ),
'date' => __( 'Date', 'pc-changelog-manager-abc123' ),
);
return array_merge( $columns, $new_columns );
}
/**
* Render custom column content.
*
* @param string $column Column name.
* @param int $post_id Post ID.
*/
public function render_custom_columns( $column, $post_id ) {
switch ( $column ) {
case 'version':
$version = $this->post_type->get_meta( $post_id, 'version_number' );
echo esc_html( $version );
break;
case 'release_date':
$release_date = $this->post_type->get_meta( $post_id, 'release_date' );
if ( $release_date ) {
echo esc_html( date_i18n( get_option( 'date_format' ), strtotime( $release_date ) ) );
} else {
echo '&mdash;';
}
break;
case 'category':
$category = $this->post_type->get_meta( $post_id, 'category' );
$categories = $this->post_type->get_categories();
$category_name = isset( $categories[ $category ] ) ? $categories[ $category ] : $category;
$category_class = 'pc-clm-category-' . esc_attr( $category );
echo '<span class="pc-clm-category-badge ' . esc_attr( $category_class ) . '">';
echo esc_html( $category_name );
echo '</span>';
break;
}
}
/**
* Modify row actions in the admin list.
*
* @param array $actions Existing actions.
* @param WP_Post $post Post object.
* @return array
*/
public function modify_row_actions( $actions, $post ) {
if ( $this->post_type->get_post_type_slug() !== $post->post_type ) {
return $actions;
}
// Remove quick edit as it doesn't work well with custom meta.
if ( isset( $actions['inline hide-if-no-js'] ) ) {
unset( $actions['inline hide-if-no-js'] );
}
return $actions;
}
}
/**
* Get the changelog page URL.
*
* @return string
*/
function pc_clm_get_changelog_page_url() {
// First try to get the custom changelog page.
$changelog_page = get_page_by_path( 'changelog' );
if ( $changelog_page && 'publish' === $changelog_page->post_status ) {
return get_permalink( $changelog_page );
}
// Fall back to the post type archive URL.
$post_type = get_post_type_object( 'pc_changelog' );
if ( $post_type && $post_type->has_archive ) {
return get_post_type_archive_link( 'pc_changelog' );
}
// Final fallback to home URL with changelog slug.
return home_url( '/changelog/' );
}

View File

@@ -0,0 +1,168 @@
<?php
/**
* Custom Post Type Handler
*
* @package PCChangelogManager
*/
// Prevent direct access to the file.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Handles registration and management of the changelog custom post type.
*/
class PC_CLM_Post_Type {
/**
* Post type slug.
*
* @var string
*/
const POST_TYPE_SLUG = 'pc_changelog';
/**
* Constructor.
*/
public function __construct() {
// Intentionally empty.
}
/**
* Register the custom post type.
*/
public function register() {
$labels = array(
'name' => _x( 'Changelogs', 'Post type general name', 'pc-changelog-manager-abc123' ),
'singular_name' => _x( 'Changelog Entry', 'Post type singular name', 'pc-changelog-manager-abc123' ),
'menu_name' => _x( 'Changelogs', 'Admin Menu text', 'pc-changelog-manager-abc123' ),
'name_admin_bar' => _x( 'Changelog Entry', 'Add New on Toolbar', 'pc-changelog-manager-abc123' ),
'add_new' => __( 'Add New', 'pc-changelog-manager-abc123' ),
'add_new_item' => __( 'Add New Changelog Entry', 'pc-changelog-manager-abc123' ),
'new_item' => __( 'New Changelog Entry', 'pc-changelog-manager-abc123' ),
'edit_item' => __( 'Edit Changelog Entry', 'pc-changelog-manager-abc123' ),
'view_item' => __( 'View Changelog Entry', 'pc-changelog-manager-abc123' ),
'all_items' => __( 'All Changelog Entries', 'pc-changelog-manager-abc123' ),
'search_items' => __( 'Search Changelog Entries', 'pc-changelog-manager-abc123' ),
'parent_item_colon' => __( 'Parent Changelog Entries:', 'pc-changelog-manager-abc123' ),
'not_found' => __( 'No changelog entries found.', 'pc-changelog-manager-abc123' ),
'not_found_in_trash' => __( 'No changelog entries found in Trash.', 'pc-changelog-manager-abc123' ),
'featured_image' => _x( 'Changelog Cover Image', 'Overrides the "Featured Image" phrase for this post type.', 'pc-changelog-manager-abc123' ),
'set_featured_image' => _x( 'Set cover image', 'Overrides the "Set featured image" phrase.', 'pc-changelog-manager-abc123' ),
'remove_featured_image' => _x( 'Remove cover image', 'Overrides the "Remove featured image" phrase.', 'pc-changelog-manager-abc123' ),
'use_featured_image' => _x( 'Use as cover image', 'Overrides the "Use as featured image" phrase.', 'pc-changelog-manager-abc123' ),
);
$args = array(
'labels' => $labels,
'public' => true,
'publicly_queryable' => true,
'show_ui' => true,
'show_in_menu' => false,
'query_var' => true,
'rewrite' => array( 'slug' => 'changelog-entry' ),
'capability_type' => 'post',
'map_meta_cap' => true,
'has_archive' => false,
'hierarchical' => false,
'menu_position' => 30,
'menu_icon' => 'dashicons-backup',
'supports' => array(
'title',
'editor',
'revisions',
'author',
),
'show_in_rest' => true,
'rest_base' => 'changelog',
);
register_post_type( self::POST_TYPE_SLUG, $args );
}
/**
* Get the post type slug.
*
* @return string
*/
public function get_post_type_slug() {
return self::POST_TYPE_SLUG;
}
/**
* Get changelog entries.
*
* @param array $args Query arguments.
* @return WP_Query
*/
public function get_entries( $args = array() ) {
$defaults = array(
'post_type' => self::POST_TYPE_SLUG,
'post_status' => 'publish',
'posts_per_page' => -1,
'orderby' => 'date',
'order' => 'DESC',
);
$args = wp_parse_args( $args, $defaults );
return new WP_Query( $args );
}
/**
* Get a single changelog entry.
*
* @param int $post_id Post ID.
* @return WP_Post|null
*/
public function get_entry( $post_id ) {
$post = get_post( $post_id );
if ( ! $post || self::POST_TYPE_SLUG !== $post->post_type ) {
return null;
}
return $post;
}
/**
* Get categories for changelog entries.
*
* @return array
*/
public function get_categories() {
return array(
'new-features' => __( 'New Features', 'pc-changelog-manager-abc123' ),
'bug-fixes' => __( 'Bug Fixes', 'pc-changelog-manager-abc123' ),
'improvements' => __( 'Improvements', 'pc-changelog-manager-abc123' ),
'security' => __( 'Security', 'pc-changelog-manager-abc123' ),
'deprecated' => __( 'Deprecated', 'pc-changelog-manager-abc123' ),
'removed' => __( 'Removed', 'pc-changelog-manager-abc123' ),
);
}
/**
* Get changelog entry meta.
*
* @param int $post_id Post ID.
* @param string $key Meta key.
* @param bool $single Whether to return a single value.
* @return mixed
*/
public function get_meta( $post_id, $key, $single = true ) {
return get_post_meta( $post_id, '_pc_clm_' . $key, $single );
}
/**
* Set changelog entry meta.
*
* @param int $post_id Post ID.
* @param string $key Meta key.
* @param mixed $value Meta value.
* @return bool
*/
public function update_meta( $post_id, $key, $value ) {
return update_post_meta( $post_id, '_pc_clm_' . $key, $value );
}
}

View File

@@ -0,0 +1,438 @@
<?php
/**
* Public Handler
*
* @package PCChangelogManager
*/
// Prevent direct access to the file.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Handles all public-facing functionality for the changelog manager.
*/
class PC_CLM_Public {
/**
* Instance of the post type class.
*
* @var PC_CLM_Post_Type
*/
private $post_type;
/**
* Constructor.
*
* @param PC_CLM_Post_Type $post_type Post type instance.
*/
public function __construct( $post_type ) {
$this->post_type = $post_type;
$this->init_hooks();
}
/**
* Initialize public hooks.
*/
private function init_hooks() {
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_styles' ) );
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
add_filter( 'template_include', array( $this, 'template_loader' ) );
add_shortcode( 'changelog', array( $this, 'shortcode_changelog' ) );
add_action( 'widgets_init', array( $this, 'register_widgets' ) );
add_action( 'wp_ajax_pc_clm_upvote', array( $this, 'handle_upvote' ) );
add_action( 'wp_ajax_nopriv_pc_clm_upvote', array( $this, 'handle_upvote' ) );
}
/**
* Enqueue public styles.
*/
public function enqueue_styles() {
// Check if we're on the changelog page or if shortcode is used.
if ( ! $this->is_changelog_page() && ! $this->is_changelog_single() && ! $this->has_shortcode() ) {
return;
}
$min_suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
if ( file_exists( PC_CLM_PLUGIN_DIR . 'public/css/public-style.css' ) ) {
// Enqueue main stylesheet.
if ( ! wp_style_is( 'pc-clm-public-style', 'enqueued' ) ) {
wp_enqueue_style(
'pc-clm-public-style',
PC_CLM_PLUGIN_URL . 'public/css/public-style.css',
array(),
PC_CLM_VERSION
);
}
}
}
/**
* Check if we're on the changelog page.
*
* @return bool
*/
private function is_changelog_page() {
return is_post_type_archive( $this->post_type->get_post_type_slug() );
}
private function is_changelog_single() {
return is_singular( $this->post_type->get_post_type_slug() );
}
/**
* Check if the page has the changelog shortcode.
*
* @return bool
*/
private function has_shortcode() {
if ( ! is_singular() ) {
return false;
}
$post = get_post();
if ( ! $post ) {
return false;
}
return has_shortcode( $post->post_content, 'changelog' );
}
public function template_loader( $template ) {
if ( is_singular( $this->post_type->get_post_type_slug() ) ) {
$custom_template = PC_CLM_PLUGIN_DIR . 'public/templates/single-pc_changelog.php';
if ( file_exists( $custom_template ) ) {
return $custom_template;
}
}
return $template;
}
/**
* Changelog shortcode.
*
* @param array $atts Shortcode attributes.
* @return string
*/
public function shortcode_changelog( $atts ) {
$atts = shortcode_atts(
array(
'limit' => 10,
'category' => '',
'show_date' => 'yes',
'show_title' => 'yes',
'order' => 'DESC',
),
$atts,
'changelog'
);
// Enqueue styles for the shortcode output.
if ( file_exists( PC_CLM_PLUGIN_DIR . 'public/css/public-style.css' ) ) {
wp_enqueue_style( 'pc-clm-public-style', PC_CLM_PLUGIN_URL . 'public/css/public-style.css', array(), PC_CLM_VERSION );
}
// Build query args.
$query_args = array(
'post_type' => $this->post_type->get_post_type_slug(),
'post_status' => 'publish',
'posts_per_page' => intval( $atts['limit'] ),
'orderby' => 'date',
'order' => 'DESC' === $atts['order'] ? 'DESC' : 'ASC',
);
// Filter by category if specified.
if ( ! empty( $atts['category'] ) ) {
$query_args['meta_query'] = array(
array(
'key' => '_pc_clm_category',
'value' => sanitize_text_field( $atts['category'] ),
),
);
}
$query = new WP_Query( $query_args );
ob_start();
if ( $query->have_posts() ) {
?>
<div class="pc-clm-shortcode-wrapper">
<?php if ( 'yes' === $atts['show_title'] ) : ?>
<h2 class="pc-clm-shortcode-title"><?php esc_html_e( 'Changelog', 'pc-changelog-manager-abc123' ); ?></h2>
<?php endif; ?>
<div class="pc-clm-entries-list">
<?php
while ( $query->have_posts() ) :
$query->the_post();
$this->render_entry( get_the_ID(), $atts );
endwhile;
?>
</div>
</div>
<?php
wp_reset_postdata();
} else {
?>
<div class="pc-clm-no-entries">
<p><?php esc_html_e( 'No changelog entries found.', 'pc-changelog-manager-abc123' ); ?></p>
</div>
<?php
}
return ob_get_clean();
}
/**
* Render a single changelog entry.
*
* @param int $post_id Post ID.
* @param array $atts Shortcode attributes.
*/
private function render_entry( $post_id, $atts ) {
$version = $this->post_type->get_meta( $post_id, 'version_number' );
$release_date = $this->post_type->get_meta( $post_id, 'release_date' );
$category = $this->post_type->get_meta( $post_id, 'category' );
$categories = $this->post_type->get_categories();
$category_name = isset( $categories[ $category ] ) ? $categories[ $category ] : $category;
$upvote_count = $this->get_upvote_count( $post_id );
$has_voted = $this->has_user_voted( $post_id );
$entry_classes = array( 'pc-clm-entry' );
if ( $category ) {
$entry_classes[] = 'pc-clm-category-' . $category;
}
?>
<article class="<?php echo esc_attr( implode( ' ', $entry_classes ) ); ?>" id="pc-clm-entry-<?php echo esc_attr( $post_id ); ?>">
<div class="pc-clm-entry-inner">
<div class="pc-clm-entry-header">
<div class="pc-clm-entry-meta">
<?php if ( $version ) : ?>
<span class="pc-clm-version-badge"><?php echo esc_html( $version ); ?></span>
<?php endif; ?>
<?php if ( 'yes' === $atts['show_date'] && $release_date ) : ?>
<span class="pc-clm-release-date"><?php echo esc_html( date_i18n( get_option( 'date_format' ), strtotime( $release_date ) ) ); ?></span>
<?php endif; ?>
<?php if ( $category ) : ?>
<span class="pc-clm-category-badge pc-clm-category-<?php echo esc_attr( $category ); ?>"><?php echo esc_html( $category_name ); ?></span>
<?php endif; ?>
</div>
<div class="pc-clm-upvote-section">
<button class="pc-clm-upvote-btn <?php echo $has_voted ? 'voted' : ''; ?>"
data-post-id="<?php echo esc_attr( $post_id ); ?>"
<?php echo $has_voted ? 'disabled' : ''; ?>>
<span class="pc-clm-upvote-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2l2.4 7.4h7.6l-6 4.6 2.3 7.4-6.3-4.6-6.3 4.6 2.3-7.4-6-4.6h7.6z"/>
</svg>
</span>
<span class="pc-clm-upvote-count"><?php echo esc_html( $upvote_count ); ?></span>
</button>
<span class="pc-clm-upvote-label"><?php echo esc_html( _n( 'upvote', 'upvotes', $upvote_count, 'pc-changelog-manager-abc123' ) ); ?></span>
</div>
</div>
<h3 class="pc-clm-entry-title">
<a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
</h3>
<div class="pc-clm-entry-content">
<?php the_excerpt(); ?>
</div>
</div>
</article>
<?php
}
/**
* Enqueue public scripts.
*/
public function enqueue_scripts() {
// Check if we're on the changelog page or if shortcode is used.
if ( ! $this->is_changelog_page() && ! $this->is_changelog_single() && ! $this->has_shortcode() ) {
return;
}
$min_suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
if ( file_exists( PC_CLM_PLUGIN_DIR . 'public/js/public-script.js' ) ) {
wp_enqueue_script(
'pc-clm-public-script',
PC_CLM_PLUGIN_URL . 'public/js/public-script.js',
array( 'jquery' ),
PC_CLM_VERSION,
true
);
wp_localize_script(
'pc-clm-public-script',
'pc_clm_ajax',
array(
'ajax_url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'pc_clm_upvote_nonce' ),
)
);
}
}
/**
* Handle upvote AJAX request.
*/
public function handle_upvote() {
check_ajax_referer( 'pc_clm_upvote_nonce', 'nonce' );
if ( ! isset( $_POST['post_id'] ) ) {
wp_die( 'Invalid request' );
}
$post_id = intval( $_POST['post_id'] );
$ip_address = $_SERVER['REMOTE_ADDR'];
$user_id = get_current_user_id();
// Check if user has already voted (by IP or user ID if logged in)
$voted_key = $user_id ? "pc_clm_voted_user_{$user_id}" : "pc_clm_voted_ip_{$ip_address}";
$voted_posts = get_transient( $voted_key );
if ( ! $voted_posts ) {
$voted_posts = array();
}
if ( in_array( $post_id, $voted_posts ) ) {
wp_send_json_error( array( 'message' => __( 'You have already voted for this entry.', 'pc-changelog-manager-abc123' ) ) );
return;
}
// Get current vote count
$vote_count = get_post_meta( $post_id, '_pc_clm_upvotes', true ) ?: 0;
$new_vote_count = intval( $vote_count ) + 1;
// Update vote count
update_post_meta( $post_id, '_pc_clm_upvotes', $new_vote_count );
// Mark as voted (expires after 30 days)
$voted_posts[] = $post_id;
set_transient( $voted_key, $voted_posts, 30 * DAY_IN_SECONDS );
wp_send_json_success( array(
'vote_count' => $new_vote_count,
'message' => __( 'Thank you for voting!', 'pc-changelog-manager-abc123' ),
) );
}
/**
* Get upvote count for a post.
*
* @param int $post_id Post ID.
* @return int
*/
public function get_upvote_count( $post_id ) {
$vote_count = get_post_meta( $post_id, '_pc_clm_upvotes', true );
return intval( $vote_count );
}
/**
* Check if current user has voted for a post.
*
* @param int $post_id Post ID.
* @return bool
*/
public function has_user_voted( $post_id ) {
$ip_address = $_SERVER['REMOTE_ADDR'];
$user_id = get_current_user_id();
$voted_key = $user_id ? "pc_clm_voted_user_{$user_id}" : "pc_clm_voted_ip_{$ip_address}";
$voted_posts = get_transient( $voted_key );
if ( ! $voted_posts ) {
return false;
}
return in_array( $post_id, $voted_posts );
}
/**
* Register widgets.
*/
public function register_widgets() {
require_once PC_CLM_PLUGIN_DIR . 'public/widgets/class-changelog-widget.php';
register_widget( 'PC_CLM_Widget' );
}
}
/**
* Template function to display changelog entries.
*
* @param array $args Query arguments.
*/
function pc_clm_display_changelog( $args = array() ) {
$post_type = new PC_CLM_Post_Type();
$query = $post_type->get_entries( $args );
if ( $query->have_posts() ) {
?>
<div class="pc-clm-entries-list">
<?php
while ( $query->have_posts() ) :
$query->the_post();
$post_id = get_the_ID();
$version = $post_type->get_meta( $post_id, 'version_number' );
$release_date = $post_type->get_meta( $post_id, 'release_date' );
$category = $post_type->get_meta( $post_id, 'category' );
$categories = $post_type->get_categories();
$category_name = isset( $categories[ $category ] ) ? $categories[ $category ] : $category;
$entry_classes = array( 'pc-clm-entry' );
if ( $category ) {
$entry_classes[] = 'pc-clm-category-' . $category;
}
?>
<article id="post-<?php the_ID(); ?>" <?php post_class( $entry_classes ); ?>>
<div class="pc-clm-entry-inner">
<div class="pc-clm-entry-meta">
<?php if ( $version ) : ?>
<span class="pc-clm-version-badge"><?php echo esc_html( $version ); ?></span>
<?php endif; ?>
<?php if ( $release_date ) : ?>
<span class="pc-clm-release-date"><?php echo esc_html( date_i18n( get_option( 'date_format' ), strtotime( $release_date ) ) ); ?></span>
<?php endif; ?>
<?php if ( $category ) : ?>
<span class="pc-clm-category-badge pc-clm-category-<?php echo esc_attr( $category ); ?>"><?php echo esc_html( $category_name ); ?></span>
<?php endif; ?>
</div>
<h3 class="pc-clm-entry-title">
<a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
</h3>
<div class="pc-clm-entry-content">
<?php the_excerpt(); ?>
</div>
</div>
</article>
<?php
endwhile;
?>
</div>
<?php
} else {
?>
<div class="pc-clm-no-entries">
<p><?php esc_html_e( 'No changelog entries found.', 'pc-changelog-manager-abc123' ); ?></p>
</div>
<?php
}
wp_reset_postdata();
}

View File

@@ -0,0 +1,100 @@
<?php
/**
* Plugin Name: Plugin Compass Change log v2
* Plugin URI: https://plugincompass.com
* Description: Manage and display changelog entries with a custom post type, shortcode, widget and templates.
* Version: 1.0.0
* Author: Plugin Compass
* Author URI: https://plugincompass.com
* Update URI: https://plugincompass.com
* Text Domain: pc-changelog-manager-abc123
* Domain Path: /languages
* License: GPLv2 or later
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// Plugin constants.
if ( ! defined( 'PC_CLM_VERSION' ) ) {
define( 'PC_CLM_VERSION', '1.0.0' );
}
if ( ! defined( 'PC_CLM_PLUGIN_DIR' ) ) {
define( 'PC_CLM_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
}
if ( ! defined( 'PC_CLM_PLUGIN_URL' ) ) {
define( 'PC_CLM_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
}
// Include core classes.
require_once PC_CLM_PLUGIN_DIR . 'includes/class-changelog-post-type.php';
require_once PC_CLM_PLUGIN_DIR . 'includes/class-changelog-admin.php';
require_once PC_CLM_PLUGIN_DIR . 'includes/class-changelog-public.php';
/**
* Initialize the plugin.
*/
function pc_clm_init() {
// Register post type and initialize handlers.
$post_type = new PC_CLM_Post_Type();
$post_type->register();
// Admin hooks.
if ( is_admin() ) {
new PC_CLM_Admin( $post_type );
}
// Public hooks.
new PC_CLM_Public( $post_type );
}
add_action( 'init', 'pc_clm_init' );
/**
* Create the changelog page if it doesn't exist.
*/
function pc_clm_create_changelog_page() {
$slug = 'changelog';
// Check if the page already exists by slug
$page_check = get_posts(array(
'name' => $slug,
'post_type' => 'page',
'post_status' => 'publish',
'numberposts' => 1
));
if(empty($page_check)){
$new_page = array(
'post_type' => 'page',
'post_title' => 'Changelog',
'post_name' => $slug,
'post_content' => '[changelog]',
'post_status' => 'publish',
'post_author' => 1,
);
wp_insert_post($new_page);
}
}
/**
* Activation callback.
*/
function pc_clm_activate() {
$pt = new PC_CLM_Post_Type();
$pt->register();
pc_clm_create_changelog_page();
flush_rewrite_rules();
}
register_activation_hook( __FILE__, 'pc_clm_activate' );
/**
* Deactivation callback.
*/
function pc_clm_deactivate() {
flush_rewrite_rules();
}
register_deactivation_hook( __FILE__, 'pc_clm_deactivate' );

View File

@@ -0,0 +1,985 @@
/**
* PC Changelog Manager - Public Styles
*
* @package PCChangelogManager
*/
/* ========================================
CSS Variables
======================================== */
:root {
/* Primary Colors */
--pc-clm-primary: #2271b1;
--pc-clm-primary-hover: #135e96;
--pc-clm-primary-light: #e8f4fd;
/* Semantic Colors */
--pc-clm-success: #000000;
--pc-clm-success-light: #ffffff;
--pc-clm-warning: #dba617;
--pc-clm-warning-light: #fef3cd;
--pc-clm-error: #000000;
--pc-clm-error-light: #ffffff;
--pc-clm-info: #000000;
--pc-clm-info-light: #ffffff;
/* Category Colors */
--pc-clm-category-new-features: #000000;
--pc-clm-category-bug-fixes: #333333;
--pc-clm-category-improvements: #666666;
--pc-clm-category-security: #dba617;
--pc-clm-category-deprecated: #888888;
--pc-clm-category-removed: #aaaaaa;
/* Typography */
--pc-clm-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
--pc-clm-font-size-base: 16px;
--pc-clm-font-size-sm: 14px;
--pc-clm-font-size-lg: 18px;
--pc-clm-font-size-xl: 24px;
--pc-clm-font-size-2xl: 32px;
/* Spacing */
--pc-clm-spacing-xs: 4px;
--pc-clm-spacing-sm: 8px;
--pc-clm-spacing-md: 16px;
--pc-clm-spacing-lg: 24px;
--pc-clm-spacing-xl: 32px;
--pc-clm-spacing-2xl: 48px;
/* Border Radius */
--pc-clm-radius-sm: 4px;
--pc-clm-radius-md: 8px;
--pc-clm-radius-lg: 12px;
/* Shadows */
--pc-clm-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--pc-clm-shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
--pc-clm-shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
/* Transitions */
--pc-clm-transition-fast: 150ms ease;
--pc-clm-transition-base: 250ms ease;
--pc-clm-transition-slow: 350ms ease;
}
.pc-clm-archive-container {
font-family: var(--pc-clm-font-family);
font-size: var(--pc-clm-font-size-base);
line-height: 1.6;
color: #000000;
padding: 0;
max-width: 100%;
margin: 0 auto;
}
.pc-clm-single-entry {
font-family: var(--pc-clm-font-family);
font-size: var(--pc-clm-font-size-base);
line-height: 1.6;
color: #000000;
background-color: #ffffff;
padding: var(--pc-clm-spacing-lg);
border-radius: var(--pc-clm-radius-md);
border: 2px solid #000000;
max-width: 900px;
margin: 20px auto;
}
.pc-clm-archive-header {
text-align: left;
margin-bottom: 40px;
padding-bottom: 20px;
border-bottom: 2px solid #000000;
}
.pc-clm-archive-title {
font-size: var(--pc-clm-font-size-2xl);
font-weight: 800;
margin: 0 0 12px 0;
color: #000000;
letter-spacing: -0.02em;
}
.pc-clm-archive-header .archive-description {
font-size: var(--pc-clm-font-size-lg);
color: #000000;
margin: 0;
background: transparent;
}
/* ========================================
Archive Content
======================================== */
.pc-clm-archive-content {
margin-bottom: 32px;
min-height: 100px;
}
.pc-clm-archive-content:empty {
display: none;
}
/* ========================================
Changelog Entry
======================================== */
.pc-clm-entry {
position: relative;
margin-bottom: var(--pc-clm-spacing-md);
background-color: #ffffff;
border-radius: var(--pc-clm-radius-md);
border: 2px solid #000000;
border-left: 4px solid #000000;
transition: all var(--pc-clm-transition-base);
overflow: hidden;
max-width: 100%;
}
.pc-clm-entry-wrapper {
padding: var(--pc-clm-spacing-lg);
background-color: #ffffff;
}
.pc-clm-entry {
position: relative;
margin-bottom: var(--pc-clm-spacing-md);
background-color: #ffffff;
border-radius: var(--pc-clm-radius-md);
border: 2px solid #000000;
border-left: 4px solid #000000;
transition: all var(--pc-clm-transition-base);
overflow: hidden;
}
.pc-clm-archive-container .pc-clm-entry {
max-width: 100%;
}
.pc-clm-entry:hover {
border-color: #000000;
box-shadow: var(--pc-clm-shadow-lg);
transform: translateY(-2px);
}
.pc-clm-entry-wrapper:hover {
box-shadow: var(--pc-clm-shadow-lg);
transform: translateY(-2px);
}
.pc-clm-entry-inner {
padding: var(--pc-clm-spacing-lg);
background-color: #ffffff;
border-radius: var(--pc-clm-radius-md);
}
.pc-clm-entry.pc-clm-category-new-features { border-left-color: var(--pc-clm-category-new-features); }
.pc-clm-entry.pc-clm-category-bug-fixes { border-left-color: var(--pc-clm-category-bug-fixes); }
.pc-clm-entry.pc-clm-category-improvements { border-left-color: var(--pc-clm-category-improvements); }
.pc-clm-entry.pc-clm-category-security { border-left-color: var(--pc-clm-category-security); }
.pc-clm-entry.pc-clm-category-deprecated { border-left-color: var(--pc-clm-category-deprecated); }
.pc-clm-entry.pc-clm-category-removed { border-left-color: var(--pc-clm-category-removed); }
.pc-clm-entry:last-child {
margin-bottom: 0;
}
.pc-clm-entry-content-wrapper {
background-color: #ffffff;
padding: 12px;
border-radius: var(--pc-clm-radius-md);
margin-top: 8px;
}
.pc-clm-entry-content {
background-color: #ffffff;
padding: 12px;
}
.pc-clm-single-entry .pc-clm-entry-content {
background-color: transparent;
padding: 0;
margin-top: 16px;
}
.pc-clm-single-entry .pc-clm-entry-title {
font-size: 28px;
font-weight: 700;
margin-bottom: 16px;
}
.pc-clm-entry-title {
font-size: var(--pc-clm-font-size-lg);
font-weight: 700;
margin: 0 0 var(--pc-clm-spacing-xs) 0;
line-height: 1.3;
}
.pc-clm-entry-title a {
color: #000000;
text-decoration: none;
}
.pc-clm-entry-title a:hover {
color: #666666;
}
.pc-clm-entry-meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
margin-bottom: var(--pc-clm-spacing-sm);
}
/* Version Badge */
.pc-clm-version-badge {
display: inline-block;
padding: 2px 8px;
font-size: 12px;
font-weight: 700;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
color: #ffffff;
background-color: #000000;
border-radius: var(--pc-clm-radius-sm);
}
/* Release Date */
.pc-clm-release-date,
.pc-clm-date {
font-size: 13px;
color: #000000;
font-weight: 600;
}
/* Category Badge */
.pc-clm-category-badge {
display: inline-block;
padding: 2px 10px;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.03em;
border-radius: 20px;
background-color: #f5f5f5;
color: #000000;
border: 1px solid #000000;
}
.pc-clm-category-badge.pc-clm-category-new-features { background-color: #f5f5f5; color: #000000; border-color: #000000; }
.pc-clm-category-badge.pc-clm-category-bug-fixes { background-color: #e0e0e0; color: #333333; border-color: #333333; }
.pc-clm-category-badge.pc-clm-category-improvements { background-color: #d0d0d0; color: #666666; border-color: #666666; }
.pc-clm-category-badge.pc-clm-category-security { background-color: #ffffff; color: #dba617; border-color: #dba617; }
/* ========================================
Entry Content
======================================== */
.pc-clm-entry-content {
font-size: 15px;
line-height: 1.6;
color: #000000;
font-weight: 500;
background-color: #ffffff;
padding: 12px;
}
.pc-clm-entry-content p {
margin: 0 0 var(--pc-clm-spacing-sm) 0;
}
.pc-clm-entry-content p:last-child {
margin-bottom: 0;
}
.pc-clm-entry-content ul,
.pc-clm-entry-content ol {
margin: 0 0 var(--pc-clm-spacing-md) 0;
padding-left: 20px;
}
.pc-clm-entry-content li {
margin-bottom: 4px;
}
/* ========================================
Entry Header
======================================== */
.pc-clm-entry-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: var(--pc-clm-spacing-sm);
gap: var(--pc-clm-spacing-md);
}
.pc-clm-entry-meta {
flex: 1;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
margin-bottom: 0;
}
/* ========================================
Upvote Section
======================================== */
.pc-clm-upvote-section {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
.pc-clm-upvote-btn {
display: flex;
align-items: center;
gap: 4px;
padding: 8px 12px;
background-color: #ffffff;
border: 2px solid #000000;
border-radius: 20px;
color: #000000;
font-size: 12px;
font-weight: 700;
cursor: pointer;
transition: all var(--pc-clm-transition-fast);
outline: none;
position: relative;
min-width: 70px;
justify-content: center;
}
.pc-clm-upvote-btn:not(:disabled):hover {
background-color: #000000;
border-color: #000000;
color: #ffffff;
transform: translateY(-2px);
box-shadow: var(--pc-clm-shadow-md);
}
.pc-clm-upvote-btn:not(:disabled):active {
transform: translateY(0);
box-shadow: none;
}
.pc-clm-upvote-btn.voted {
background-color: #000000;
border-color: #000000;
color: #ffffff;
}
.pc-clm-upvote-btn.loading {
opacity: 0.7;
}
.pc-clm-upvote-btn.loading .pc-clm-upvote-icon {
animation: pulse 1s ease-in-out infinite;
}
.pc-clm-upvote-icon svg {
width: 16px;
height: 16px;
transition: transform var(--pc-clm-transition-fast);
}
.pc-clm-upvote-btn:hover .pc-clm-upvote-icon svg {
transform: scale(1.1);
}
.pc-clm-upvote-count {
font-weight: 700;
min-width: 16px;
text-align: center;
}
.pc-clm-upvote-label {
font-size: 10px;
color: #000000;
text-transform: uppercase;
letter-spacing: 0.03em;
font-weight: 600;
white-space: nowrap;
}
.pc-clm-upvote-btn.voted + .pc-clm-upvote-label {
color: #000000;
font-weight: 700;
}
/* Loading animation */
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
/* ========================================
Entry Footer
======================================== */
.pc-clm-entry-footer {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #f0f0f0;
}
.pc-clm-read-more {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 13px;
font-weight: 600;
color: #000000;
text-decoration: none;
transition: color 0.15s ease;
}
.pc-clm-read-more:hover {
color: #666666;
}
.pc-clm-read-more .dashicons {
font-size: 14px;
width: 14px;
height: 14px;
}
.pc-clm-read-more:hover .dashicons {
transform: translateX(2px);
}
/* ========================================
No Entries State
======================================== */
.pc-clm-no-entries,
.pc-clm-widget-empty {
text-align: center;
padding: 48px 20px;
color: #000000;
background-color: #ffffff;
border-radius: 6px;
border: 2px solid #000000;
font-weight: 600;
margin: 20px 0;
}
.pc-clm-no-entries p,
.pc-clm-widget-empty p {
margin: 0;
}
/* ========================================
Shortcode Styles
======================================== */
.pc-clm-shortcode-wrapper {
font-family: var(--pc-clm-font-family);
}
.pc-clm-shortcode-title {
font-size: var(--pc-clm-font-size-xl);
font-weight: 700;
margin: 0 0 var(--pc-clm-spacing-lg) 0;
color: #000000;
}
/* ========================================
Widget Styles
======================================== */
.pc-clm-widget-list {
list-style: none;
margin: 0;
padding: 0;
}
.pc-clm-widget-entry {
padding: var(--pc-clm-spacing-md) 0;
border-bottom: 1px solid #000000;
}
.pc-clm-widget-entry:last-child {
border-bottom: none;
padding-bottom: 0;
}
.pc-clm-widget-entry-header {
display: flex;
align-items: center;
gap: var(--pc-clm-spacing-sm);
margin-bottom: var(--pc-clm-spacing-xs);
}
.pc-clm-widget-version {
font-family: 'Menlo', 'Monaco', 'Consolas', monospace;
font-size: 12px;
font-weight: 600;
color: #000000;
}
.pc-clm-widget-date {
font-size: 12px;
color: #000000;
}
.pc-clm-widget-entry-title {
font-size: var(--pc-clm-font-size-sm);
font-weight: 600;
color: #000000;
text-decoration: none;
transition: color var(--pc-clm-transition-fast);
display: block;
line-height: 1.4;
}
.pc-clm-widget-entry-title:hover {
color: #000000;
}
.pc-clm-widget-category {
display: inline-block;
margin-top: var(--pc-clm-spacing-xs);
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.3px;
padding: 2px 6px;
border-radius: 3px;
background-color: #000000;
color: #ffffff;
}
/* ========================================
Pagination
======================================== */
.pc-clm-archive-container .pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
margin-top: 40px;
padding-top: 24px;
border-top: 2px solid #000000;
}
.pc-clm-archive-container .page-numbers {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 36px;
height: 36px;
padding: 0 8px;
font-size: 14px;
font-weight: 600;
color: #000000;
background-color: #ffffff;
border: 2px solid #000000;
border-radius: 4px;
text-decoration: none;
transition: all 0.15s ease;
}
.pc-clm-archive-container .page-numbers:hover {
background-color: #000000;
border-color: #000000;
color: #ffffff;
}
.pc-clm-archive-container .page-numbers.current {
background-color: #000000;
border-color: #000000;
color: #ffffff;
}
.pc-clm-archive-container .page-numbers.dots {
border: none;
background: none;
color: #000000;
}
.pc-clm-archive-container .prev,
.pc-clm-archive-container .next {
min-width: auto;
padding: 0 16px;
}
/* ========================================
Dark Mode Support
======================================== */
@media (prefers-color-scheme: dark) {
.pc-clm-archive-container {
color: #000000;
background-color: #ffffff;
}
.pc-clm-archive-header {
border-bottom-color: #000000;
}
.pc-clm-archive-title {
color: #000000;
}
.pc-clm-archive-header .archive-description {
color: #000000;
}
.pc-clm-entry {
background-color: #ffffff;
border-color: #000000;
}
.pc-clm-entry:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.pc-clm-entry-title a {
color: #000000;
}
.pc-clm-entry-title a:hover {
color: #000000;
}
.pc-clm-entry-content {
color: #000000;
}
.pc-clm-read-more {
color: #000000;
}
.pc-clm-read-more:hover {
color: #000000;
}
.pc-clm-no-entries,
.pc-clm-widget-empty {
background-color: #ffffff;
border-color: #000000;
color: #000000;
}
.pc-clm-archive-container .page-numbers {
background-color: #ffffff;
border-color: #000000;
color: #000000;
}
.pc-clm-archive-container .page-numbers:hover {
background-color: #000000;
color: #ffffff;
}
.pc-clm-archive-container .pagination {
border-top-color: #000000;
}
.pc-clm-version-badge {
background-color: #000000;
color: #ffffff;
}
.pc-clm-release-date,
.pc-clm-date {
color: #000000;
}
.pc-clm-widget-entry {
border-bottom-color: #000000;
}
.pc-clm-widget-entry-title {
color: #000000;
}
.pc-clm-widget-entry-title:hover {
color: #000000;
}
.pc-clm-category-new-features {
background-color: #000000;
color: #ffffff;
}
.pc-clm-category-bug-fixes {
background-color: #333333;
color: #ffffff;
}
.pc-clm-category-improvements {
background-color: #666666;
color: #ffffff;
}
.pc-clm-category-security {
background-color: #dba617;
color: #000000;
}
.pc-clm-category-deprecated {
background-color: #888888;
color: #ffffff;
}
.pc-clm-category-removed {
background-color: #aaaaaa;
color: #ffffff;
}
/* Upvote section dark mode */
.pc-clm-upvote-btn {
background-color: #ffffff;
border-color: #000000;
color: #000000;
}
.pc-clm-upvote-btn:not(:disabled):hover {
background-color: #000000;
border-color: #000000;
color: #ffffff;
}
}
.pc-clm-upvote-btn.voted {
background-color: #000000;
border-color: #000000;
color: #ffffff;
}
.pc-clm-upvote-label {
color: #000000;
}
.pc-clm-upvote-btn.voted + .pc-clm-upvote-label {
color: #000000;
}
/* Notifications dark mode */
.pc-clm-notification {
background-color: #ffffff;
border: 2px solid #000000;
color: #000000;
}
}
/* ========================================
Responsive Design
======================================== */
@media screen and (max-width: 768px) {
.pc-clm-archive-container {
padding: 0 12px;
}
.pc-clm-archive-header {
margin-bottom: 20px;
padding: 20px 0;
}
.pc-clm-archive-title {
font-size: 22px;
}
.pc-clm-entry {
padding: 16px;
}
.pc-clm-entry-title {
font-size: 17px;
}
.pc-clm-entry-meta {
gap: 6px;
}
.pc-clm-version-badge {
padding: 3px 8px;
font-size: 12px;
}
.pc-clm-category-badge {
padding: 3px 8px;
font-size: 10px;
}
.pc-clm-entry-header {
flex-direction: column;
align-items: stretch;
gap: var(--pc-clm-spacing-sm);
}
.pc-clm-upvote-section {
align-self: flex-end;
}
}
@media screen and (max-width: 480px) {
.pc-clm-archive-container {
padding: 0 8px;
}
.pc-clm-archive-title {
font-size: 20px;
}
.pc-clm-entry {
padding: 14px;
}
.pc-clm-entry-title {
font-size: 16px;
}
.pc-clm-entry-meta {
flex-direction: column;
align-items: flex-start;
gap: 6px;
}
.pc-clm-version-badge,
.pc-clm-release-date {
display: inline-block;
}
.pc-clm-upvote-section {
align-self: flex-end;
margin-top: var(--pc-clm-spacing-sm);
}
.pc-clm-upvote-btn {
min-width: 70px;
padding: 8px 12px;
}
}
/* ========================================
Accessibility
======================================== */
@media (prefers-reduced-motion: reduce) {
.pc-clm-entry {
transition: none;
}
.pc-clm-entry:hover {
transform: none;
}
.pc-clm-read-more .dashicons {
transition: none;
}
.pc-clm-read-more:hover .dashicons {
transform: none;
}
}
@media (prefers-contrast: high) {
.pc-clm-entry {
border-width: 2px;
}
.pc-clm-version-badge {
border: 1px solid currentColor;
}
.pc-clm-category-badge {
border: 1px solid currentColor;
}
}
/* ========================================
Notifications
======================================== */
.pc-clm-notification {
position: fixed;
top: 20px;
right: 20px;
padding: 12px 16px;
background-color: #fff;
border-radius: var(--pc-clm-radius-md);
border-left: 4px solid;
box-shadow: var(--pc-clm-shadow-lg);
font-size: 14px;
font-weight: 500;
z-index: 10000;
transform: translateX(100%);
opacity: 0;
transition: all var(--pc-clm-transition-base);
max-width: 300px;
}
.pc-clm-notification.show {
transform: translateX(0);
opacity: 1;
}
.pc-clm-notification-success {
border-left-color: var(--pc-clm-success);
color: var(--pc-clm-success);
}
.pc-clm-notification-error {
border-left-color: var(--pc-clm-error);
color: var(--pc-clm-error);
}
.pc-clm-notification-info {
border-left-color: var(--pc-clm-info);
color: var(--pc-clm-info);
}
/* Focus visible for keyboard navigation */
.pc-clm-entry-title a:focus-visible,
.pc-clm-read-more:focus-visible,
.pc-clm-upvote-btn:focus-visible {
outline: 2px solid var(--pc-clm-primary);
outline-offset: 2px;
}
/* ========================================
Print Styles
======================================== */
@media print {
.pc-clm-archive-container {
max-width: 100%;
padding: 0;
}
.pc-clm-entry {
box-shadow: none;
border: 1px solid #ccc;
page-break-inside: avoid;
}
.pc-clm-entry:hover {
transform: none;
box-shadow: none;
}
.pc-clm-read-more {
display: none;
}
.pc-clm-category-badge {
border: 1px solid #000;
}
}

View File

@@ -0,0 +1,115 @@
/**
* PC Changelog Manager - Public JavaScript
*
* @package PCChangelogManager
*/
(function($) {
'use strict';
$(document).ready(function() {
// Handle upvote button clicks
$(document).on('click', '.pc-clm-upvote-btn', function(e) {
e.preventDefault();
var $button = $(this);
var postId = $button.data('post-id');
var $count = $button.find('.pc-clm-upvote-count');
var $label = $button.siblings('.pc-clm-upvote-label');
var originalCount = parseInt($count.text());
// Prevent multiple clicks
if ($button.prop('disabled')) {
return;
}
$button.prop('disabled', true);
// Show loading state
$button.addClass('loading');
// Make AJAX request
$.ajax({
url: pc_clm_ajax.ajax_url,
type: 'POST',
data: {
action: 'pc_clm_upvote',
post_id: postId,
nonce: pc_clm_ajax.nonce
},
success: function(response) {
if (response.success) {
// Update the count
$count.text(response.data.vote_count);
// Update label
var labelText = response.data.vote_count === 1 ? 'upvote' : 'upvotes';
$label.text(labelText);
// Mark as voted
$button.addClass('voted');
$button.prop('disabled', true);
// Show success message (optional)
showNotification(response.data.message, 'success');
} else {
// Show error message
showNotification(response.data.message, 'error');
// Re-enable button if not already voted
if (!response.data.voted) {
$button.prop('disabled', false);
}
}
},
error: function(xhr, status, error) {
console.error('Upvote error:', error);
showNotification('An error occurred. Please try again.', 'error');
$button.prop('disabled', false);
},
complete: function() {
$button.removeClass('loading');
}
});
});
// Helper function to show notifications
function showNotification(message, type) {
var $notification = $('<div class="pc-clm-notification pc-clm-notification-' + type + '">' + message + '</div>');
// Add to page
$('body').append($notification);
// Show with animation
setTimeout(function() {
$notification.addClass('show');
}, 100);
// Auto hide after 3 seconds
setTimeout(function() {
$notification.removeClass('show');
setTimeout(function() {
$notification.remove();
}, 300);
}, 3000);
}
// Handle keyboard accessibility for upvote buttons
$(document).on('keydown', '.pc-clm-upvote-btn', function(e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
$(this).click();
}
});
// Add hover effects for upvote buttons
$('.pc-clm-upvote-btn:not(.voted)').hover(
function() {
$(this).addClass('hover');
},
function() {
$(this).removeClass('hover');
}
);
});
})(jQuery);

View File

@@ -0,0 +1,133 @@
<?php
/**
* Changelog Archive Template
*
* @package PCChangelogManager
*/
// Prevent direct access to file.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// Let theme handle header/footer
get_header();
?>
<div class="site-content" role="main">
<div class="pc-clm-archive-container">
<header class="pc-clm-archive-header">
<h1 class="pc-clm-archive-title">
<?php
$archive_title = post_type_archive_title( '', false );
if ( $archive_title ) {
echo esc_html( $archive_title );
} else {
esc_html_e( 'Changelog', 'pc-changelog-manager-abc123' );
}
?>
</h1>
<?php if ( $description = get_the_archive_description() ) : ?>
<div class="pc-clm-archive-description"><?php echo $description; ?></div>
<?php endif; ?>
</header>
<div class="pc-clm-archive-content">
<?php
$post_type = new PC_CLM_Post_Type();
if ( have_posts() ) :
while ( have_posts() ) :
the_post();
$post_id = get_the_ID();
$version = $post_type->get_meta( $post_id, 'version_number' );
$release_date = $post_type->get_meta( $post_id, 'release_date' );
$category = $post_type->get_meta( $post_id, 'category' );
$categories = $post_type->get_categories();
$category_name = isset( $categories[ $category ] ) ? $categories[ $category ] : $category;
$upvote_count = (int) get_post_meta( $post_id, '_pc_clm_upvotes', true );
$has_voted = false;
$ip_address = $_SERVER['REMOTE_ADDR'];
$user_id = get_current_user_id();
$voted_key = $user_id ? "pc_clm_voted_user_{$user_id}" : "pc_clm_voted_ip_{$ip_address}";
$voted_posts = get_transient( $voted_key );
if ( $voted_posts && in_array( $post_id, $voted_posts ) ) {
$has_voted = true;
}
$entry_classes = array( 'pc-clm-entry' );
if ( $category ) {
$entry_classes[] = 'pc-clm-category-' . $category;
}
?>
<article id="post-<?php the_ID(); ?>" <?php post_class( $entry_classes ); ?>>
<div class="pc-clm-entry-wrapper">
<div class="pc-clm-entry-header">
<div class="pc-clm-entry-meta">
<?php if ( $version ) : ?>
<span class="pc-clm-version-badge"><?php echo esc_html( $version ); ?></span>
<?php endif; ?>
<?php if ( $release_date ) : ?>
<span class="pc-clm-release-date"><?php echo esc_html( date_i18n( get_option( 'date_format' ), strtotime( $release_date ) ) ); ?></span>
<?php endif; ?>
<?php if ( $category ) : ?>
<span class="pc-clm-category-badge pc-clm-category-<?php echo esc_attr( $category ); ?>"><?php echo esc_html( $category_name ); ?></span>
<?php endif; ?>
</div>
<div class="pc-clm-upvote-section">
<button class="pc-clm-upvote-btn <?php echo $has_voted ? 'voted' : ''; ?>"
data-post-id="<?php echo esc_attr( $post_id ); ?>"
<?php echo $has_voted ? 'disabled' : ''; ?>>
<span class="pc-clm-upvote-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2l2.4 7.4h7.6l-6 4.6 2.3 7.4-6.3-4.6-6.3 4.6 2.3-7.4-6-4.6h7.6z"/>
</svg>
</span>
<span class="pc-clm-upvote-count"><?php echo esc_html( $upvote_count ); ?></span>
</button>
<span class="pc-clm-upvote-label"><?php echo esc_html( _n( 'upvote', 'upvotes', $upvote_count, 'pc-changelog-manager-abc123' ) ); ?></span>
</div>
</div>
<div class="pc-clm-entry-content-wrapper">
<h3 class="pc-clm-entry-title">
<a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
</h3>
<div class="pc-clm-entry-content">
<?php the_excerpt(); ?>
</div>
</div>
</div>
</article>
<?php
endwhile;
the_posts_pagination(
array(
'prev_text' => __( 'Previous', 'pc-changelog-manager-abc123' ),
'next_text' => __( 'Next', 'pc-changelog-manager-abc123' ),
'before_page_number' => '<span class="meta-nav screen-reader-text">' . __( 'Page', 'pc-changelog-manager-abc123' ) . ' </span>',
)
);
else :
?>
<div class="pc-clm-no-entries">
<p><?php esc_html_e( 'No changelog entries found.', 'pc-changelog-manager-abc123' ); ?></p>
</div>
<?php
endif;
?>
</div>
</div>
</div>
<?php get_footer(); ?>

View File

@@ -0,0 +1,99 @@
<?php
/**
* Single Changelog Template
*
* @package PCChangelogManager
*/
// Prevent direct access to file.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// Let theme handle header/footer
get_header();
?>
<div class="site-content" role="main">
<?php
while ( have_posts() ) :
the_post();
$post_id = get_the_ID();
$post_type = new PC_CLM_Post_Type();
$version = $post_type->get_meta( $post_id, 'version_number' );
$release_date = $post_type->get_meta( $post_id, 'release_date' );
$category = $post_type->get_meta( $post_id, 'category' );
$categories = $post_type->get_categories();
$category_name = isset( $categories[ $category ] ) ? $categories[ $category ] : $category;
$upvote_count = (int) get_post_meta( $post_id, '_pc_clm_upvotes', true );
$has_voted = false;
$ip_address = $_SERVER['REMOTE_ADDR'];
$user_id = get_current_user_id();
$voted_key = $user_id ? "pc_clm_voted_user_{$user_id}" : "pc_clm_voted_ip_{$ip_address}";
$voted_posts = get_transient( $voted_key );
if ( $voted_posts && in_array( $post_id, $voted_posts ) ) {
$has_voted = true;
}
$entry_classes = array( 'pc-clm-entry', 'pc-clm-single-entry' );
if ( $category ) {
$entry_classes[] = 'pc-clm-category-' . $category;
}
?>
<article id="post-<?php the_ID(); ?>" <?php post_class( $entry_classes ); ?>>
<div class="pc-clm-entry-wrapper">
<div class="pc-clm-entry-header">
<div class="pc-clm-entry-meta">
<?php if ( $version ) : ?>
<span class="pc-clm-version-badge"><?php echo esc_html( $version ); ?></span>
<?php endif; ?>
<?php if ( $release_date ) : ?>
<span class="pc-clm-release-date"><?php echo esc_html( date_i18n( get_option( 'date_format' ), strtotime( $release_date ) ) ); ?></span>
<?php endif; ?>
<?php if ( $category ) : ?>
<span class="pc-clm-category-badge pc-clm-category-<?php echo esc_attr( $category ); ?>"><?php echo esc_html( $category_name ); ?></span>
<?php endif; ?>
</div>
<div class="pc-clm-upvote-section">
<button class="pc-clm-upvote-btn <?php echo $has_voted ? 'voted' : ''; ?>"
data-post-id="<?php echo esc_attr( $post_id ); ?>"
<?php echo $has_voted ? 'disabled' : ''; ?>>
<span class="pc-clm-upvote-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2l2.4 7.4h7.6l-6 4.6 2.3 7.4-6.3-4.6-6.3 4.6 2.3-7.4-6-4.6h7.6z"/>
</svg>
</span>
<span class="pc-clm-upvote-count"><?php echo esc_html( $upvote_count ); ?></span>
</button>
<span class="pc-clm-upvote-label"><?php echo esc_html( _n( 'upvote', 'upvotes', $upvote_count, 'pc-changelog-manager-abc123' ) ); ?></span>
</div>
</div>
<div class="pc-clm-entry-content-wrapper">
<h1 class="pc-clm-entry-title">
<?php the_title(); ?>
</h1>
<div class="pc-clm-entry-content">
<?php the_content(); ?>
</div>
</div>
</div>
</article>
<?php
if ( comments_open() || get_comments_number() ) {
comments_template();
}
endwhile;
?>
</div>
<?php get_footer(); ?>

View File

@@ -0,0 +1,180 @@
<?php
/**
* Changelog Widget
*
* @package PCChangelogManager
*/
// Prevent direct access to the file.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Changelog Widget class.
*/
class PC_CLM_Widget extends WP_Widget {
/**
* Constructor.
*/
public function __construct() {
parent::__construct(
'pc_clm_changelog_widget',
__( 'Changelog', 'pc-changelog-manager-abc123' ),
array(
'description' => __( 'Display recent changelog entries.', 'pc-changelog-manager-abc123' ),
'customize_selective_refresh' => true,
)
);
}
/**
* Output the widget content.
*
* @param array $args Display arguments.
* @param array $instance Widget instance.
*/
public function widget( $args, $instance ) {
// Enqueue styles.
if ( file_exists( PC_CLM_PLUGIN_DIR . 'public/css/public-style.css' ) ) {
wp_enqueue_style( 'pc-clm-public-style', PC_CLM_PLUGIN_URL . 'public/css/public-style.css', array(), PC_CLM_VERSION );
}
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Changelog', 'pc-changelog-manager-abc123' );
$limit = ! empty( $instance['limit'] ) ? intval( $instance['limit'] ) : 5;
$show_date = ! empty( $instance['show_date'] ) ? $instance['show_date'] : 'yes';
$show_category = ! empty( $instance['show_category'] ) ? $instance['show_category'] : 'yes';
echo wp_kses_post( $args['before_widget'] );
if ( $title ) {
echo wp_kses_post( $args['before_title'] . apply_filters( 'widget_title', $title ) . $args['after_title'] );
}
// Query changelog entries.
$query_args = array(
'post_type' => 'pc_changelog',
'post_status' => 'publish',
'posts_per_page' => $limit,
'orderby' => 'date',
'order' => 'DESC',
);
$query = new WP_Query( $query_args );
if ( $query->have_posts() ) {
?>
<ul class="pc-clm-widget-list">
<?php
while ( $query->have_posts() ) :
$query->the_post();
$post_id = get_the_ID();
$version = get_post_meta( $post_id, '_pc_clm_version_number', true );
$release_date = get_post_meta( $post_id, '_pc_clm_release_date', true );
$category = get_post_meta( $post_id, '_pc_clm_category', true );
$categories = array(
'new-features' => __( 'New Features', 'pc-changelog-manager-abc123' ),
'bug-fixes' => __( 'Bug Fixes', 'pc-changelog-manager-abc123' ),
'improvements' => __( 'Improvements', 'pc-changelog-manager-abc123' ),
'security' => __( 'Security', 'pc-changelog-manager-abc123' ),
'deprecated' => __( 'Deprecated', 'pc-changelog-manager-abc123' ),
'removed' => __( 'Removed', 'pc-changelog-manager-abc123' ),
);
?>
<li class="pc-clm-widget-entry">
<div class="pc-clm-widget-entry-header">
<?php if ( $version ) : ?>
<span class="pc-clm-widget-version"><?php echo esc_html( $version ); ?></span>
<?php endif; ?>
<?php if ( 'yes' === $show_date && $release_date ) : ?>
<span class="pc-clm-widget-date"><?php echo esc_html( date_i18n( 'M j, Y', strtotime( $release_date ) ) ); ?></span>
<?php endif; ?>
</div>
<a href="<?php the_permalink(); ?>" class="pc-clm-widget-entry-title">
<?php the_title(); ?>
</a>
<?php if ( 'yes' === $show_category && $category && isset( $categories[ $category ] ) ) : ?>
<span class="pc-clm-widget-category pc-clm-category-<?php echo esc_attr( $category ); ?>">
<?php echo esc_html( $categories[ $category ] ); ?>
</span>
<?php endif; ?>
</li>
<?php
endwhile;
?>
</ul>
<?php
wp_reset_postdata();
} else {
?>
<p class="pc-clm-widget-empty"><?php esc_html_e( 'No changelog entries found.', 'pc-changelog-manager-abc123' ); ?></p>
<?php
}
echo wp_kses_post( $args['after_widget'] );
}
/**
* Output the widget form in the admin.
*
* @param array $instance Current instance.
*/
public function form( $instance ) {
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Changelog', 'pc-changelog-manager-abc123' );
$limit = ! empty( $instance['limit'] ) ? intval( $instance['limit'] ) : 5;
$show_date = ! empty( $instance['show_date'] ) ? $instance['show_date'] : 'yes';
$show_category = ! empty( $instance['show_category'] ) ? $instance['show_category'] : 'yes';
?>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">
<?php esc_html_e( 'Title:', 'pc-changelog-manager-abc123' ); ?>
</label>
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>" />
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'limit' ) ); ?>">
<?php esc_html_e( 'Number of entries to show:', 'pc-changelog-manager-abc123' ); ?>
</label>
<input class="tiny-text" id="<?php echo esc_attr( $this->get_field_id( 'limit' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'limit' ) ); ?>" type="number" min="1" max="20" value="<?php echo esc_attr( $limit ); ?>" />
</p>
<p>
<input class="checkbox" id="<?php echo esc_attr( $this->get_field_id( 'show_date' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'show_date' ) ); ?>" type="checkbox" value="yes" <?php checked( $show_date, 'yes' ); ?> />
<label for="<?php echo esc_attr( $this->get_field_id( 'show_date' ) ); ?>">
<?php esc_html_e( 'Show release date', 'pc-changelog-manager-abc123' ); ?>
</label>
</p>
<p>
<input class="checkbox" id="<?php echo esc_attr( $this->get_field_id( 'show_category' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'show_category' ) ); ?>" type="checkbox" value="yes" <?php checked( $show_category, 'yes' ); ?> />
<label for="<?php echo esc_attr( $this->get_field_id( 'show_category' ) ); ?>">
<?php esc_html_e( 'Show category', 'pc-changelog-manager-abc123' ); ?>
</label>
</p>
<?php
}
/**
* Save widget settings.
*
* @param array $new_instance New instance.
* @param array $old_instance Old instance.
* @return array
*/
public function update( $new_instance, $old_instance ) {
$instance = array();
$instance['title'] = ! empty( $new_instance['title'] ) ? sanitize_text_field( $new_instance['title'] ) : '';
$instance['limit'] = ! empty( $new_instance['limit'] ) ? intval( $new_instance['limit'] ) : 5;
$instance['show_date'] = ! empty( $new_instance['show_date'] ) ? 'yes' : 'no';
$instance['show_category'] = ! empty( $new_instance['show_category'] ) ? 'yes' : 'no';
return $instance;
}
}

View File

@@ -0,0 +1,115 @@
=== PC Changelog Manager ===
Contributors: plugincompass
Tags: changelog, version history, release notes, software updates
Requires at least: 5.8
Tested up to: 6.4
Requires PHP: 7.4
Stable tag: 1.0.0
License: GPL v2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Update URI: false
Create and manage changelogs directly from the WordPress admin panel. Automatically generates a /changelog page.
== Description ==
PC Changelog Manager is a powerful and easy-to-use WordPress plugin that allows you to create and manage changelogs directly from your WordPress admin panel. Perfect for software projects, plugins, themes, or any project that needs to track version history.
### Features
* **Easy Entry Management** - Create changelog entries with version number, release date, description, and category
* **Automatic Page Generation** - Automatically creates a `/changelog` page on your site
* **Categorized Entries** - Organize changelogs into categories: New Features, Bug Fixes, Improvements, Security, Deprecated, and Removed
* **Administrator Only** - Only administrators can create, edit, or delete changelog entries
* **Shortcode Support** - Display changelog entries anywhere using the `[changelog]` shortcode
* **Widget Support** - Add a changelog widget to your sidebar
* **Responsive Design** - Works perfectly on all devices
* **Dark Mode Support** - Automatically adapts to your site's color scheme
* **Translation Ready** - Fully transllatable with language files
* **Clean Uninstall** - Removes all plugin data when uninstalled
### Usage
1. **Add New Changelog Entry**
* Navigate to Changelogs > Add New in your WordPress admin
* Enter the version number (e.g., 1.0.0)
* Select the release date
* Choose a category
* Write your changelog content in the editor
* Publish the entry
2. **View the Changelog Page**
* Visit `/changelog/` on your site to see all entries
* Entries are displayed in reverse chronological order
3. **Use the Shortcode**
* Add `[changelog]` to any page or post
* Use `[changelog limit="5"]` to show only 5 entries
* Use `[changelog category="bug-fixes"]` to filter by category
4. **Add the Widget**
* Go to Appearance > Widgets
* Add the "Changelog" widget to your sidebar
== Installation ==
1. Upload the plugin files to the `/wp-content/plugins/pc-changelog-manager-abc123` directory, or install the plugin through the WordPress plugins screen directly.
2. Activate the plugin through the 'Plugins' screen in WordPress.
3. Navigate to Changelogs > Add New to create your first changelog entry.
4. Visit the `/changelog/` page to see your entries.
== Frequently Asked Questions ==
= Can I edit the changelog page content? =
Yes, the `/changelog` page is a regular WordPress page. You can edit it by navigating to Pages > Changelog in your admin panel.
= Can I change the changelog URL? =
The changelog uses the WordPress page system, so you can change the page slug by editing the page.
= Can I display changelog entries on the homepage? =
Yes, use the `[changelog]` shortcode in any page, post, or widget area.
= Will my changelog entries be deleted if I uninstall the plugin? =
Yes, the plugin includes a clean uninstall process that removes all changelog entries and associated data when the plugin is deleted.
= Can I import existing changelogs? =
Currently, manual entry is required. You can use the WordPress import/export functionality to move entries between sites.
== Screenshots ==
1. Changelog entries list in admin panel
2. Add new changelog entry screen
3. Public changelog page display
4. Shortcode and widget display options
== Changelog ==
= 1.0.0 =
* Initial release
* Custom post type for changelog entries
* Automatic changelog page creation
* Shortcode support
* Widget support
* Admin UI with meta boxes
* Category support (New Features, Bug Fixes, Improvements, Security, Deprecated, Removed)
* Responsive design
* Dark mode support
* Clean uninstall functionality
== Upgrade Notice ==
= 1.0.0 =
Initial release of PC Changelog Manager.
== Privacy ==
This plugin doesn't collect or store any personal data.
== Support ==
For support, please visit the Plugin Compass support channels.

View File

@@ -0,0 +1,232 @@
#!/bin/bash
#
# WordPress Plugin Validation Script
# Validates PHP syntax and checks plugin header for WordPress.org compliance
#
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Plugin root directory
PLUGIN_DIR="${1:-.}"
# Counters
TOTAL_FILES=0
ERROR_COUNT=0
WARNING_COUNT=0
echo "================================================"
echo "WordPress Plugin Validation Script"
echo "================================================"
echo ""
echo "Validating plugin: $PLUGIN_DIR"
echo ""
# Function to check PHP syntax
check_php_syntax() {
local file="$1"
local result=$(php -l "$file" 2>&1)
if echo "$result" | grep -q "No syntax errors detected"; then
echo -e "${GREEN}[PASS]${NC} $file"
return 0
else
echo -e "${RED}[FAIL]${NC} $file"
echo "$result"
return 1
fi
}
# Function to check plugin header
check_plugin_header() {
local file="$1"
local dir=$(dirname "$file")
local basename=$(basename "$dir")
# Check for required plugin header fields
if ! grep -q "Plugin Name:" "$file"; then
echo -e "${RED}[MISSING]${NC} Plugin Name header in $file"
return 1
fi
if ! grep -q "Version:" "$file"; then
echo -e "${RED}[MISSING]${NC} Version header in $file"
return 1
fi
if ! grep -q "Text Domain:" "$file"; then
echo -e "${YELLOW}[WARNING]${NC} Text Domain header missing in $file"
return 2
fi
echo -e "${GREEN}[OK]${NC} Plugin header in $file"
return 0
}
# Function to validate file structure
validate_structure() {
local plugin_file="$1"
local plugin_dir=$(dirname "$plugin_file")
local plugin_slug=$(basename "$plugin_file" .php)
echo ""
echo "Checking plugin structure..."
echo ""
# Check for required files
local required_files=(
"pc-changelog-manager-abc123.php"
"uninstall.php"
)
local required_dirs=(
"includes"
"admin"
"public"
)
local missing=0
for file in "${required_files[@]}"; do
if [ ! -f "$plugin_dir/$file" ]; then
echo -e "${RED}[MISSING]${NC} Required file: $file"
missing=1
else
echo -e "${GREEN}[FOUND]${NC} $file"
fi
TOTAL_FILES=$((TOTAL_FILES + 1))
done
for dir in "${required_dirs[@]}"; do
if [ ! -d "$plugin_dir/$dir" ]; then
echo -e "${RED}[MISSING]${NC} Required directory: $dir"
missing=1
else
echo -e "${GREEN}[FOUND]${NC} $dir/"
fi
done
if [ $missing -eq 1 ]; then
return 1
fi
return 0
}
# Function to find all PHP files
find_php_files() {
local dir="$1"
find "$dir" -name "*.php" -type f
}
# Main execution
main() {
# Check if plugin directory exists
if [ ! -d "$PLUGIN_DIR" ]; then
echo -e "${RED}[ERROR]${NC} Directory not found: $PLUGIN_DIR"
exit 1
fi
# Find the main plugin file
local main_file=""
local plugin_slug=""
# Look for PHP files with plugin header
for file in $(find_php_files "$PLUGIN_DIR"); do
if grep -q "Plugin Name:" "$file" && grep -q "Version:" "$file"; then
main_file="$file"
plugin_slug=$(basename "$file" .php)
break
fi
done
if [ -z "$main_file" ]; then
echo -e "${RED}[ERROR]${NC} No plugin main file found in $PLUGIN_DIR"
exit 1
fi
echo "Main plugin file: $main_file"
echo "Plugin slug: $plugin_slug"
echo ""
# Validate structure
validate_structure "$main_file"
struct_result=$?
if [ $struct_result -ne 0 ]; then
ERROR_COUNT=$((ERROR_COUNT + 1))
fi
# Check main plugin header
check_plugin_header "$main_file"
header_result=$?
if [ $header_result -eq 1 ]; then
ERROR_COUNT=$((ERROR_COUNT + 1))
elif [ $header_result -eq 2 ]; then
WARNING_COUNT=$((WARNING_COUNT + 1))
fi
# Find and check all PHP files
echo ""
echo "Validating PHP syntax..."
echo ""
while IFS= read -r file; do
TOTAL_FILES=$((TOTAL_FILES + 1))
check_php_syntax "$file"
result=$?
if [ $result -ne 0 ]; then
ERROR_COUNT=$((ERROR_COUNT + 1))
fi
done < <(find_php_files "$PLUGIN_DIR")
# Check for common issues
echo ""
echo "Checking for common issues..."
echo ""
# Check for debug statements
debug_patterns=("var_dump" "print_r" "console.log" "error_log")
for pattern in "${debug_patterns[@]}"; do
if grep -rq "$pattern" "$PLUGIN_DIR" --include="*.php" | grep -v "uninstall.php" | grep -v ".git"; then
echo -e "${YELLOW}[WARNING]${NC} Potential debug statement found: $pattern"
WARNING_COUNT=$((WARNING_COUNT + 1))
fi
done
# Check for proper escaping
if grep -rq "echo \$" "$PLUGIN_DIR" --include="*.php" | grep -v "esc_html\|esc_attr\|esc_url\|esc_textarea"; then
echo -e "${YELLOW}[WARNING]${NC} Potential missing escaping found"
WARNING_COUNT=$((WARNING_COUNT + 1))
fi
# Summary
echo ""
echo "================================================"
echo "Validation Summary"
echo "================================================"
echo ""
echo "Total files checked: $TOTAL_FILES"
echo -e "Errors: ${RED}$ERROR_COUNT${NC}"
echo -e "Warnings: ${YELLOW}$WARNING_COUNT${NC}"
echo ""
if [ $ERROR_COUNT -gt 0 ]; then
echo -e "${RED}Validation FAILED${NC}"
echo "Please fix the errors above."
exit 1
elif [ $WARNING_COUNT -gt 0 ]; then
echo -e "${YELLOW}Validation PASSED with warnings${NC}"
echo "Review the warnings above."
exit 0
else
echo -e "${GREEN}Validation PASSED${NC}"
echo "Plugin is ready for use."
exit 0
fi
}
# Run main function
main

View File

@@ -0,0 +1,91 @@
<?php
/**
* PC Changelog Manager - Uninstall Cleanup
*
* Cleans up all plugin data when plugin is deleted.
*
* @package PCChangelogManager
*/
// Prevent direct access to file.
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit;
}
// Check if this is the correct plugin being uninstalled.
if ( false === strpos( WP_UNINSTALL_PLUGIN, 'pc-changelog-manager-abc123' ) ) {
exit;
}
// Use direct SQL queries for reliable cleanup during uninstall.
global $wpdb;
// Add error logging for debugging.
function pc_clm_log_error( $message ) {
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( 'PC Changelog Uninstall Error: ' . $message );
}
}
// Delete post meta for pc_changelog posts.
try {
$wpdb->query(
"DELETE FROM {$wpdb->postmeta} WHERE post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_type = 'pc_changelog')"
);
} catch ( Exception $e ) {
pc_clm_log_error( 'Failed to delete post meta: ' . $e->getMessage() );
// Continue with cleanup despite error
}
// Delete pc_changelog posts.
try {
$wpdb->query(
"DELETE FROM {$wpdb->posts} WHERE post_type = 'pc_changelog'"
);
} catch ( Exception $e ) {
pc_clm_log_error( 'Failed to delete posts: ' . $e->getMessage() );
// Continue with cleanup despite error
}
// Delete upvote transients (use wildcard for better cleanup).
try {
$wpdb->query(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE 'transient_timeout_pc_clm_%' OR option_name LIKE '_transient_timeout_pc_clm_%'"
);
} catch ( Exception $e ) {
pc_clm_log_error( 'Failed to delete transients: ' . $e->getMessage() );
// Continue with cleanup despite error
}
// Delete vote tracking transients.
try {
$wpdb->query(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE 'pc_clm_voted_%'"
);
} catch ( Exception $e ) {
pc_clm_log_error( 'Failed to delete vote tracking: ' . $e->getMessage() );
// Continue with cleanup despite error
}
// Delete all transients with our prefix.
try {
$wpdb->query(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE 'pc_clm_%'"
);
} catch ( Exception $e ) {
pc_clm_log_error( 'Failed to delete all transients: ' . $e->getMessage() );
// Continue with cleanup despite error
}
// Delete our plugin options.
try {
delete_option( 'pc_clm_rewrite_rules_flushed' );
} catch ( Exception $e ) {
pc_clm_log_error( 'Failed to delete plugin options: ' . $e->getMessage() );
// Continue with cleanup despite error
}
// Clear any remaining plugin cache.
if ( function_exists( 'wp_cache_flush' ) ) {
wp_cache_flush();
}

View File

@@ -0,0 +1,255 @@
<?php
class PC_Community_Suggestions_Admin {
public function __construct() {
add_action('admin_menu', array($this, 'add_admin_menu'));
add_action('admin_init', array($this, 'register_settings'));
add_action('admin_post_pc_delete_suggestion', array($this, 'handle_suggestion_deletion'));
}
public function add_admin_menu() {
add_menu_page(
__('Community Suggestions', 'pc-community-suggestions-7d3f'),
__('Suggestions', 'pc-community-suggestions-7d3f'),
'manage_options',
'pc_community_suggestions',
array($this, 'render_admin_page'),
'dashicons-lightbulb',
30
);
add_submenu_page(
'pc_community_suggestions',
__('All Suggestions', 'pc-community-suggestions-7d3f'),
__('All Suggestions', 'pc-community-suggestions-7d3f'),
'manage_options',
'pc_community_suggestions',
array($this, 'render_admin_page')
);
add_submenu_page(
'pc_community_suggestions',
__('Settings', 'pc-community-suggestions-7d3f'),
__('Settings', 'pc-community-suggestions-7d3f'),
'manage_options',
'pc_community_suggestions_settings',
array($this, 'render_settings_page')
);
}
public function register_settings() {
register_setting('pc_community_suggestions_settings', 'pc_community_suggestions_default_sort');
add_settings_section(
'pc_community_suggestions_general',
__('General Settings', 'pc-community-suggestions-7d3f'),
array($this, 'render_settings_section'),
'pc_community_suggestions_settings'
);
add_settings_field(
'pc_community_suggestions_default_sort',
__('Default Sort Order', 'pc-community-suggestions-7d3f'),
array($this, 'render_sort_field'),
'pc_community_suggestions_settings',
'pc_community_suggestions_general'
);
}
public function render_settings_section() {
echo '<p>' . __('Configure how community suggestions work on your site.', 'pc-community-suggestions-7d3f') . '</p>';
}
public function render_sort_field() {
$default_sort = get_option('pc_community_suggestions_default_sort', 'popular');
?>
<select name="pc_community_suggestions_default_sort">
<option value="popular" <?php selected($default_sort, 'popular'); ?>><?php esc_html_e('Most Upvoted', 'pc-community-suggestions-7d3f'); ?></option>
<option value="newest" <?php selected($default_sort, 'newest'); ?>><?php esc_html_e('Newest', 'pc-community-suggestions-7d3f'); ?></option>
</select>
<p class="description">
<?php esc_html_e('Default sorting method for the suggestions page.', 'pc-community-suggestions-7d3f'); ?>
</p>
<?php
}
public function render_admin_page() {
$current_tab = $_GET['tab'] ?? 'all';
$sort = isset($_GET['sort']) && in_array($_GET['sort'], ['popular', 'newest']) ? $_GET['sort'] : 'newest';
?>
<div class="wrap">
<h1><?php esc_html_e('Community Suggestions', 'pc-community-suggestions-7d3f'); ?></h1>
<div class="pc-admin-controls">
<div class="pc-sort-dropdown">
<label for="pc-admin-sort"><?php esc_html_e('Sort by:', 'pc-community-suggestions-7d3f'); ?></label>
<select id="pc-admin-sort" onchange="window.location.href='?page=pc_community_suggestions&sort=' + this.value">
<option value="newest" <?php selected($sort, 'newest'); ?>><?php esc_html_e('Newest', 'pc-community-suggestions-7d3f'); ?></option>
<option value="popular" <?php selected($sort, 'popular'); ?>><?php esc_html_e('Most Upvoted', 'pc-community-suggestions-7d3f'); ?></option>
</select>
</div>
</div>
<div class="pc-admin-content">
<?php $this->render_suggestions_table($sort); ?>
</div>
</div>
<?php
}
public function render_settings_page() {
?>
<div class="wrap">
<h1><?php esc_html_e('Community Suggestions Settings', 'pc-community-suggestions-7d3f'); ?></h1>
<form method="post" action="options.php">
<?php
settings_fields('pc_community_suggestions_settings');
do_settings_sections('pc_community_suggestions_settings');
submit_button();
?>
</form>
<div class="pc-settings-stats">
<h2><?php esc_html_e('Statistics', 'pc-community-suggestions-7d3f'); ?></h2>
<div class="pc-stats-grid">
<div class="pc-stat-card">
<h3><?php echo esc_html(PC_Community_Suggestions_Database::get_suggestion_count()); ?></h3>
<p><?php esc_html_e('Total Suggestions', 'pc-community-suggestions-7d3f'); ?></p>
</div>
</div>
</div>
</div>
<?php
}
private function render_suggestions_table($sort = 'newest') {
global $wpdb;
$table_name = $wpdb->prefix . 'pc_community_suggestions';
$order_by = $sort === 'newest' ? 'created_at DESC' : 'upvotes DESC, created_at DESC';
$suggestions = $wpdb->get_results(
"SELECT s.*, u.user_login, u.display_name
FROM $table_name s
LEFT JOIN {$wpdb->users} u ON s.user_id = u.ID
ORDER BY $order_by"
);
?>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th><?php esc_html_e('Title', 'pc-community-suggestions-7d3f'); ?></th>
<th><?php esc_html_e('Author', 'pc-community-suggestions-7d3f'); ?></th>
<th><?php esc_html_e('Upvotes', 'pc-community-suggestions-7d3f'); ?></th>
<th><?php esc_html_e('Date', 'pc-community-suggestions-7d3f'); ?></th>
<th><?php esc_html_e('Actions', 'pc-community-suggestions-7d3f'); ?></th>
</tr>
</thead>
<tbody>
<?php if (empty($suggestions)) : ?>
<tr>
<td colspan="5"><?php esc_html_e('No suggestions found.', 'pc-community-suggestions-7d3f'); ?></td>
</tr>
<?php else : ?>
<?php foreach ($suggestions as $suggestion) : ?>
<tr>
<td>
<strong><?php echo esc_html($suggestion->title); ?></strong>
<div class="row-actions">
<span class="view">
<a href="#" onclick="pcPreviewSuggestion(<?php echo esc_attr($suggestion->id); ?>)">
<?php esc_html_e('View Details', 'pc-community-suggestions-7d3f'); ?>
</a> |
</span>
<span class="comment">
<a href="#" onclick="pcAddComment(<?php echo esc_attr($suggestion->id); ?>)">
<?php esc_html_e('Add Comment', 'pc-community-suggestions-7d3f'); ?>
</a>
</span>
</div>
</td>
<td><?php echo esc_html($suggestion->display_name ?: $suggestion->user_login); ?></td>
<td><?php echo esc_html($suggestion->upvotes); ?></td>
<td><?php echo date_i18n(get_option('date_format'), strtotime($suggestion->created_at)); ?></td>
<td>
<a href="<?php echo wp_nonce_url(admin_url('admin-post.php?action=pc_delete_suggestion&id=' . $suggestion->id), 'pc_delete_suggestion_' . $suggestion->id); ?>"
class="button button-small button-link-delete"
onclick="return confirm('<?php esc_attr_e('Are you sure you want to delete this suggestion?', 'pc-community-suggestions-7d3f'); ?>')">
<?php esc_html_e('Delete', 'pc-community-suggestions-7d3f'); ?>
</a>
</td>
</tr>
<tr id="pc-comments-<?php echo esc_attr($suggestion->id); ?>" class="pc-comments-row" style="display: none;">
<td colspan="5" class="pc-comments-cell">
<div class="pc-comments-container">
<h4><?php esc_html_e('Admin Comments', 'pc-community-suggestions-7d3f'); ?></h4>
<div class="pc-comments-list" id="pc-comments-list-<?php echo esc_attr($suggestion->id); ?>">
<?php
$comments = PC_Community_Suggestions_Database::get_comments($suggestion->id);
if (empty($comments)) {
echo '<p class="pc-no-comments">' . esc_html__('No comments yet.', 'pc-community-suggestions-7d3f') . '</p>';
} else {
foreach ($comments as $comment) {
echo '<div class="pc-comment-item" data-comment-id="' . esc_attr($comment->id) . '">';
echo '<div class="pc-comment-header">';
echo '<strong>' . esc_html($comment->display_name ?: $comment->user_login) . '</strong>';
echo '<span class="pc-comment-date">' . esc_html(date_i18n(get_option('date_format'), strtotime($comment->created_at))) . '</span>';
echo '</div>';
echo '<div class="pc-comment-content">' . wp_kses_post(wpautop($comment->comment)) . '</div>';
echo '<div class="pc-comment-actions">';
echo '<button class="button button-small button-link-delete" onclick="pcDeleteComment(' . esc_attr($comment->id) . ')">' . esc_html__('Delete', 'pc-community-suggestions-7d3f') . '</button>';
echo '</div>';
echo '</div>';
}
}
?>
</div>
<div class="pc-add-comment">
<h5><?php esc_html_e('Add Comment', 'pc-community-suggestions-7d3f'); ?></h5>
<textarea id="pc-comment-<?php echo esc_attr($suggestion->id); ?>" rows="3" placeholder="<?php esc_attr_e('Enter your comment...', 'pc-community-suggestions-7d3f'); ?>"></textarea>
<button type="button" class="button button-small" onclick="pcSubmitComment(<?php echo esc_attr($suggestion->id); ?>)">
<?php esc_html_e('Add Comment', 'pc-community-suggestions-7d3f'); ?>
</button>
</div>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
<?php
}
public function handle_suggestion_deletion() {
if (!current_user_can('manage_options')) {
wp_die(__('You do not have permission to perform this action.', 'pc-community-suggestions-7d3f'));
}
$suggestion_id = intval($_GET['id'] ?? 0);
check_admin_referer('pc_delete_suggestion_' . $suggestion_id);
if ($suggestion_id > 0) {
PC_Community_Suggestions_Database::delete_suggestion($suggestion_id);
wp_redirect(admin_url('admin.php?page=pc_community_suggestions&deleted=1'));
exit;
}
wp_redirect(admin_url('admin.php?page=pc_community_suggestions&error=1'));
exit;
}
}

View File

@@ -0,0 +1,552 @@
/* Community Suggestions Admin Styles */
.pc-admin-content {
/* Admin-scoped palette to keep admin UI consistent */
--pc-admin-primary: #2271b1;
--pc-admin-primary-strong: #135e96;
--pc-admin-muted: #646970;
--pc-admin-bg: #fff;
margin-top: 20px;
padding: 18px 0;
}
/* Admin button enforcement */
.pc-admin-content .pc-button {
border-radius: 6px;
}
.pc-admin-content .pc-button-primary {
background: var(--pc-admin-primary) !important;
border-color: var(--pc-admin-primary) !important;
color: #fff !important;
}
.wp-list-table .row-actions .view,
.wp-list-table .row-actions .comment {
color: var(--pc-admin-primary);
}
.pc-admin-controls {
display: flex;
justify-content: flex-end;
align-items: center;
margin-bottom: 15px;
gap: 15px;
}
.pc-sort-dropdown {
display: flex;
align-items: center;
gap: 8px;
}
.pc-sort-dropdown label {
font-weight: 600;
color: #1d2327;
font-size: 13px;
}
.pc-sort-dropdown select {
padding: 6px 12px;
border: 1px solid #8c8f94;
border-radius: 4px;
background: #fff;
font-size: 13px;
}
.pc-sort-dropdown select:focus {
outline: none;
border-color: #2271b1;
box-shadow: 0 0 0 1px #2271b1;
}
.pc-comment-item {
background: #2c3338;
border-color: #3c434a;
border-left-color: #2271b1;
}
.pc-add-comment textarea {
background: #1d2327;
border-color: #3c434a;
color: #dcdcde;
}
.pc-add-comment textarea:focus {
border-color: #2271b1;
box-shadow: 0 0 0 2px rgba(34, 113, 177, 0.2);
}
.pc-no-comments {
background: #2c3338;
border-color: #3c434a;
color: #9ca1a7;
}
.pc-settings-stats {
margin-top: 30px;
border-top: 1px solid #ccd0d4;
padding-top: 30px;
}
.pc-stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-top: 20px;
}
.pc-stat-card {
background: #fff;
border: 1px solid #ccd0d4;
border-radius: 4px;
padding: 20px;
text-align: center;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.pc-stat-card h3 {
font-size: 2em;
font-weight: bold;
color: #2271b1;
margin: 0 0 10px 0;
}
.pc-stat-card p {
color: #646970;
margin: 0;
font-size: 14px;
}
/* Status badges */
.pc-status {
display: inline-block;
padding: 4px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
line-height: 1;
}
.pc-status-pending {
background: #fff0c2;
color: #946c00;
border: 1px solid #ffd54f;
}
.pc-status-approved {
background: #e7f5e7;
color: #2a6b2a;
border: 1px solid #8bc34a;
}
.pc-status-rejected {
background: #fbe7e7;
color: #b32d2e;
border: 1px solid #f44336;
}
/* Table improvements */
.wp-list-table .pc-status {
margin-right: 8px;
}
.wp-list-table .row-actions {
margin-top: 4px;
}
.wp-list-table .row-actions .view,
.wp-list-table .row-actions .comment {
color: #2271b1;
}
.wp-list-table .row-actions .comment {
margin-left: 8px;
}
/* Comments styles */
.pc-comments-row {
background: #f9f9f9 !important;
}
.pc-comments-cell {
padding: 0 !important;
}
.pc-comments-container {
padding: 20px;
}
.pc-comments-container h4 {
margin: 0 0 15px 0;
font-size: 14px;
font-weight: 600;
color: #111;
}
.pc-comments-list {
margin-bottom: 20px;
}
.pc-comment-item {
background: #fff;
border: 1px solid #dcdcde;
border-radius: 6px;
padding: 15px;
margin-bottom: 15px;
border-left: 3px solid #2271b1;
}
/* Remove purple accents from admin comments: override public styles that use purple */
.pc-comments-container .pc-comment-card,
.pc-comments-container .pc-comment-item,
.pc-comments-container .pc-comments-section {
border-left-color: var(--pc-admin-primary) !important; /* use admin blue */
border-color: #dcdcde !important;
background: #fff !important;
color: #111 !important;
box-shadow: 0 1px 3px rgba(0,0,0,0.06) !important;
}
/* Make comment headers readable */
.pc-comments-container .pc-comment-header strong,
.pc-comments-container .pc-comment-content {
color: #111 !important;
}
.pc-comment-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.pc-comment-header strong {
font-weight: 600;
color: #dcdcde;
font-size: 13px;
}
.pc-sort-dropdown label {
color: #dcdcde;
}
.pc-comment-date {
color: #646970;
font-size: 12px;
font-style: italic;
}
.pc-comment-content {
line-height: 1.5;
color: #c3c4c7;
font-size: 13px;
margin-bottom: 10px;
}
.pc-comment-actions {
text-align: right;
}
.pc-no-comments {
text-align: center;
color: #646970;
font-style: italic;
padding: 15px;
background: #fff;
border: 1px dashed #dcdcde;
border-radius: 6px;
}
.pc-add-comment {
border-top: 1px solid #e0e0e0;
padding-top: 15px;
}
.pc-add-comment h5 {
margin: 0 0 10px 0;
font-size: 13px;
font-weight: 600;
color: #dcdcde;
}
.pc-add-comment textarea {
width: 100%;
min-height: 80px;
padding: 10px;
border: 1px solid #dcdcde;
border-radius: 6px;
font-size: 13px;
font-family: inherit;
resize: vertical;
margin-bottom: 10px;
}
.pc-add-comment textarea:focus {
outline: none;
border-color: #2271b1;
box-shadow: 0 0 0 2px rgba(34, 113, 177, 0.1);
}
/* Form styles */
.pc-form-group {
margin-bottom: 20px;
}
.pc-form-group label {
display: block;
font-weight: 600;
margin-bottom: 5px;
color: #1d2327;
}
.pc-form-group input[type="text"],
.pc-form-group textarea {
width: 100%;
max-width: 400px;
padding: 8px 12px;
border: 1px solid #8c8f94;
border-radius: 4px;
font-size: 14px;
line-height: 1.4;
}
.pc-form-group input[type="text"]:focus,
.pc-form-group textarea:focus {
border-color: #2271b1;
box-shadow: 0 0 0 1px #2271b1;
outline: none;
}
.pc-form-actions {
display: flex;
gap: 10px;
margin-top: 20px;
}
/* Button styles */
.pc-button {
display: inline-block;
padding: 8px 16px;
border: 1px solid #2271b1;
border-radius: 4px;
background: #2271b1;
color: #fff;
text-decoration: none;
font-size: 13px;
font-weight: 400;
line-height: 1.4;
cursor: pointer;
transition: all 0.15s ease-in-out;
}
.pc-button:hover {
background: #135e96;
border-color: #135e96;
color: #fff;
}
.pc-button:active {
background: #0a4b78;
border-color: #0a4b78;
}
.pc-button-primary {
background: #2271b1;
border-color: #2271b1;
}
.pc-button-primary:hover {
background: #135e96;
border-color: #135e96;
}
.pc-button-secondary {
background: #f6f7f7;
border-color: #dcdcde;
color: #2c3338;
}
.pc-button-secondary:hover {
background: #dcdcde;
border-color: #c3c4c7;
}
/* Card styles */
.pc-card {
background: #fff;
border: 1px solid #ccd0d4;
border-radius: 4px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.pc-card h3 {
margin: 0 0 15px 0;
color: #1d2327;
font-size: 16px;
font-weight: 600;
}
/* Responsive design */
@media screen and (max-width: 782px) {
.pc-stats-grid {
grid-template-columns: 1fr;
gap: 15px;
}
.pc-stat-card {
padding: 15px;
}
.pc-stat-card h3 {
font-size: 1.5em;
}
.pc-form-group input[type="text"],
.pc-form-group textarea {
max-width: 100%;
}
.pc-form-actions {
flex-direction: column;
gap: 10px;
}
.wp-list-table {
font-size: 14px;
}
.wp-list-table th,
.wp-list-table td {
padding: 8px 10px;
}
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
.pc-stat-card,
.pc-card {
background: #2c3338;
border-color: #3c434a;
}
.pc-stat-card p,
.pc-form-group label {
color: #dcdcde;
}
.pc-form-group input[type="text"],
.pc-form-group textarea {
background: #1d2327;
border-color: #3c434a;
color: #dcdcde;
}
.pc-form-group input[type="text"]:focus,
.pc-form-group textarea:focus {
border-color: #2271b1;
box-shadow: 0 0 0 1px #2271b1;
}
.pc-button-secondary {
background: #3c434a;
border-color: #4f5660;
color: #dcdcde;
}
.pc-button-secondary:hover {
background: #4f5660;
border-color: #646970;
}
}
/* Print styles */
@media print {
.pc-stats-grid {
grid-template-columns: repeat(3, 1fr);
gap: 15px;
}
.pc-stat-card {
border: 1px solid #000;
background: #fff !important;
color: #000 !important;
}
.pc-stat-card h3 {
color: #000 !important;
}
.pc-button {
display: none;
}
}
/* Animation for status changes */
@keyframes pc-fade-in {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.pc-stat-card,
.pc-card {
animation: pc-fade-in 0.3s ease-in-out;
}
/* Loading states */
.pc-loading {
opacity: 0.6;
pointer-events: none;
}
.pc-loading::after {
content: '';
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid #f3f3f3;
border-top: 2px solid #2271b1;
border-radius: 50%;
animation: pc-spin 1s linear infinite;
margin-left: 8px;
vertical-align: middle;
}
@keyframes pc-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Success/error messages */
.pc-notice {
padding: 12px;
border-radius: 4px;
margin-bottom: 20px;
border-left: 4px solid;
}
.pc-notice-success {
background: #edfaef;
border-color: #00a32a;
color: #2a6b2a;
}
.pc-notice-error {
background: #fcf0f1;
border-color: #d63638;
color: #b32d2e;
}
.pc-notice-info {
background: #f0f6ff;
border-color: #2271b1;
color: #135e96;
}

View File

@@ -0,0 +1,369 @@
jQuery(document).ready(function($) {
// View suggestion details modal
window.pcPreviewSuggestion = function(suggestionId) {
$.ajax({
url: pcCommunitySuggestionsAdmin.ajax_url,
type: 'POST',
data: {
action: 'pc_get_suggestion',
suggestion_id: suggestionId,
nonce: pcCommunitySuggestionsAdmin.nonce
},
success: function(response) {
if (response.success) {
pcShowSuggestionModal(response.data);
} else {
alert(pcCommunitySuggestionsAdmin.i18n.action_failed + ': ' + response.data.message);
}
},
error: function() {
alert(pcCommunitySuggestionsAdmin.i18n.action_failed);
}
});
};
function pcShowSuggestionModal(suggestion) {
const modalHtml = `
<div class="pc-modal" style="display: none;">
<div class="pc-modal-overlay"></div>
<div class="pc-modal-content">
<div class="pc-modal-header">
<h3>${suggestion.title}</h3>
<button class="pc-modal-close" onclick="pcCloseModal()">&times;</button>
</div>
<div class="pc-modal-body">
<div class="pc-suggestion-content">
${suggestion.content}
</div>
<div class="pc-suggestion-meta">
<p><strong>Author:</strong> ${suggestion.author}</p>
<p><strong>Submitted:</strong> ${suggestion.date}</p>
<p><strong>Upvotes:</strong> ${suggestion.upvotes}</p>
</div>
</div>
<div class="pc-modal-footer">
<button class="button button-primary" onclick="pcCloseModal()">Close</button>
</div>
</div>
</div>
`;
$('body').append(modalHtml);
$('.pc-modal').fadeIn(200);
// Prevent background scrolling
$('body').css('overflow', 'hidden');
}
window.pcCloseModal = function() {
$('.pc-modal').fadeOut(200, function() {
$(this).remove();
});
$('body').css('overflow', '');
};
// Add comment functionality
window.pcAddComment = function(suggestionId) {
var $commentsRow = $('#pc-comments-' + suggestionId);
$commentsRow.slideToggle();
};
window.pcSubmitComment = function(suggestionId) {
var $textarea = $('#pc-comment-' + suggestionId);
var comment = $textarea.val().trim();
if (!comment) {
alert('Please enter a comment.');
return;
}
var $button = $textarea.siblings('button');
var originalText = $button.text();
$button.prop('disabled', true).text('Adding...');
$.ajax({
url: pcCommunitySuggestionsAdmin.ajax_url,
type: 'POST',
data: {
action: 'pc_add_comment',
suggestion_id: suggestionId,
comment: comment,
nonce: pcCommunitySuggestionsAdmin.nonce
},
success: function(response) {
if (response.success) {
var commentHtml = '<div class="pc-comment-item" data-comment-id="' + response.data.comment.id + '">' +
'<div class="pc-comment-header">' +
'<strong>' + response.data.comment.author + '</strong> ' +
'<span class="pc-comment-date">' + response.data.comment.date + '</span>' +
'</div>' +
'<div class="pc-comment-content">' + response.data.comment.content + '</div>' +
'<div class="pc-comment-actions">' +
'<button class="button button-small button-link-delete" onclick="pcDeleteComment(' + response.data.comment.id + ')">Delete</button>' +
'</div>' +
'</div>';
var $commentsList = $('#pc-comments-list-' + suggestionId);
$commentsList.find('.pc-no-comments').remove();
$commentsList.append(commentHtml);
$textarea.val('');
alert(response.data.message);
} else {
alert(response.data.message || pcCommunitySuggestionsAdmin.i18n.action_failed);
}
},
error: function() {
alert(pcCommunitySuggestionsAdmin.i18n.action_failed);
},
complete: function() {
$button.prop('disabled', false).text(originalText);
}
});
};
window.pcDeleteComment = function(commentId) {
if (!confirm('Are you sure you want to delete this comment?')) {
return;
}
$.ajax({
url: pcCommunitySuggestionsAdmin.ajax_url,
type: 'POST',
data: {
action: 'pc_delete_comment',
comment_id: commentId,
nonce: pcCommunitySuggestionsAdmin.nonce
},
success: function(response) {
if (response.success) {
$('.pc-comment-item[data-comment-id="' + commentId + '"]').fadeOut(300, function() {
$(this).remove();
});
alert(response.data.message);
} else {
alert(response.data.message || pcCommunitySuggestionsAdmin.i18n.action_failed);
}
},
error: function() {
alert(pcCommunitySuggestionsAdmin.i18n.action_failed);
}
});
};
// Close modal on overlay click
$(document).on('click', '.pc-modal-overlay', function(e) {
if (e.target === this) {
pcCloseModal();
}
});
// Close modal on ESC key
$(document).on('keydown', function(e) {
if (e.key === 'Escape' && $('.pc-modal').is(':visible')) {
pcCloseModal();
}
});
// Add modal styles
$('<style>')
.text(`
.pc-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 99999;
display: flex;
align-items: center;
justify-content: center;
}
.pc-modal-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
}
.pc-modal-content {
background: #fff;
border-radius: 8px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow: hidden;
display: flex;
flex-direction: column;
z-index: 100000;
position: relative;
}
.pc-modal-header {
padding: 20px 25px;
border-bottom: 1px solid #dcdcde;
display: flex;
align-items: center;
justify-content: space-between;
}
.pc-modal-header h3 {
margin: 0;
font-size: 1.3em;
color: #1d2327;
flex: 1;
margin-right: 20px;
}
.pc-modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
line-height: 1;
color: #646970;
}
.pc-modal-close:hover {
color: #2271b1;
}
.pc-modal-body {
padding: 25px;
flex: 1;
overflow-y: auto;
}
.pc-suggestion-content {
margin-bottom: 20px;
line-height: 1.6;
color: #2c3338;
}
.pc-suggestion-content p {
margin: 0 0 15px 0;
}
.pc-suggestion-content p:last-child {
margin-bottom: 0;
}
.pc-suggestion-meta {
border-top: 1px solid #dcdcde;
padding-top: 20px;
font-size: 14px;
}
.pc-suggestion-meta p {
margin: 0 0 8px 0;
}
.pc-suggestion-meta p:last-child {
margin-bottom: 0;
}
.pc-modal-footer {
padding: 20px 25px;
border-top: 1px solid #dcdcde;
text-align: right;
}
@media screen and (max-width: 782px) {
.pc-modal-content {
width: 95%;
margin: 20px;
}
.pc-modal-header {
padding: 15px 20px;
}
.pc-modal-body {
padding: 20px;
}
.pc-modal-footer {
padding: 15px 20px;
}
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
.pc-modal-content {
background: #2c3338;
border: 1px solid #3c434a;
}
.pc-modal-header,
.pc-modal-footer {
border-color: #3c434a;
}
.pc-modal-header h3 {
color: #dcdcde;
}
.pc-modal-close {
color: #9ca1a7;
}
.pc-modal-close:hover {
color: #2271b1;
}
.pc-suggestion-content {
color: #c3c4c7;
}
.pc-suggestion-meta {
border-color: #3c434a;
}
}
`)
.appendTo('head');
// Handle bulk actions
$('.pc-bulk-action').on('change', function() {
const action = $(this).val();
if (action) {
const checked = $('.pc-bulk-checkbox:checked');
if (checked.length === 0) {
alert(pcCommunitySuggestionsAdmin.i18n.no_items_selected);
$(this).val('');
return;
}
const suggestionIds = checked.map(function() {
return $(this).val();
}).get();
if (confirm(pcCommunitySuggestionsAdmin.i18n.confirm_bulk_action.replace('{count}', checked.length).replace('{action}', action))) {
$.ajax({
url: pcCommunitySuggestionsAdmin.ajax_url,
type: 'POST',
data: {
action: 'pc_get_stats',
nonce: pcCommunitySuggestionsAdmin.nonce
},
success: function(response) {
if (response.success) {
$('#pc-stat-total').text(response.data.total);
}
}
});
}
// Update stats every 30 seconds if on stats page
if ($('.pc-settings-stats').length) {
setInterval(pcUpdateStats, 30000);
}
});

View File

@@ -0,0 +1,342 @@
<?php
class PC_Community_Suggestions_Database {
public static function create_tables() {
global $wpdb;
error_log('PC Community Suggestions: create_tables called');
$charset_collate = $wpdb->get_charset_collate();
$suggestions_table = $wpdb->prefix . 'pc_community_suggestions';
$votes_table = $wpdb->prefix . 'pc_community_votes';
$comments_table = $wpdb->prefix . 'pc_community_comments';
error_log('PC Community Suggestions: Creating tables - ' . $suggestions_table . ', ' . $votes_table . ', ' . $comments_table);
// Suggestions table
$sql_suggestions = "CREATE TABLE IF NOT EXISTS $suggestions_table (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
upvotes INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY user_id (user_id),
KEY created_at (created_at),
KEY upvotes (upvotes)
) $charset_collate;";
// Votes table
$sql_votes = "CREATE TABLE IF NOT EXISTS $votes_table (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
suggestion_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY user_suggestion (user_id, suggestion_id),
KEY suggestion_id (suggestion_id)
) $charset_collate;";
// Comments table
$sql_comments = "CREATE TABLE IF NOT EXISTS $comments_table (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
suggestion_id BIGINT UNSIGNED NOT NULL,
admin_id BIGINT UNSIGNED NOT NULL,
comment TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY suggestion_id (suggestion_id),
KEY admin_id (admin_id)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
// Clear any previous errors
$wpdb->last_error = '';
// Create tables directly using dbDelta
dbDelta($sql_suggestions);
dbDelta($sql_votes);
dbDelta($sql_comments);
// Log any errors for debugging
if (!empty($wpdb->last_error)) {
error_log('PC Community Suggestions Table Creation Error: ' . $wpdb->last_error);
} else {
error_log('PC Community Suggestions: Tables created without database errors');
}
// Verify tables were created
$tables_created = true;
$tables_to_check = array($suggestions_table, $votes_table, $comments_table);
foreach ($tables_to_check as $table) {
$exists = $wpdb->get_var("SHOW TABLES LIKE '$table'");
if (!$exists) {
error_log('PC Community Suggestions: Failed to create table ' . $table);
$tables_created = false;
} else {
error_log('PC Community Suggestions: Table exists - ' . $table);
}
}
return $tables_created;
}
public static function add_suggestion($user_id, $title, $content) {
global $wpdb;
$table_name = $wpdb->prefix . 'pc_community_suggestions';
error_log('PC Community Suggestions: add_suggestion called, table: ' . $table_name);
error_log('PC Community Suggestions: user_id=' . $user_id . ', title=' . substr($title, 0, 50));
// Check if table exists
$table_exists = $wpdb->get_var("SHOW TABLES LIKE '$table_name'");
if (!$table_exists) {
error_log('PC Community Suggestions: Table does not exist - ' . $table_name);
error_log('PC Community Suggestions: Creating tables now...');
self::create_tables();
// Check again
$table_exists = $wpdb->get_var("SHOW TABLES LIKE '$table_name'");
if (!$table_exists) {
error_log('PC Community Suggestions: Still cannot find table after creation attempt');
return false;
}
}
$result = $wpdb->insert(
$table_name,
array(
'user_id' => $user_id,
'title' => sanitize_text_field($title),
'content' => wp_kses_post($content)
),
array('%d', '%s', '%s')
);
if ($result === false) {
error_log('PC Community Suggestions: Failed to insert suggestion - ' . $wpdb->last_error);
return false;
}
$insert_id = $wpdb->insert_id;
error_log('PC Community Suggestions: Suggestion inserted with ID: ' . $insert_id);
return $insert_id;
}
public static function get_suggestions($page = 1, $per_page = 10, $sort = 'popular') {
global $wpdb;
$table_name = $wpdb->prefix . 'pc_community_suggestions';
$offset = ($page - 1) * $per_page;
$order_by = $sort === 'newest' ? 'created_at DESC' : 'upvotes DESC, created_at DESC';
$query = $wpdb->prepare(
"SELECT s.*, u.user_login, u.display_name
FROM $table_name s
LEFT JOIN {$wpdb->users} u ON s.user_id = u.ID
ORDER BY $order_by
LIMIT %d OFFSET %d",
$per_page,
$offset
);
return $wpdb->get_results($query);
}
public static function get_suggestion_count() {
global $wpdb;
$table_name = $wpdb->prefix . 'pc_community_suggestions';
return $wpdb->get_var("SELECT COUNT(*) FROM $table_name");
}
public static function get_suggestion_by_id($suggestion_id) {
global $wpdb;
$table_name = $wpdb->prefix . 'pc_community_suggestions';
return $wpdb->get_row($wpdb->prepare(
"SELECT s.*, u.user_login, u.display_name
FROM $table_name s
LEFT JOIN {$wpdb->users} u ON s.user_id = u.ID
WHERE s.id = %d",
$suggestion_id
));
}
public static function add_vote($suggestion_id, $user_id) {
global $wpdb;
$votes_table = $wpdb->prefix . 'pc_community_votes';
$suggestions_table = $wpdb->prefix . 'pc_community_suggestions';
// Check if user already voted
$existing_vote = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM $votes_table WHERE suggestion_id = %d AND user_id = %d",
$suggestion_id,
$user_id
)
);
if ($existing_vote > 0) {
return false;
}
$wpdb->query('START TRANSACTION');
// Add vote
$vote_result = $wpdb->insert(
$votes_table,
array(
'suggestion_id' => $suggestion_id,
'user_id' => $user_id
),
array('%d', '%d')
);
if (!$vote_result) {
$wpdb->query('ROLLBACK');
return false;
}
// Update suggestion upvote count
$update_result = $wpdb->query(
$wpdb->prepare(
"UPDATE $suggestions_table SET upvotes = upvotes + 1 WHERE id = %d",
$suggestion_id
)
);
if (!$update_result) {
$wpdb->query('ROLLBACK');
return false;
}
$wpdb->query('COMMIT');
return true;
}
public static function get_user_vote_count($user_id) {
global $wpdb;
$votes_table = $wpdb->prefix . 'pc_community_votes';
return $wpdb->get_var(
$wpdb->prepare("SELECT COUNT(*) FROM $votes_table WHERE user_id = %d", $user_id)
);
}
public static function has_user_voted($suggestion_id, $user_id) {
global $wpdb;
$votes_table = $wpdb->prefix . 'pc_community_votes';
return (bool) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM $votes_table WHERE suggestion_id = %d AND user_id = %d",
$suggestion_id,
$user_id
)
);
}
public static function add_comment($suggestion_id, $admin_id, $comment) {
global $wpdb;
$comments_table = $wpdb->prefix . 'pc_community_comments';
return $wpdb->insert(
$comments_table,
array(
'suggestion_id' => $suggestion_id,
'admin_id' => $admin_id,
'comment' => wp_kses_post($comment)
),
array('%d', '%d', '%s')
);
}
public static function get_comments($suggestion_id) {
global $wpdb;
$comments_table = $wpdb->prefix . 'pc_community_comments';
return $wpdb->get_results($wpdb->prepare(
"SELECT c.*, u.user_login, u.display_name
FROM $comments_table c
LEFT JOIN {$wpdb->users} u ON c.admin_id = u.ID
WHERE c.suggestion_id = %d
ORDER BY c.created_at ASC",
$suggestion_id
));
}
public static function delete_comment($comment_id) {
global $wpdb;
$comments_table = $wpdb->prefix . 'pc_community_comments';
return $wpdb->delete(
$comments_table,
array('id' => $comment_id),
array('%d')
);
}
public static function delete_suggestion($suggestion_id) {
global $wpdb;
$suggestions_table = $wpdb->prefix . 'pc_community_suggestions';
$votes_table = $wpdb->prefix . 'pc_community_votes';
$comments_table = $wpdb->prefix . 'pc_community_comments';
$wpdb->query('START TRANSACTION');
// Delete votes first
$wpdb->delete($votes_table, array('suggestion_id' => $suggestion_id), array('%d'));
// Delete comments
$wpdb->delete($comments_table, array('suggestion_id' => $suggestion_id), array('%d'));
// Delete suggestion
$result = $wpdb->delete($suggestions_table, array('id' => $suggestion_id), array('%d'));
if ($result === false) {
$wpdb->query('ROLLBACK');
return false;
}
$wpdb->query('COMMIT');
return true;
}
public static function verify_tables() {
global $wpdb;
$suggestions_table = $wpdb->prefix . 'pc_community_suggestions';
$votes_table = $wpdb->prefix . 'pc_community_votes';
$comments_table = $wpdb->prefix . 'pc_community_comments';
$tables_to_check = array($suggestions_table, $votes_table, $comments_table);
$all_exist = true;
foreach ($tables_to_check as $table) {
$exists = $wpdb->get_var("SHOW TABLES LIKE '$table'");
if (!$exists) {
error_log('PC Community Suggestions: Missing table ' . $table);
$all_exist = false;
}
}
return $all_exist;
}
}

View File

@@ -0,0 +1,401 @@
<?php
class PC_Community_Suggestions_REST_API {
public function __construct() {
add_action('rest_api_init', array($this, 'register_routes'));
add_action('wp_ajax_pc_submit_suggestion', array($this, 'handle_suggestion_submission'));
add_action('wp_ajax_pc_vote_suggestion', array($this, 'handle_vote'));
add_action('wp_ajax_pc_get_suggestion', array($this, 'handle_get_suggestion'));
add_action('wp_ajax_pc_get_stats', array($this, 'handle_get_stats'));
add_action('wp_ajax_pc_add_comment', array($this, 'handle_add_comment'));
add_action('wp_ajax_pc_delete_comment', array($this, 'handle_delete_comment'));
add_action('wp_ajax_nopriv_pc_submit_suggestion', array($this, 'handle_not_logged_in'));
add_action('wp_ajax_nopriv_pc_vote_suggestion', array($this, 'handle_not_logged_in'));
}
public function register_routes() {
register_rest_route('pc-community-suggestions/v1', '/suggestions', array(
'methods' => 'POST',
'callback' => array($this, 'create_suggestion'),
'permission_callback' => array($this, 'check_user_permission'),
'args' => array(
'title' => array(
'required' => true,
'validate_callback' => array($this, 'validate_title'),
'sanitize_callback' => 'sanitize_text_field'
),
'content' => array(
'required' => true,
'validate_callback' => array($this, 'validate_content'),
'sanitize_callback' => 'wp_kses_post'
)
)
));
register_rest_route('pc-community-suggestions/v1', '/suggestions/(?P<id>\d+)/vote', array(
'methods' => 'POST',
'callback' => array($this, 'vote_suggestion'),
'permission_callback' => array($this, 'check_user_permission'),
'args' => array(
'id' => array(
'required' => true,
'validate_callback' => array($this, 'validate_suggestion_id')
)
)
));
register_rest_route('pc-community-suggestions/v1', '/suggestions', array(
'methods' => 'GET',
'callback' => array($this, 'get_suggestions'),
'permission_callback' => '__return_true',
'args' => array(
'page' => array(
'default' => 1,
'validate_callback' => array($this, 'validate_page')
),
'per_page' => array(
'default' => 10,
'validate_callback' => array($this, 'validate_per_page')
),
'sort' => array(
'default' => 'popular',
'validate_callback' => array($this, 'validate_sort')
)
)
));
}
public function check_user_permission() {
return is_user_logged_in();
}
public function validate_title($title) {
$title = sanitize_text_field($title);
return !empty($title) && strlen($title) <= 255;
}
public function validate_content($content) {
$content = wp_kses_post($content);
return !empty($content) && strlen($content) >= 10;
}
public function validate_suggestion_id($id) {
return is_numeric($id) && $id > 0;
}
public function validate_page($page) {
return is_numeric($page) && $page > 0;
}
public function validate_per_page($per_page) {
return is_numeric($per_page) && $per_page > 0 && $per_page <= 50;
}
public function validate_sort($sort) {
return in_array($sort, array('popular', 'newest'));
}
public function create_suggestion(WP_REST_Request $request) {
$title = $request->get_param('title');
$content = $request->get_param('content');
$user_id = get_current_user_id();
// Check for duplicate submissions
$existing = $this->check_duplicate_suggestion($user_id, $title);
if ($existing) {
return new WP_Error('duplicate', __('You have already submitted a similar suggestion.', 'pc-community-suggestions-7d3f'), array('status' => 400));
}
$suggestion_id = PC_Community_Suggestions_Database::add_suggestion($user_id, $title, $content);
if (!$suggestion_id) {
return new WP_Error('database_error', __('Failed to create suggestion.', 'pc-community-suggestions-7d3f'), array('status' => 500));
}
return rest_ensure_response(array(
'success' => true,
'suggestion_id' => $suggestion_id,
'message' => __('Your suggestion has been published successfully.', 'pc-community-suggestions-7d3f')
));
}
private function check_duplicate_suggestion($user_id, $title) {
global $wpdb;
$table_name = $wpdb->prefix . 'pc_community_suggestions';
$similar_title = '%' . $wpdb->esc_like($title) . '%';
$count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM $table_name WHERE user_id = %d AND title LIKE %s AND created_at > DATE_SUB(NOW(), INTERVAL 1 HOUR)",
$user_id,
$similar_title
)
);
return $count > 0;
}
public function vote_suggestion(WP_REST_Request $request) {
$suggestion_id = $request->get_param('id');
$user_id = get_current_user_id();
$result = PC_Community_Suggestions_Database::add_vote($suggestion_id, $user_id);
if (!$result) {
return new WP_Error('already_voted', __('You have already voted for this suggestion.', 'pc-community-suggestions-7d3f'), array('status' => 400));
}
return rest_ensure_response(array(
'success' => true,
'message' => __('Vote recorded successfully.', 'pc-community-suggestions-7d3f')
));
}
public function get_suggestions(WP_REST_Request $request) {
$page = $request->get_param('page');
$per_page = $request->get_param('per_page');
$sort = $request->get_param('sort');
$suggestions = PC_Community_Suggestions_Database::get_suggestions($page, $per_page, $sort);
$total = PC_Community_Suggestions_Database::get_suggestion_count();
$formatted_suggestions = array();
foreach ($suggestions as $suggestion) {
$formatted_suggestions[] = array(
'id' => $suggestion->id,
'title' => $suggestion->title,
'content' => $suggestion->content,
'upvotes' => $suggestion->upvotes,
'author' => $suggestion->display_name ?: $suggestion->user_login,
'created_at' => $suggestion->created_at,
'human_date' => human_time_diff(strtotime($suggestion->created_at), current_time('timestamp'))
);
}
return rest_ensure_response(array(
'suggestions' => $formatted_suggestions,
'pagination' => array(
'current_page' => $page,
'per_page' => $per_page,
'total' => $total,
'total_pages' => ceil($total / $per_page)
)
));
}
public function handle_suggestion_submission() {
error_log('PC Community Suggestions: handle_suggestion_submission called');
$nonce = isset($_POST['nonce']) ? $_POST['nonce'] : '';
error_log('PC Community Suggestions: Nonce value: ' . substr($nonce, 0, 20) . '...');
if (!check_ajax_referer('pc_community_suggestions_nonce', 'nonce', false)) {
error_log('PC Community Suggestions: Nonce check failed');
wp_send_json_error(array('message' => __('Security check failed. Please try again.', 'pc-community-suggestions-7d3f')));
}
if (!is_user_logged_in()) {
error_log('PC Community Suggestions: User not logged in');
wp_send_json_error(array('message' => __('You must be logged in to submit suggestions.', 'pc-community-suggestions-7d3f')));
}
$title = isset($_POST['title']) ? sanitize_text_field($_POST['title']) : '';
$content = isset($_POST['content']) ? wp_kses_post($_POST['content']) : '';
error_log('PC Community Suggestions: Title length: ' . strlen($title) . ', Content length: ' . strlen($content));
if (empty($title) || empty($content)) {
wp_send_json_error(array('message' => __('Please fill in all required fields.', 'pc-community-suggestions-7d3f')));
}
if (strlen($title) > 255) {
wp_send_json_error(array('message' => __('Title must be less than 255 characters.', 'pc-community-suggestions-7d3f')));
}
if (strlen($content) < 10) {
wp_send_json_error(array('message' => __('Content must be at least 10 characters long.', 'pc-community-suggestions-7d3f')));
}
$user_id = get_current_user_id();
error_log('PC Community Suggestions: User ID: ' . $user_id);
try {
$existing = $this->check_duplicate_suggestion($user_id, $title);
if ($existing) {
error_log('PC Community Suggestions: Duplicate suggestion detected');
wp_send_json_error(array('message' => __('You have already submitted a similar suggestion recently.', 'pc-community-suggestions-7d3f')));
}
if (!PC_Community_Suggestions_Database::verify_tables()) {
error_log('PC Community Suggestions: Tables do not exist, creating...');
PC_Community_Suggestions_Database::create_tables();
}
$suggestion_id = PC_Community_Suggestions_Database::add_suggestion($user_id, $title, $content);
if (!$suggestion_id) {
global $wpdb;
$last_error = $wpdb->last_error;
error_log('PC Community Suggestions DB Error: ' . $last_error);
wp_send_json_error(array('message' => __('Failed to submit suggestion. Database error.', 'pc-community-suggestions-7d3f')));
}
error_log('PC Community Suggestions: Suggestion created successfully with ID: ' . $suggestion_id);
wp_send_json_success(array(
'message' => __('Your suggestion has been published successfully.', 'pc-community-suggestions-7d3f')
));
} catch (Exception $e) {
error_log('PC Community Suggestions Exception: ' . $e->getMessage());
wp_send_json_error(array('message' => __('An error occurred: ', 'pc-community-suggestions-7d3f') . $e->getMessage()));
}
}
public function handle_vote() {
if (!check_ajax_referer('pc_community_suggestions_nonce', 'nonce', false)) {
wp_send_json_error(array('message' => __('Security check failed. Please try again.', 'pc-community-suggestions-7d3f')));
}
if (!is_user_logged_in()) {
wp_send_json_error(array('message' => __('You must be logged in to vote.', 'pc-community-suggestions-7d3f')));
}
$suggestion_id = isset($_POST['suggestion_id']) ? intval($_POST['suggestion_id']) : 0;
$user_id = get_current_user_id();
if ($suggestion_id <= 0) {
wp_send_json_error(array('message' => __('Invalid suggestion.', 'pc-community-suggestions-7d3f')));
}
$result = PC_Community_Suggestions_Database::add_vote($suggestion_id, $user_id);
if (!$result) {
wp_send_json_error(array('message' => __('You have already voted for this suggestion.', 'pc-community-suggestions-7d3f')));
}
wp_send_json_success(array('message' => __('Vote recorded successfully.', 'pc-community-suggestions-7d3f')));
}
public function handle_not_logged_in() {
wp_send_json_error(array('message' => __('You must be logged in to perform this action.', 'pc-community-suggestions-7d3f')));
}
public function handle_get_suggestion() {
if (!current_user_can('manage_options')) {
wp_send_json_error(array('message' => __('You do not have permission to view this suggestion.', 'pc-community-suggestions-7d3f')));
}
if (!check_ajax_referer('pc_admin_nonce', 'nonce', false)) {
wp_send_json_error(array('message' => __('Security check failed. Please try again.', 'pc-community-suggestions-7d3f')));
}
$suggestion_id = isset($_POST['suggestion_id']) ? intval($_POST['suggestion_id']) : 0;
if ($suggestion_id <= 0) {
wp_send_json_error(array('message' => __('Invalid suggestion ID.', 'pc-community-suggestions-7d3f')));
}
global $wpdb;
$table_name = $wpdb->prefix . 'pc_community_suggestions';
$suggestion = $wpdb->get_row(
$wpdb->prepare(
"SELECT s.*, u.user_login, u.display_name
FROM $table_name s
LEFT JOIN {$wpdb->users} u ON s.user_id = u.ID
WHERE s.id = %d",
$suggestion_id
)
);
if (!$suggestion) {
wp_send_json_error(array('message' => __('Suggestion not found.', 'pc-community-suggestions-7d3f')));
}
wp_send_json_success(array(
'id' => $suggestion->id,
'title' => $suggestion->title,
'content' => wpautop($suggestion->content),
'author' => $suggestion->display_name ?: $suggestion->user_login,
'date' => date_i18n(get_option('date_format'), strtotime($suggestion->created_at)),
'upvotes' => $suggestion->upvotes,
));
}
public function handle_get_stats() {
if (!current_user_can('manage_options')) {
wp_send_json_error(array('message' => __('You do not have permission to view statistics.', 'pc-community-suggestions-7d3f')));
}
if (!check_ajax_referer('pc_admin_nonce', 'nonce', false)) {
wp_send_json_error(array('message' => __('Security check failed. Please try again.', 'pc-community-suggestions-7d3f')));
}
wp_send_json_success(array(
'total' => PC_Community_Suggestions_Database::get_suggestion_count()
));
}
public function handle_add_comment() {
if (!current_user_can('manage_options')) {
wp_send_json_error(array('message' => __('You do not have permission to add comments.', 'pc-community-suggestions-7d3f')));
}
if (!check_ajax_referer('pc_admin_nonce', 'nonce', false)) {
wp_send_json_error(array('message' => __('Security check failed. Please try again.', 'pc-community-suggestions-7d3f')));
}
$suggestion_id = isset($_POST['suggestion_id']) ? intval($_POST['suggestion_id']) : 0;
$comment = isset($_POST['comment']) ? wp_kses_post($_POST['comment']) : '';
$admin_id = get_current_user_id();
if ($suggestion_id <= 0 || empty($comment)) {
wp_send_json_error(array('message' => __('Invalid data provided.', 'pc-community-suggestions-7d3f')));
}
$comment_id = PC_Community_Suggestions_Database::add_comment($suggestion_id, $admin_id, $comment);
if (!$comment_id) {
wp_send_json_error(array('message' => __('Failed to add comment.', 'pc-community-suggestions-7d3f')));
}
$admin = get_userdata($admin_id);
$admin_name = $admin->display_name ?: $admin->user_login;
wp_send_json_success(array(
'message' => __('Comment added successfully.', 'pc-community-suggestions-7d3f'),
'comment' => array(
'id' => $comment_id,
'content' => wpautop($comment),
'author' => $admin_name,
'date' => date_i18n(get_option('date_format'), current_time('timestamp'))
)
));
}
public function handle_delete_comment() {
if (!current_user_can('manage_options')) {
wp_send_json_error(array('message' => __('You do not have permission to delete comments.', 'pc-community-suggestions-7d3f')));
}
if (!check_ajax_referer('pc_admin_nonce', 'nonce', false)) {
wp_send_json_error(array('message' => __('Security check failed. Please try again.', 'pc-community-suggestions-7d3f')));
}
$comment_id = isset($_POST['comment_id']) ? intval($_POST['comment_id']) : 0;
if ($comment_id <= 0) {
wp_send_json_error(array('message' => __('Invalid comment ID.', 'pc-community-suggestions-7d3f')));
}
$result = PC_Community_Suggestions_Database::delete_comment($comment_id);
if (!$result) {
wp_send_json_error(array('message' => __('Failed to delete comment.', 'pc-community-suggestions-7d3f')));
}
wp_send_json_success(array(
'message' => __('Comment deleted successfully.', 'pc-community-suggestions-7d3f')
));
}
}

View File

@@ -0,0 +1,229 @@
<?php
class PC_Community_Suggestions_Shortcodes {
public function __construct() {
add_shortcode('community_suggestions', array($this, 'render_suggestions_page'));
}
public function render_suggestions_page($atts) {
$current_suggestion = isset($_GET['suggestion']) ? intval($_GET['suggestion']) : 0;
ob_start();
if ($current_suggestion > 0) {
$this->render_suggestion_detail($current_suggestion);
} else {
$this->render_suggestions_interface();
}
return ob_get_clean();
}
private function render_suggestion_detail($suggestion_id) {
$suggestion = PC_Community_Suggestions_Database::get_suggestion_by_id($suggestion_id);
$comments = $suggestion ? PC_Community_Suggestions_Database::get_comments($suggestion_id) : array();
if (!$suggestion) {
echo '<div class="pc-suggestions-card">';
echo '<p>' . esc_html__('Suggestion not found.', 'pc-community-suggestions-7d3f') . '</p>';
echo '<a href="' . esc_url(remove_query_arg('suggestion')) . '" class="pc-button">' . esc_html__('Back to Suggestions', 'pc-community-suggestions-7d3f') . '</a>';
echo '</div>';
return;
}
?>
<div class="pc-suggestions-container">
<div class="pc-suggestions-header">
<a href="<?php echo esc_url(remove_query_arg('suggestion')); ?>" class="pc-back-link">
← <?php esc_html_e('Back to Suggestions', 'pc-community-suggestions-7d3f'); ?>
</a>
</div>
<div class="pc-suggestion-detail">
<?php $this->render_suggestion_card($suggestion, true); ?>
<div class="pc-comments-section">
<h3><?php esc_html_e('Admin Comments', 'pc-community-suggestions-7d3f'); ?></h3>
<?php if (empty($comments)) : ?>
<p class="pc-no-comments"><?php esc_html_e('No admin comments yet.', 'pc-community-suggestions-7d3f'); ?></p>
<?php else : ?>
<?php foreach ($comments as $comment) : ?>
<div class="pc-comment-card">
<div class="pc-comment-header">
<span class="pc-comment-author"><?php echo esc_html($comment->display_name ?: $comment->user_login); ?></span>
<span class="pc-comment-date">
<?php echo date_i18n(get_option('date_format'), strtotime($comment->created_at)); ?>
</span>
</div>
<div class="pc-comment-content">
<?php echo wp_kses_post(wpautop($comment->comment)); ?>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</div>
<?php
}
private function render_suggestions_interface() {
$current_page = max(1, get_query_var('paged', 1));
$sort = isset($_GET['sort']) && in_array($_GET['sort'], ['popular', 'newest']) ? $_GET['sort'] : 'popular';
$per_page = 10;
$suggestions = PC_Community_Suggestions_Database::get_suggestions($current_page, $per_page, $sort);
$total_suggestions = PC_Community_Suggestions_Database::get_suggestion_count();
$total_pages = ceil($total_suggestions / $per_page);
?>
<div class="pc-community-suggestions-container">
<div class="pc-suggestions-header">
<div class="pc-suggestions-controls">
<?php if (is_user_logged_in()) : ?>
<button class="pc-button pc-button-primary" onclick="pcToggleSuggestionForm()">
<?php esc_html_e('Add Suggestion', 'pc-community-suggestions-7d3f'); ?>
</button>
<?php endif; ?>
<div class="pc-sort-dropdown">
<select onchange="pcChangeSort(this.value)">
<option value="popular" <?php selected($sort, 'popular'); ?>><?php esc_html_e('Most Upvoted', 'pc-community-suggestions-7d3f'); ?></option>
<option value="newest" <?php selected($sort, 'newest'); ?>><?php esc_html_e('Newest', 'pc-community-suggestions-7d3f'); ?></option>
</select>
</div>
</div>
</div>
<?php if (is_user_logged_in()) : ?>
<div id="pc-suggestion-form" class="pc-suggestion-form" style="display: none;">
<div class="pc-form-card">
<h3><?php esc_html_e('Add Your Suggestion', 'pc-community-suggestions-7d3f'); ?></h3>
<form id="pc-new-suggestion-form">
<div class="pc-form-group">
<label for="pc-suggestion-title"><?php esc_html_e('Title', 'pc-community-suggestions-7d3f'); ?></label>
<input type="text" id="pc-suggestion-title" name="title" required maxlength="255">
</div>
<div class="pc-form-group">
<label for="pc-suggestion-content"><?php esc_html_e('Details', 'pc-community-suggestions-7d3f'); ?></label>
<textarea id="pc-suggestion-content" name="content" rows="5" required></textarea>
</div>
<div class="pc-form-actions">
<button type="button" class="pc-button" onclick="pcToggleSuggestionForm()">
<?php esc_html_e('Cancel', 'pc-community-suggestions-7d3f'); ?>
</button>
<button type="submit" class="pc-button pc-button-primary">
<?php esc_html_e('Submit Suggestion', 'pc-community-suggestions-7d3f'); ?>
</button>
</div>
</form>
</div>
</div>
<?php endif; ?>
<div class="pc-suggestions-list">
<?php if (empty($suggestions)) : ?>
<div class="pc-no-suggestions">
<p><?php esc_html_e('No suggestions yet. Be the first to share your idea!', 'pc-community-suggestions-7d3f'); ?></p>
</div>
<?php else : ?>
<?php foreach ($suggestions as $suggestion) : ?>
<?php $this->render_suggestion_card($suggestion); ?>
<?php endforeach; ?>
<?php endif; ?>
</div>
<?php if ($total_pages > 1) : ?>
<div class="pc-suggestions-pagination">
<?php
echo paginate_links(array(
'base' => get_pagenum_link(1) . '%_%',
'format' => 'page/%#%/',
'current' => $current_page,
'total' => $total_pages,
'prev_text' => __('« Previous', 'pc-community-suggestions-7d3f'),
'next_text' => __('Next »', 'pc-community-suggestions-7d3f'),
'add_args' => array('sort' => $sort)
));
?>
</div>
<?php endif; ?>
</div>
<?php
}
private function render_suggestion_card($suggestion, $detail_view = false) {
$user_has_voted = is_user_logged_in() ? PC_Community_Suggestions_Database::has_user_voted($suggestion->id, get_current_user_id()) : false;
$vote_class = $user_has_voted ? 'pc-voted' : '';
if ($detail_view) {
$vote_button_html = '';
} else {
$vote_button_html = 'onclick="pcVoteSuggestion(' . esc_attr($suggestion->id) . ')"';
}
?>
<div class="pc-suggestion-card <?php echo $detail_view ? 'pc-suggestion-card-detail' : ''; ?>" data-suggestion-id="<?php echo esc_attr($suggestion->id); ?>">
<div class="pc-suggestion-vote">
<?php if (is_user_logged_in()) : ?>
<button class="pc-vote-button <?php echo esc_attr($vote_class); ?>"
<?php echo $vote_button_html; ?>
<?php echo $user_has_voted || $detail_view ? 'disabled' : ''; ?>>
<span class="pc-vote-arrow">↑</span>
<span class="pc-vote-count"><?php echo esc_html($suggestion->upvotes); ?></span>
</button>
<?php else : ?>
<div class="pc-vote-button pc-vote-disabled">
<span class="pc-vote-arrow">↑</span>
<span class="pc-vote-count"><?php echo esc_html($suggestion->upvotes); ?></span>
</div>
<?php endif; ?>
</div>
<div class="pc-suggestion-content">
<h3 class="pc-suggestion-title">
<?php if (!$detail_view) : ?>
<a href="<?php echo esc_url(add_query_arg('suggestion', $suggestion->id, get_permalink())); ?>">
<?php echo esc_html($suggestion->title); ?>
</a>
<?php else : ?>
<?php echo esc_html($suggestion->title); ?>
<?php endif; ?>
</h3>
<div class="pc-suggestion-body">
<?php
if ($detail_view) {
echo wp_kses_post(wpautop($suggestion->content));
} else {
echo wp_kses_post(wpautop(wp_trim_words($suggestion->content, 30, '...')));
}
?>
</div>
<?php if (!$detail_view) : ?>
<div class="pc-suggestion-actions">
<a href="<?php echo esc_url(add_query_arg('suggestion', $suggestion->id, get_permalink())); ?>"
class="pc-button pc-button-small">
<?php esc_html_e('View Details', 'pc-community-suggestions-7d3f'); ?>
</a>
</div>
<?php endif; ?>
<div class="pc-suggestion-meta">
<span class="pc-suggestion-author">
<?php
printf(
__('By %s', 'pc-community-suggestions-7d3f'),
esc_html($suggestion->display_name ?: $suggestion->user_login)
);
?>
</span>
<span class="pc-suggestion-date">
<?php echo human_time_diff(strtotime($suggestion->created_at), current_time('timestamp')) . ' ' . __('ago', 'pc-community-suggestions-7d3f'); ?>
</span>
</div>
</div>
</div>
<?php
}
}

View File

@@ -0,0 +1,295 @@
<?php
/**
* Plugin Name: Plugin Compass Community suggestions
* Plugin URI: https://plugincompass.com/plugins/pc-community-suggestions-7d3f
* Description: A community suggestions system where logged-in users can create and upvote improvement suggestions.
* Version: 1.0.0
* Author: Plugin Compass
* Author URI: https://plugincompass.com
* Text Domain: pc-community-suggestions-7d3f
* Domain Path: /languages
* Update URI: https://plugincompass.com/plugins/pc-community-suggestions-7d3f
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
// Define plugin constants
define('PC_COMMUNITY_SUGGESTIONS_VERSION', '1.0.0');
define('PC_COMMUNITY_SUGGESTIONS_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('PC_COMMUNITY_SUGGESTIONS_PLUGIN_URL', plugin_dir_url(__FILE__));
define('PC_COMMUNITY_SUGGESTIONS_PLUGIN_BASENAME', plugin_basename(__FILE__));
// Prevent WordPress.org update checks
add_filter('site_transient_update_plugins', function($value) {
$plugin_file = PC_COMMUNITY_SUGGESTIONS_PLUGIN_BASENAME;
if (isset($value->response[$plugin_file])) {
unset($value->response[$plugin_file]);
}
return $value;
});
// Activation hook
register_activation_hook(__FILE__, 'pc_community_suggestions_activate');
function pc_community_suggestions_activate() {
require_once PC_COMMUNITY_SUGGESTIONS_PLUGIN_DIR . 'includes/class-database.php';
PC_Community_Suggestions_Database::create_tables();
// Set default options
add_option('pc_community_suggestions_default_sort', 'popular');
add_option('pc_community_suggestions_page_id', '0');
add_option('pc_community_suggestions_page_created', false);
// Create default page immediately
pc_community_suggestions_create_default_page();
// Flush rewrite rules to ensure URL routing works
flush_rewrite_rules();
}
// Deactivation hook
register_deactivation_hook(__FILE__, 'pc_community_suggestions_deactivate');
function pc_community_suggestions_deactivate() {
// Cleanup on deactivation if needed
}
// Create default suggestions page
function pc_community_suggestions_create_default_page() {
// Check if page already exists
$existing_page = get_page_by_path('suggestions');
if ($existing_page) {
update_option('pc_community_suggestions_page_id', $existing_page->ID);
return;
}
$page_content = '<!-- wp:shortcode -->[community_suggestions]<!-- /wp:shortcode -->';
$page_data = array(
'post_title' => __('Community Suggestions', 'pc-community-suggestions-7d3f'),
'post_content' => $page_content,
'post_status' => 'publish',
'post_type' => 'page',
'post_name' => 'suggestions',
'post_author' => 1 // Set to admin user
);
$page_id = wp_insert_post($page_data);
if ($page_id && !is_wp_error($page_id)) {
update_option('pc_community_suggestions_page_id', $page_id);
// Flush rewrite rules to ensure the URL works immediately
flush_rewrite_rules();
// Also set this page as the option for our plugin
update_option('pc_community_suggestions_page_created', true);
}
}
// Add rewrite rule for /suggestions URL
function pc_community_suggestions_add_rewrite_rule() {
add_rewrite_rule(
'^suggestions/?$',
'index.php?pagename=suggestions',
'top'
);
}
add_action('init', 'pc_community_suggestions_add_rewrite_rule');
// Handle template redirect for suggestions page
function pc_community_suggestions_template_include($template) {
if (get_query_var('pagename') === 'suggestions') {
// Check if the page exists, if not create it
$page_id = get_option('pc_community_suggestions_page_id');
if (!$page_id || !get_post($page_id)) {
pc_community_suggestions_create_default_page();
}
// Use the page template if it exists
$page_template = locate_template(array('page-suggestions.php', 'page.php', 'singular.php'));
if ($page_template) {
return $page_template;
}
}
return $template;
}
add_filter('template_include', 'pc_community_suggestions_template_include');
// Query var for suggestions page
function pc_community_suggestions_query_vars($query_vars) {
$query_vars[] = 'pagename';
return $query_vars;
}
add_filter('query_vars', 'pc_community_suggestions_query_vars');
// Handle 404 for suggestions page and create it if needed
function pc_community_suggestions_handle_404() {
if (is_404()) {
$requested_url = $_SERVER['REQUEST_URI'];
// Check if this is a request for /suggestions/
if (preg_match('/^\/suggestions\/?$/', $requested_url) || strpos($requested_url, '/suggestions') !== false) {
// Check if page exists
$page_id = get_option('pc_community_suggestions_page_id');
if (!$page_id || !get_post($page_id)) {
// Create the page
pc_community_suggestions_create_default_page();
// Redirect to the newly created page
$suggestions_url = home_url('/suggestions/');
wp_redirect($suggestions_url);
exit;
}
}
}
}
add_action('template_redirect', 'pc_community_suggestions_handle_404', 1);
// Fallback handler - if no page is found for /suggestions, render our content
function pc_community_suggestions_fallback_handler() {
if (is_404()) {
$requested_path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
if ($requested_path === '/suggestions/' || $requested_path === '/suggestions') {
status_header(200);
// Render the suggestions content directly
$shortcode = new PC_Community_Suggestions_Shortcodes();
echo $shortcode->render_suggestions_page();
exit;
}
}
}
add_action('template_redirect', 'pc_community_suggestions_fallback_handler', 999);
// Load plugin classes
function pc_community_suggestions_init() {
// Include required files
require_once PC_COMMUNITY_SUGGESTIONS_PLUGIN_DIR . 'includes/class-database.php';
require_once PC_COMMUNITY_SUGGESTIONS_PLUGIN_DIR . 'includes/class-shortcodes.php';
require_once PC_COMMUNITY_SUGGESTIONS_PLUGIN_DIR . 'includes/class-rest-api.php';
// Initialize REST API early to ensure AJAX handlers are registered
$rest_api = new PC_Community_Suggestions_REST_API();
// Initialize shortcodes
new PC_Community_Suggestions_Shortcodes();
if (is_admin()) {
require_once PC_COMMUNITY_SUGGESTIONS_PLUGIN_DIR . 'admin/class-admin.php';
new PC_Community_Suggestions_Admin();
}
// Ensure suggestions page exists on every load (safety check)
$page_id = get_option('pc_community_suggestions_page_id');
if (!$page_id || !get_post($page_id)) {
pc_community_suggestions_create_default_page();
}
}
add_action('plugins_loaded', 'pc_community_suggestions_init', 5);
// Enqueue scripts and styles
function pc_community_suggestions_enqueue_scripts() {
// Frontend styles
wp_enqueue_style(
'pc-community-suggestions-frontend',
PC_COMMUNITY_SUGGESTIONS_PLUGIN_URL . 'public/css/public-style.css',
array(),
PC_COMMUNITY_SUGGESTIONS_VERSION
);
// Frontend scripts
wp_enqueue_script(
'pc-community-suggestions-frontend',
PC_COMMUNITY_SUGGESTIONS_PLUGIN_URL . 'public/js/public-script.js',
array('jquery'),
PC_COMMUNITY_SUGGESTIONS_VERSION,
true
);
// Localize script for AJAX
wp_localize_script('pc-community-suggestions-frontend', 'pcCommunitySuggestions', array(
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('pc_community_suggestions_nonce'),
'is_user_logged_in' => is_user_logged_in(),
'i18n' => array(
'fill_all_fields' => __('Please fill in all required fields.', 'pc-community-suggestions-7d3f'),
'title_too_long' => __('Title must be less than 255 characters.', 'pc-community-suggestions-7d3f'),
'login_to_vote' => __('You must be logged in to vote.', 'pc-community-suggestions-7d3f'),
'login_to_submit' => __('You must be logged in to submit suggestions.', 'pc-community-suggestions-7d3f'),
'ajax_error' => __('An error occurred. Please try again.', 'pc-community-suggestions-7d3f')
)
));
}
add_action('wp_enqueue_scripts', 'pc_community_suggestions_enqueue_scripts');
// Admin enqueue
function pc_community_suggestions_admin_enqueue_scripts($hook) {
if (strpos($hook, 'pc_community_suggestions') !== false) {
wp_enqueue_style(
'pc-community-suggestions-admin',
PC_COMMUNITY_SUGGESTIONS_PLUGIN_URL . 'admin/css/admin-style.css',
array(),
PC_COMMUNITY_SUGGESTIONS_VERSION
);
wp_enqueue_script(
'pc-community-suggestions-admin',
PC_COMMUNITY_SUGGESTIONS_PLUGIN_URL . 'admin/js/admin-script.js',
array('jquery'),
PC_COMMUNITY_SUGGESTIONS_VERSION,
true
);
// Localize admin script
wp_localize_script('pc-community-suggestions-admin', 'pcCommunitySuggestionsAdmin', array(
'nonce' => wp_create_nonce('pc_admin_nonce'),
'ajax_url' => admin_url('admin-ajax.php'),
'i18n' => array(
'confirm_bulk_action' => __('Are you sure you want to perform this action on the selected suggestions?', 'pc-community-suggestions-7d3f'),
'no_items_selected' => __('Please select at least one suggestion.', 'pc-community-suggestions-7d3f'),
'action_failed' => __('The action failed. Please try again.', 'pc-community-suggestions-7d3f'),
'loading' => __('Loading...', 'pc-community-suggestions-7d3f'),
'processing' => __('Processing...', 'pc-community-suggestions-7d3f')
)
));
}
}
add_action('admin_enqueue_scripts', 'pc_community_suggestions_admin_enqueue_scripts');
// Add plugin action links
function pc_community_suggestions_action_links($links) {
$settings_link = '<a href="' . admin_url('admin.php?page=pc_community_suggestions_settings') . '">' . __('Settings', 'pc-community-suggestions-7d3f') . '</a>';
array_unshift($links, $settings_link);
return $links;
}
add_filter('plugin_action_links_' . PC_COMMUNITY_SUGGESTIONS_PLUGIN_BASENAME, 'pc_community_suggestions_action_links');
// Load textdomain
function pc_community_suggestions_load_textdomain() {
load_plugin_textdomain('pc-community-suggestions-7d3f', false, dirname(PC_COMMUNITY_SUGGESTIONS_PLUGIN_BASENAME) . '/languages');
}
add_action('plugins_loaded', 'pc_community_suggestions_load_textdomain');
// Manual page creation trigger (for debugging)
function pc_community_suggestions_manual_create_page() {
if (isset($_GET['pc_create_suggestions_page']) && current_user_can('manage_options')) {
check_admin_referer('pc_create_page_nonce');
pc_community_suggestions_create_default_page();
flush_rewrite_rules();
$page_id = get_option('pc_community_suggestions_page_id');
$page_url = get_permalink($page_id);
wp_redirect(admin_url('admin.php?page=pc_community_suggestions_settings&created=1&url=' . urlencode($page_url)));
exit;
}
}
add_action('admin_init', 'pc_community_suggestions_manual_create_page');
// Note: Do not require the uninstall file here. WordPress will include `uninstall.php` when the plugin is uninstalled.
// The uninstall logic is self-contained in `uninstall.php` and should not run during normal plugin load.

View File

@@ -0,0 +1,752 @@
/* Community Suggestions Frontend Styles */
.pc-community-suggestions-container {
/* Palette (used throughout the plugin to avoid inheriting theme styles) */
--pc-primary: #2563eb; /* main blue */
--pc-primary-strong: #1e40af;
--pc-accent: #06b6d4;
--pc-success: #059669;
--pc-muted: #64748b;
--pc-text: #000000; /* make body text pure black to stand out */
--pc-heading: #000000; /* headings black for emphasis */
--pc-card-bg: #ffffff; /* keep cards white */
--pc-card-border: #e6f2fb;
--pc-input-bg: #ffffff;
--pc-card-shadow: 0 6px 20px rgba(15, 23, 42, 0.06);
--pc-radius: 12px;
max-width: 880px;
margin: 0 auto;
padding: 30px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
box-sizing: border-box;
}
/* Enforce plugin palette with higher specificity so theme defaults don't leak in */
.pc-community-suggestions-container .pc-button-primary {
background: var(--pc-primary) !important;
border-color: var(--pc-primary) !important;
color: #fff !important;
box-shadow: 0 6px 18px rgba(37,99,235,0.12);
}
.pc-community-suggestions-container .pc-button-primary:hover {
background: var(--pc-primary-strong) !important;
border-color: var(--pc-primary-strong) !important;
transform: translateY(-1px);
}
.pc-community-suggestions-container .pc-button-secondary {
background: #f1f5f9;
color: var(--pc-text);
border-color: #e2e8f0;
}
.pc-community-suggestions-container .pc-suggestion-card {
background: var(--pc-card-bg);
border: 1px solid var(--pc-card-border);
box-shadow: var(--pc-card-shadow);
border-radius: var(--pc-radius);
padding: 22px;
}
.pc-community-suggestions-container .pc-form-card {
background: var(--pc-card-bg) !important;
border: 1px solid var(--pc-card-border);
box-shadow: var(--pc-card-shadow);
border-radius: calc(var(--pc-radius) - 2px);
}
/* Inputs */
.pc-community-suggestions-container .pc-form-group input,
.pc-community-suggestions-container .pc-form-group textarea,
.pc-community-suggestions-container .pc-sort-dropdown select {
background: var(--pc-input-bg);
border-color: #e6eef8;
color: var(--pc-text);
}
/* Vote button */
.pc-community-suggestions-container .pc-vote-button {
border-color: var(--pc-success);
background: linear-gradient(180deg, #d1fae5 0%, #a7f3d0 100%);
box-shadow: 0 6px 12px rgba(5,150,105,0.12);
border-radius: 10px;
width: 56px;
height: 56px;
}
.pc-community-suggestions-container .pc-vote-button:hover:not(:disabled) {
transform: scale(1.04);
}
/* Headings and text */
.pc-community-suggestions-container .pc-suggestions-header h2 { color: var(--pc-heading); }
.pc-community-suggestions-container .pc-suggestion-title { color: var(--pc-heading); }
.pc-community-suggestions-container .pc-suggestion-body { color: var(--pc-text); }
/* Links */
.pc-community-suggestions-container .pc-back-link { color: var(--pc-primary); }
.pc-community-suggestions-container .pc-back-link:hover { color: var(--pc-primary-strong); }
/* Apply box-sizing to all elements */
.pc-community-suggestions-container *,
.pc-community-suggestions-container *::before,
.pc-community-suggestions-container *::after {
box-sizing: border-box;
}
.pc-suggestions-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
flex-wrap: wrap;
gap: 20px;
}
.pc-suggestions-header h2 {
font-size: 2em;
font-weight: 700;
color: #1e3a5f;
margin: 0;
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.1);
}
.pc-suggestions-controls {
display: flex;
gap: 15px;
align-items: center;
}
.pc-sort-dropdown select {
padding: 10px 15px;
border: 2px solid #8b5cf6;
border-radius: 8px;
background: #faf5ff;
font-size: 14px;
color: #1e0533;
cursor: pointer;
transition: all 0.2s ease-in-out;
}
.pc-sort-dropdown select:focus {
outline: none;
border-color: #7c3aed;
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.3);
}
/* Button styles */
.pc-button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 12px 24px;
border: 2px solid transparent;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
text-decoration: none;
cursor: pointer;
transition: all 0.2s ease-in-out;
min-height: 44px;
line-height: 1;
}
.pc-button-primary {
background: #3b82f6;
color: #fff;
border-color: #3b82f6;
}
.pc-button-primary:hover {
background: #2563eb;
border-color: #2563eb;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
.pc-button-primary:active {
transform: translateY(0);
box-shadow: 0 2px 6px rgba(34, 113, 177, 0.2);
}
/* Suggestion form */
.pc-suggestion-form {
margin-bottom: 30px;
animation: pc-slide-down 0.3s ease-out;
}
.pc-form-card {
background: linear-gradient(135deg, #ffffff 0%, #fef3c7 100%);
border: 2px solid #f59e0b;
border-radius: 12px;
padding: 25px;
box-shadow: 0 4px 15px rgba(245, 158, 11, 0.15);
}
.pc-form-card h3 {
margin: 0 0 20px 0;
font-size: 1.3em;
font-weight: 600;
color: #1e293b;
}
.pc-form-group {
margin-bottom: 20px;
}
.pc-form-group label {
display: block;
font-weight: 600;
margin-bottom: 8px;
color: #0f172a;
font-size: 14px;
}
.pc-form-group input,
.pc-form-group textarea {
width: 100%;
padding: 12px 16px;
border: 2px solid #e5e7eb;
border-radius: 8px;
font-size: 16px;
line-height: 1.5;
transition: all 0.3s ease-in-out;
background: #ffffff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.pc-form-group input:focus,
.pc-form-group textarea:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.15);
background: linear-gradient(135deg, #ffffff 0%, #eff6ff 100%);
}
.pc-form-group textarea {
min-height: 120px;
resize: vertical;
}
.pc-form-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
}
/* Suggestions list */
.pc-suggestions-list {
display: flex;
flex-direction: column;
gap: 20px;
}
.pc-no-suggestions {
text-align: center;
padding: 40px 20px;
color: #ef4444;
font-style: italic;
font-size: 1.1em;
}
/* Suggestion card */
.pc-suggestion-card {
display: flex;
gap: 20px;
background: var(--pc-card-bg) !important;
border: 1px solid var(--pc-card-border) !important;
border-radius: 16px;
padding: 20px;
box-shadow: var(--pc-card-shadow);
transition: all 0.3s ease-in-out;
position: relative;
}
.pc-suggestion-card:hover {
transform: translateY(-4px);
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.12);
border-color: var(--pc-card-border);
background: var(--pc-card-bg) !important; /* keep background white */
}
.pc-suggestion-card.pc-suggestion-hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
border-color: #c3c4c7;
}
.pc-suggestion-vote {
display: flex;
flex-direction: column;
align-items: center;
flex-shrink: 0;
}
.pc-vote-button {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 50px;
height: 50px;
border: 2px solid #10b981;
border-radius: 10px;
background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%);
cursor: pointer;
transition: all 0.3s ease-in-out;
padding: 0;
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.2);
}
.pc-vote-button:hover:not(:disabled) {
border-color: #059669;
background: linear-gradient(135deg, #a7f3d0 0%, #6ee7b7 100%);
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
}
.pc-vote-button.pc-voted {
background: linear-gradient(135deg, #059669 0%, #047857 100%);
border-color: #047857;
color: #ffffff;
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(5, 150, 105, 0.4);
}
.pc-vote-button:disabled {
cursor: not-allowed;
opacity: 0.6;
}
.pc-vote-button.pc-vote-disabled {
cursor: not-allowed;
opacity: 0.6;
}
.pc-vote-arrow {
font-size: 18px;
font-weight: bold;
margin-bottom: 2px;
}
.pc-vote-count {
font-size: 16px;
font-weight: 600;
color: #2c3338;
}
.pc-vote-button.pc-voted .pc-vote-count {
color: #2a6b2a;
}
.pc-suggestion-content {
flex: 1;
min-width: 0;
}
.pc-suggestion-title {
font-size: 1.2em;
font-weight: 600;
margin: 0 0 12px 0;
color: #000000 !important; /* enforce black heading */
line-height: 1.3;
}
.pc-suggestion-body {
margin-bottom: 16px;
color: #000000 !important; /* enforce black body text */
line-height: 1.6;
font-size: 1.05em;
}
.pc-suggestion-body p {
margin: 0 0 12px 0;
}
.pc-suggestion-body p:last-child {
margin-bottom: 0;
}
.pc-suggestion-meta {
display: flex;
gap: 16px;
font-size: 13px;
color: #64748b;
}
.pc-suggestion-author {
font-weight: 500;
}
.pc-suggestion-date {
font-style: italic;
}
.pc-suggestion-actions {
margin-bottom: 12px;
}
.pc-button-small {
padding: 8px 16px;
font-size: 13px;
min-height: auto;
}
.pc-back-link {
color: #3b82f6;
text-decoration: none;
font-weight: 500;
display: inline-flex;
align-items: center;
gap: 5px;
margin-bottom: 20px;
}
.pc-back-link:hover {
color: #2563eb;
text-decoration: underline;
}
.pc-suggestion-detail {
max-width: 100%;
}
.pc-suggestion-card.pc-suggestion-card-detail {
margin-bottom: 30px;
}
.pc-comments-section {
background: linear-gradient(135deg, #ffffff 0%, #faf5ff 100%);
border: 2px solid #a855f7;
border-radius: 16px;
padding: 25px;
margin-top: 30px;
box-shadow: 0 4px 15px rgba(168, 85, 247, 0.15);
}
.pc-comments-section h3 {
margin: 0 0 20px 0;
font-size: 1.2em;
font-weight: 600;
color: #1e293b;
}
.pc-comment-card {
border-left: 4px solid #a855f7;
padding-left: 15px;
margin-bottom: 20px;
background: linear-gradient(135deg, #faf5ff 0%, #f3e8ff 100%);
border-radius: 8px;
padding: 15px 15px 15px 20px;
box-shadow: 0 2px 8px rgba(168, 85, 247, 0.1);
}
.pc-comment-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
font-size: 14px;
}
.pc-comment-author {
font-weight: 600;
color: #2c3338;
}
.pc-comment-date {
color: #646970;
font-style: italic;
}
.pc-comment-content {
line-height: 1.6;
color: #2c3338;
}
.pc-comment-content p {
margin: 0 0 12px 0;
}
.pc-comment-content p:last-child {
margin-bottom: 0;
}
.pc-no-comments {
text-align: center;
color: #7c3aed;
font-style: italic;
padding: 20px;
background: linear-gradient(135deg, #faf5ff 0%, #f3e8ff 100%);
border-radius: 8px;
border: 2px solid #c4b5fd;
font-weight: 500;
}
.pc-char-counter {
font-size: 12px;
color: #646970;
margin-top: 5px;
text-align: right;
}
.pc-counter-exceeded {
color: #d63638;
font-weight: 500;
}
/* Login required */
.pc-community-suggestions-login-required {
max-width: 500px;
margin: 40px auto;
}
.pc-suggestions-card {
background: #fff;
border: 1px solid #dcdcde;
border-radius: 12px;
padding: 30px;
text-align: center;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
}
.pc-suggestions-card h3 {
font-size: 1.5em;
font-weight: 600;
margin: 0 0 15px 0;
color: #1d2327;
}
.pc-suggestions-card p {
margin: 0 0 25px 0;
color: #646970;
line-height: 1.5;
}
.pc-suggestions-actions {
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
}
/* Pagination */
.pc-suggestions-pagination {
margin-top: 30px;
text-align: center;
}
.pc-suggestions-pagination .page-numbers {
display: inline-flex;
gap: 8px;
flex-wrap: wrap;
justify-content: center;
}
/* Animations */
@keyframes pc-slide-down {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pc-fade-in-up {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.pc-suggestion-card {
animation: pc-fade-in-up 0.4s ease-out;
}
/* Responsive design */
@media screen and (max-width: 768px) {
.pc-community-suggestions-container {
padding: 15px;
}
.pc-suggestions-header {
flex-direction: column;
align-items: stretch;
text-align: center;
}
.pc-suggestions-header h2 {
font-size: 1.5em;
}
.pc-suggestions-controls {
justify-content: center;
flex-wrap: wrap;
}
.pc-suggestion-card {
flex-direction: column;
gap: 15px;
padding: 15px;
}
.pc-suggestion-vote {
flex-direction: row;
justify-content: center;
}
.pc-vote-button {
width: 60px;
height: 40px;
flex-direction: row;
gap: 5px;
}
.pc-form-card {
padding: 20px;
}
.pc-form-actions {
flex-direction: column;
}
.pc-suggestions-actions {
flex-direction: column;
align-items: center;
}
.pc-suggestion-meta {
flex-direction: column;
gap: 8px;
}
}
@media screen and (max-width: 480px) {
.pc-suggestions-header h2 {
font-size: 1.3em;
}
.pc-button {
padding: 10px 20px;
font-size: 13px;
}
.pc-form-group input,
.pc-form-group textarea {
font-size: 14px;
padding: 10px 14px;
}
.pc-suggestion-title {
font-size: 1.1em;
}
}
/* Dark mode support using the same variable palette (keeps styles consistent across themes) */
@media (prefers-color-scheme: dark) {
.pc-community-suggestions-container {
--pc-primary: #60a5fa;
--pc-primary-strong: #3b82f6;
--pc-accent: #0891b2;
--pc-success: #10b981;
--pc-muted: #9ca3af;
--pc-text: #e6eef8;
--pc-heading: #ffffff;
--pc-card-bg: #0b1220;
--pc-card-border: #374151;
--pc-input-bg: #0b1220;
--pc-card-shadow: 0 6px 20px rgba(2,6,23,0.6);
}
.pc-community-suggestions-container .pc-form-group input,
.pc-community-suggestions-container .pc-form-group textarea,
.pc-community-suggestions-container .pc-sort-dropdown select {
background: var(--pc-input-bg);
border-color: var(--pc-card-border);
color: var(--pc-text);
}
/* Force white cards and black text even in dark mode as requested */
.pc-community-suggestions-container .pc-form-card,
.pc-community-suggestions-container .pc-suggestion-card {
background: #ffffff !important;
color: #000000 !important;
border-color: #e6f2fb !important;
box-shadow: 0 6px 20px rgba(15, 23, 42, 0.06) !important;
}
.pc-community-suggestions-container .pc-suggestion-card:hover {
background: #ffffff !important;
border-color: #e6f2fb !important;
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.12) !important;
}
.pc-community-suggestions-container .pc-suggestion-title,
.pc-community-suggestions-container .pc-suggestion-body {
color: #000000 !important;
}
.pc-community-suggestions-container .pc-form-group input,
.pc-community-suggestions-container .pc-form-group textarea,
.pc-community-suggestions-container .pc-sort-dropdown select {
background: #ffffff !important;
color: #000000 !important;
border-color: #e6eef8 !important;
}
.pc-community-suggestions-container .pc-comment-card { border-left-color: var(--pc-accent); }
}
/* Accessibility improvements */
.pc-vote-button:focus {
outline: 2px solid #2271b1;
outline-offset: 2px;
}
.pc-button:focus,
.pc-form-group input:focus,
.pc-form-group textarea:focus,
.pc-sort-dropdown select:focus {
outline: 2px solid #2271b1;
outline-offset: 2px;
}
/* High contrast mode */
@media (prefers-contrast: high) {
.pc-suggestion-card {
border: 2px solid #000;
}
.pc-vote-button {
border: 2px solid #000;
}
.pc-button {
border: 2px solid #000;
}
}
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
.pc-suggestion-card,
.pc-suggestion-form,
.pc-button,
.pc-vote-button {
transition: none;
animation: none;
}
.pc-button:hover,
.pc-vote-button:hover {
transform: none;
}
}

View File

@@ -0,0 +1,265 @@
jQuery(document).ready(function($) {
// Toggle suggestion form
window.pcToggleSuggestionForm = function() {
const form = document.getElementById('pc-suggestion-form');
if (form.style.display === 'none') {
form.style.display = 'block';
form.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
} else {
form.style.display = 'none';
}
};
// Change sort method
window.pcChangeSort = function(sort) {
const url = new URL(window.location.href);
url.searchParams.set('sort', sort);
window.location.href = url.toString();
};
// Handle suggestion submission
$('#pc-new-suggestion-form').on('submit', function(e) {
e.preventDefault();
const form = $(this);
const submitBtn = form.find('button[type="submit"]');
const title = $('#pc-suggestion-title').val().trim();
const content = $('#pc-suggestion-content').val().trim();
// Basic validation
if (!title || !content) {
alert(pcCommunitySuggestions.i18n.fill_all_fields);
return;
}
if (title.length > 255) {
alert(pcCommunitySuggestions.i18n.title_too_long);
return;
}
submitBtn.prop('disabled', true).addClass('pc-loading');
$.ajax({
url: pcCommunitySuggestions.ajax_url,
type: 'POST',
data: {
action: 'pc_submit_suggestion',
title: title,
content: content,
nonce: pcCommunitySuggestions.nonce
},
success: function(response) {
console.log('AJAX Response:', response);
if (response.success) {
// Clear form and show success message
form[0].reset();
pcToggleSuggestionForm();
// Show success message
pcShowMessage(response.data.message, 'success');
// Reload page to show new suggestion
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
pcShowMessage(response.data.message, 'error');
}
},
error: function(xhr, status, error) {
console.error('AJAX Error:', status, error);
console.error('Response Text:', xhr.responseText);
console.error('Status Code:', xhr.status);
if (xhr.responseText) {
pcShowMessage('Error: ' + xhr.responseText.substring(0, 200) + '...', 'error');
} else {
pcShowMessage(pcCommunitySuggestions.i18n.ajax_error + ' (Status: ' + xhr.status + ')', 'error');
}
},
complete: function() {
submitBtn.prop('disabled', false).removeClass('pc-loading');
}
});
});
// Handle voting
window.pcVoteSuggestion = function(suggestionId) {
if (!pcCommunitySuggestions.is_user_logged_in) {
alert(pcCommunitySuggestions.i18n.login_to_vote);
return;
}
const voteBtn = $(`.pc-vote-button[onclick*="${suggestionId}"]`);
if (voteBtn.prop('disabled')) {
return;
}
voteBtn.prop('disabled', true).addClass('pc-loading');
$.ajax({
url: pcCommunitySuggestions.ajax_url,
type: 'POST',
data: {
action: 'pc_vote_suggestion',
suggestion_id: suggestionId,
nonce: pcCommunitySuggestions.nonce
},
success: function(response) {
if (response.success) {
// Update UI
const voteCount = voteBtn.find('.pc-vote-count');
const currentVotes = parseInt(voteCount.text());
voteCount.text(currentVotes + 1);
voteBtn.addClass('pc-voted');
voteBtn.find('.pc-vote-arrow').css('color', '#2a6b2a');
pcShowMessage(response.data.message, 'success');
} else {
pcShowMessage(response.data.message, 'error');
}
},
error: function() {
pcShowMessage(pcCommunitySuggestions.i18n.ajax_error, 'error');
},
complete: function() {
voteBtn.removeClass('pc-loading');
}
});
};
// Show message function
function pcShowMessage(message, type = 'info') {
// Remove existing messages
$('.pc-message').remove();
const messageClass = `pc-message pc-message-${type}`;
const messageHtml = `
<div class="${messageClass}">
<p>${message}</p>
<button class="pc-message-close" onclick="$(this).parent().fadeOut(300, function() { $(this).remove(); })">
&times;
</button>
</div>
`;
$('.pc-suggestions-header').after(messageHtml);
// Auto-hide after 5 seconds
setTimeout(() => {
$('.pc-message').fadeOut(300, function() {
$(this).remove();
});
}, 5000);
}
// Add message styles dynamically
$('<style>')
.text(`
.pc-message {
padding: 15px 20px;
margin: 20px 0;
border-radius: 6px;
border-left: 4px solid;
position: relative;
animation: pc-slide-in 0.3s ease-out;
}
.pc-message-success {
background: #edfaef;
border-color: #00a32a;
color: #2a6b2a;
}
.pc-message-error {
background: #fcf0f1;
border-color: #d63638;
color: #b32d2e;
}
.pc-message-info {
background: #f0f6ff;
border-color: #2271b1;
color: #135e96;
}
.pc-message-close {
position: absolute;
top: 10px;
right: 10px;
background: none;
border: none;
font-size: 18px;
cursor: pointer;
padding: 0;
width: 20px;
height: 20px;
line-height: 1;
}
@keyframes pc-slide-in {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
`)
.appendTo('head');
// Add loading class styles
$('<style>')
.text(`
.pc-loading {
opacity: 0.7;
pointer-events: none;
position: relative;
}
.pc-loading::after {
content: '';
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid #f3f3f3;
border-top: 2px solid #2271b1;
border-radius: 50%;
animation: pc-spin 1s linear infinite;
margin-left: 8px;
vertical-align: middle;
}
@keyframes pc-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`)
.appendTo('head');
// Handle enter key in form
$('#pc-suggestion-title, #pc-suggestion-content').on('keydown', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
$('#pc-new-suggestion-form').submit();
}
});
// Auto-resize textarea
$('#pc-suggestion-content').on('input', function() {
this.style.height = 'auto';
this.style.height = (this.scrollHeight) + 'px';
}).trigger('input');
// Focus on title field when form opens
$(document).on('click', '#pc-suggestion-form', function(e) {
if (e.target === this) {
$('#pc-suggestion-title').focus();
}
});
});

View File

@@ -0,0 +1,176 @@
#!/usr/bin/env php
<?php
/**
* Check for duplicate class declarations in WordPress plugin
* Detects duplicate classes, interfaces, traits, functions, and constants
*/
$plugin_dir = $argv[1] ?? __DIR__;
echo "=== Checking for Duplicates ===\n\n";
function scan_directory($dir, $extensions = ['php']) {
$files = [];
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
if (in_array(strtolower($file->getExtension()), $extensions)) {
$files[] = $file->getPathname();
}
}
return $files;
}
function extract_declarations($content) {
$declarations = [];
// Extract classes
if (preg_match_all('/^\s*(?:abstract\s+)?class\s+(\w+)/m', $content, $matches)) {
foreach ($matches[1] as $class) {
$declarations['classes'][$class][] = 'class';
}
}
// Extract interfaces
if (preg_match_all('/^\s*interface\s+(\w+)/m', $content, $matches)) {
foreach ($matches[1] as $interface) {
$declarations['interfaces'][$interface][] = 'interface';
}
}
// Extract traits
if (preg_match_all('/^\s*trait\s+(\w+)/m', $content, $matches)) {
foreach ($matches[1] as $trait) {
$declarations['traits'][$trait][] = 'trait';
}
}
// Extract functions (non-namespaced)
if (preg_match_all('/^\s*function\s+(\w+)/m', $content, $matches)) {
foreach ($matches[1] as $function) {
if (!isset($declarations['functions'][$function])) {
$declarations['functions'][$function] = [];
}
}
}
return $declarations;
}
function check_duplicate_classes($files) {
$all_classes = [];
$duplicates = [];
foreach ($files as $file) {
$content = file_get_contents($file);
// Check for classes
if (preg_match_all('/^\s*(?:abstract\s+)?class\s+(\w+)/m', $content, $matches)) {
foreach ($matches[1] as $class) {
if (!isset($all_classes[$class])) {
$all_classes[$class] = [];
}
$all_classes[$class][] = $file;
}
}
// Check for interfaces
if (preg_match_all('/^\s*interface\s+(\w+)/m', $content, $matches)) {
foreach ($matches[1] as $interface) {
$key = strtolower($interface);
if (!isset($all_classes[$key])) {
$all_classes[$key] = [];
}
$all_classes[$key][] = $file;
}
}
}
foreach ($all_classes as $name => $files_list) {
if (count($files_list) > 1) {
$duplicates['classes'][$name] = $files_list;
}
}
return $duplicates;
}
function check_duplicate_functions($files) {
$all_functions = [];
$duplicates = [];
foreach ($files as $file) {
$content = file_get_contents($file);
// Skip content inside classes, interfaces, and traits
$content = preg_replace('/class\s+\w+\s*\{[^}]*\}/s', '', $content);
$content = preg_replace('/interface\s+\w+\s*\{[^}]*\}/s', '', $content);
$content = preg_replace('/trait\s+\w+\s*\{[^}]*\}/s', '', $content);
if (preg_match_all('/^\s*function\s+(\w+)\s*\(/m', $content, $matches)) {
foreach ($matches[1] as $function) {
if (!isset($all_functions[$function])) {
$all_functions[$function] = [];
}
$all_functions[$function][] = $file;
}
}
}
foreach ($all_functions as $name => $files_list) {
if (count($files_list) > 1) {
$duplicates['functions'][$name] = $files_list;
}
}
return $duplicates;
}
echo "Scanning directory: $plugin_dir\n";
$files = scan_directory($plugin_dir);
echo "Found " . count($files) . " PHP files\n\n";
$class_duplicates = check_duplicate_classes($files);
$function_duplicates = check_duplicate_functions($files);
$has_issues = false;
if (!empty($class_duplicates['classes'])) {
$has_issues = true;
echo "❌ Duplicate class/interface declarations found:\n";
foreach ($class_duplicates['classes'] as $name => $files_list) {
echo " - $name found in:\n";
foreach ($files_list as $file) {
echo " $file\n";
}
}
echo "\n";
}
if (!empty($function_duplicates['functions'])) {
$has_issues = true;
echo "⚠️ Potential duplicate function declarations:\n";
foreach ($function_duplicates['functions'] as $name => $files_list) {
echo " - $name() found in:\n";
foreach ($files_list as $file) {
echo " $file\n";
}
}
echo "\n";
}
if (!$has_issues) {
echo "✅ No duplicate declarations found\n";
} else {
echo "⚠️ Please review the duplicates above and fix them.\n";
echo " Solutions:\n";
echo " - Use class_exists() guards around class definitions\n";
echo " - Consolidate duplicate declarations into one file\n";
echo " - Namespace conflicting classes differently\n";
}
echo "\n=== Check Complete ===\n";
exit($has_issues ? 1 : 0);

View File

@@ -0,0 +1,195 @@
#!/bin/bash
# WordPress Plugin Validation Script
# Validates plugin structure, syntax, and WordPress coding standards
PLUGIN_DIR="$(pwd)"
PLUGIN_SLUG="pc-community-suggestions-7d3f"
MAIN_FILE="$PLUGIN_SLUG.php"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
echo -e "${BLUE}=== WordPress Plugin Validation ===${NC}"
echo -e "Plugin: $PLUGIN_SLUG"
echo -e "Directory: $PLUGIN_DIR"
echo
# Check if main plugin file exists
if [[ ! -f "$MAIN_FILE" ]]; then
echo -e "${RED}❌ ERROR: Main plugin file $MAIN_FILE not found${NC}"
exit 1
fi
echo -e "${GREEN}✅ Main plugin file found${NC}"
# Check plugin header
echo -e "${BLUE}Checking plugin header...${NC}"
if ! grep -q "Plugin Name:" "$MAIN_FILE"; then
echo -e "${RED}❌ ERROR: Plugin header missing 'Plugin Name:'${NC}"
exit 1
fi
if ! grep -q "Plugin URI:" "$MAIN_FILE"; then
echo -e "${YELLOW}⚠️ WARNING: Plugin header missing 'Plugin URI:'${NC}"
fi
if ! grep -q "Text Domain:" "$MAIN_FILE"; then
echo -e "${RED}❌ ERROR: Plugin header missing 'Text Domain:'${NC}"
exit 1
fi
if ! grep -q "Update URI:" "$MAIN_FILE"; then
echo -e "${YELLOW}⚠️ WARNING: Plugin header missing 'Update URI:'${NC}"
fi
echo -e "${GREEN}✅ Plugin header check passed${NC}"
# Check required directories
echo -e "${BLUE}Checking directory structure...${NC}"
REQUIRED_DIRS=("includes" "admin" "admin/css" "admin/js" "public" "public/css" "public/js" "assets")
for dir in "${REQUIRED_DIRS[@]}"; do
if [[ ! -d "$dir" ]]; then
echo -e "${YELLOW}⚠️ WARNING: Directory $dir not found${NC}"
else
echo -e "${GREEN}✅ Directory $dir found${NC}"
fi
done
# Check required files
echo -e "${BLUE}Checking required files...${NC}"
REQUIRED_FILES=(
"includes/class-database.php"
"includes/class-shortcodes.php"
"includes/class-rest-api.php"
"admin/class-admin.php"
"admin/css/admin-style.css"
"public/css/public-style.css"
"public/js/public-script.js"
"admin/js/admin-script.js"
"uninstall.php"
)
for file in "${REQUIRED_FILES[@]}"; do
if [[ ! -f "$file" ]]; then
echo -e "${YELLOW}⚠️ WARNING: File $file not found${NC}"
else
echo -e "${GREEN}✅ File $file found${NC}"
fi
done
# PHP syntax check
echo -e "${BLUE}Running PHP syntax checks...${NC}"
PHP_FILES=$(find . -name "*.php" -type f)
SYNTAX_ERRORS=0
for php_file in $PHP_FILES; do
if php -l "$php_file" > /dev/null 2>&1; then
echo -e "${GREEN}✅ Syntax OK: $php_file${NC}"
else
echo -e "${RED}❌ Syntax ERROR: $php_file${NC}"
php -l "$php_file"
SYNTAX_ERRORS=$((SYNTAX_ERRORS + 1))
fi
done
if [[ $SYNTAX_ERRORS -gt 0 ]]; then
echo -e "${RED}❌ Found $SYNTAX_ERRORS PHP syntax errors${NC}"
exit 1
else
echo -e "${GREEN}✅ All PHP files passed syntax check${NC}"
fi
# Check for WordPress functions
echo -e "${BLUE}Checking WordPress function usage...${NC}"
if ! grep -q "add_action\|add_filter" "$MAIN_FILE"; then
echo -e "${YELLOW}⚠️ WARNING: No WordPress hooks found in main file${NC}"
fi
# Check for proper enqueueing
echo -e "${BLUE}Checking script enqueueing...${NC}"
if grep -q "wp_enqueue_style\|wp_enqueue_script" "$MAIN_FILE"; then
echo -e "${GREEN}✅ Script enqueueing functions found${NC}"
else
echo -e "${YELLOW}⚠️ WARNING: No script enqueueing functions found${NC}"
fi
# Check for security measures
echo -e "${BLUE}Checking security measures...${NC}"
if grep -q "check_admin_referer\|wp_verify_nonce" "$MAIN_FILE" || grep -q "check_admin_referer\|wp_verify_nonce" includes/*.php; then
echo -e "${GREEN}✅ Security nonce checks found${NC}"
else
echo -e "${YELLOW}⚠️ WARNING: No security nonce checks found${NC}"
fi
if grep -q "current_user_can\|is_user_logged_in" "$MAIN_FILE" || grep -q "current_user_can\|is_user_logged_in" includes/*.php; then
echo -e "${GREEN}✅ User capability checks found${NC}"
else
echo -e "${YELLOW}⚠️ WARNING: No user capability checks found${NC}"
fi
# Check for database operations
echo -e "${BLUE}Checking database operations...${NC}"
if grep -q "\$wpdb->" includes/class-database.php; then
echo -e "${GREEN}✅ Database operations found${NC}"
else
echo -e "${YELLOW}⚠️ WARNING: No database operations found${NC}"
fi
# Check for activation/deactivation hooks
echo -e "${BLUE}Checking activation hooks...${NC}"
if grep -q "register_activation_hook\|register_deactivation_hook" "$MAIN_FILE"; then
echo -e "${GREEN}✅ Activation/deactivation hooks found${NC}"
else
echo -e "${YELLOW}⚠️ WARNING: No activation/deactivation hooks found${NC}"
fi
# Check for shortcodes
echo -e "${BLUE}Checking shortcodes...${NC}"
if grep -q "add_shortcode" includes/class-shortcodes.php; then
echo -e "${GREEN}✅ Shortcode registration found${NC}"
else
echo -e "${YELLOW}⚠️ WARNING: No shortcode registration found${NC}"
fi
# Check for REST API
echo -e "${BLUE}Checking REST API...${NC}"
if grep -q "register_rest_route" includes/class-rest-api.php; then
echo -e "${GREEN}✅ REST API routes found${NC}"
else
echo -e "${YELLOW}⚠️ WARNING: No REST API routes found${NC}"
fi
# Check for admin menu
echo -e "${BLUE}Checking admin menu...${NC}"
if grep -q "add_menu_page\|add_submenu_page" admin/class-admin.php; then
echo -e "${GREEN}✅ Admin menu registration found${NC}"
else
echo -e "${YELLOW}⚠️ WARNING: No admin menu registration found${NC}"
fi
# Final summary
echo
echo -e "${BLUE}=== Validation Summary ===${NC}"
echo -e "${GREEN}✅ Plugin structure looks good${NC}"
echo -e "${GREEN}✅ All PHP files passed syntax check${NC}"
echo -e "${GREEN}✅ WordPress integration features found${NC}"
echo -e "${GREEN}✅ Security measures implemented${NC}"
echo
echo -e "${GREEN}🎉 Plugin validation completed successfully!${NC}"
echo -e "${BLUE}The plugin is ready for WordPress installation.${NC}"
# Display plugin information
echo
echo -e "${BLUE}=== Plugin Information ===${NC}"
grep "Plugin Name:" "$MAIN_FILE" | head -1
grep "Version:" "$MAIN_FILE" | head -1
grep "Author:" "$MAIN_FILE" | head -1
grep "Text Domain:" "$MAIN_FILE" | head -1
exit 0

View File

@@ -0,0 +1,44 @@
<?php
/**
* Uninstall Community Suggestions plugin
*
* @package Community Suggestions
*/
// Prevent direct access
if (!defined('WP_UNINSTALL_PLUGIN')) {
exit;
}
// Remove database tables
function pc_community_suggestions_uninstall() {
global $wpdb;
$suggestions_table = $wpdb->prefix . 'pc_community_suggestions';
$votes_table = $wpdb->prefix . 'pc_community_votes';
// Drop tables
$wpdb->query("DROP TABLE IF EXISTS $suggestions_table");
$wpdb->query("DROP TABLE IF EXISTS $votes_table");
// Remove options
delete_option('pc_community_suggestions_moderation');
delete_option('pc_community_suggestions_default_sort');
delete_option('pc_community_suggestions_page_id');
delete_option('pc_community_suggestions_version');
// Remove any transients
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '%pc_community_suggestions_%'");
// Remove scheduled events
wp_clear_scheduled_hook('pc_community_suggestions_daily_cleanup');
// Remove the default page if it exists
$page_id = get_option('pc_community_suggestions_page_id', 0);
if ($page_id && get_post_type($page_id) === 'page') {
wp_delete_post($page_id, true);
}
}
// Run uninstall function
pc_community_suggestions_uninstall();

View File

@@ -0,0 +1,104 @@
# FAQ Manager Plugin
A comprehensive WordPress plugin that allows administrators to easily create and manage FAQ pages with drag-and-drop reordering functionality.
## Features
- **Easy FAQ Management**: Add, edit, and delete FAQs from a user-friendly admin interface
- **Drag-and-Drop Reordering**: Intuitive FAQ ordering with visual feedback
- **Automatic Page Creation**: Automatically creates an FAQ page upon plugin activation
- **Responsive Design**: Mobile-friendly frontend FAQ display with modern styling
- **Accessibility**: WCAG 2.1 AA compliant with keyboard navigation and screen reader support
- **Dark Mode Support**: Automatic styling for dark mode preferences
- **Clean Uninstallation**: Complete data removal when plugin is uninstalled
## Installation
1. Upload the `pc-faq-manager-abc123` folder to your WordPress plugins directory
2. Activate the plugin through the "Plugins" menu in WordPress
3. Access "FAQ Manager" from your WordPress admin menu to start adding FAQs
4. The FAQ page will be automatically created at `/faq/`
## Usage
### For Administrators
1. **Adding FAQs**: Navigate to FAQ Manager → Add New FAQ, enter your question and answer, and click "Create FAQ"
2. **Editing FAQs**: From the main FAQ Manager page, click "Edit" on any FAQ to modify it inline
3. **Reordering FAQs**: Drag and drop FAQs to reorder them, then click "Save Order"
4. **Deleting FAQs**: Click "Delete" on any FAQ to remove it permanently
### For Website Visitors
- Visit `/faq/` on your website to view all FAQs
- Click on any question to expand/collapse the answer
- Use keyboard navigation (arrow keys) to browse FAQs
- Print-friendly format is available
## Files Structure
```
pc-faq-manager-abc123/
├── pc-faq-manager-abc123.php # Main plugin file
├── uninstall.php # Cleanup on uninstall
├── includes/
│ ├── class-pc-faq-manager-helper.php
│ └── class-pc-faq-manager-post-type.php
├── admin/
│ ├── class-pc-faq-manager-admin.php
│ ├── css/admin-style.css
│ ├── js/admin-script.js
│ └── templates/
│ ├── main-page.php
│ └── add-page.php
├── public/
│ ├── class-pc-faq-manager-public.php
│ ├── css/public-style.css
│ └── js/public-script.js
└── assets/ # For future media files
```
## Security Features
- Nonce verification for all AJAX requests
- Capability checks to ensure only authorized users can manage FAQs
- Input sanitization and output escaping
- SQL query protection using WordPress $wpdb prepare
- XSS prevention with proper data filtering
## Compatibility
- WordPress 5.0+
- PHP 7.4+
- Tested with popular themes and plugins
- Compatible with WordPress block editor (Gutenberg)
## Customization
### Styling
The plugin uses CSS custom properties that can be overridden in your theme:
```css
:root {
--pc-faq-primary-color: #your-color;
--pc-faq-secondary-color: #your-bg-color;
/* ... other variables */
}
```
### Shortcode
Use the `[pc_faq_page]` shortcode in any page or post to display FAQs:
```html
[pc_faq_page title="Custom Title" show_count="false"]
```
## Support
For support and feature requests, please visit: https://plugincompass.com/plugins/pc-faq-manager-abc123
## License
This plugin is licensed under the GPLv2 or later license.

View File

@@ -0,0 +1,221 @@
<?php
/**
* FAQ Manager Admin Interface
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
class PC_FAQ_Manager_Admin {
/**
* Constructor
*/
public function __construct() {
add_action('admin_menu', array($this, 'add_admin_menu'));
add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts'));
add_action('wp_ajax_pc_faq_reorder', array($this, 'handle_ajax_reorder'));
add_action('wp_ajax_pc_faq_add_new', array($this, 'handle_ajax_add_new'));
add_action('wp_ajax_pc_faq_edit', array($this, 'handle_ajax_edit'));
add_action('wp_ajax_pc_faq_delete', array($this, 'handle_ajax_delete'));
}
/**
* Add admin menu
*/
public function add_admin_menu() {
add_menu_page(
__('FAQ Manager', PC_FAQ_MANAGER_SLUG),
__('FAQ', PC_FAQ_MANAGER_SLUG),
'manage_options',
'pc-faq-manager',
array($this, 'render_main_page'),
'dashicons-editor-help',
25
);
add_submenu_page(
'pc-faq-manager',
__('All FAQs', PC_FAQ_MANAGER_SLUG),
__('All FAQs', PC_FAQ_MANAGER_SLUG),
'manage_options',
'pc-faq-manager',
array($this, 'render_main_page')
);
add_submenu_page(
'pc-faq-manager',
__('Add New FAQ', PC_FAQ_MANAGER_SLUG),
__('Add New', PC_FAQ_MANAGER_SLUG),
'manage_options',
'pc-faq-manager-add',
array($this, 'render_add_page')
);
}
/**
* Enqueue admin scripts and styles
*/
public function enqueue_admin_scripts($hook) {
if (strpos($hook, 'pc-faq-manager') !== false) {
wp_enqueue_style('pc-faq-manager-admin', PC_FAQ_MANAGER_PLUGIN_URL . 'admin/css/admin-style.css', array(), PC_FAQ_MANAGER_VERSION);
wp_enqueue_script('jquery-ui-sortable');
wp_enqueue_script('pc-faq-manager-admin', PC_FAQ_MANAGER_PLUGIN_URL . 'admin/js/admin-script.js', array('jquery', 'jquery-ui-sortable'), PC_FAQ_MANAGER_VERSION, true);
wp_localize_script('pc-faq-manager-admin', 'pc_faq_manager', array(
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('pc_faq_manager_nonce'),
'confirm_delete' => __('Are you sure you want to delete this FAQ?', PC_FAQ_MANAGER_SLUG),
'saving' => __('Saving...', PC_FAQ_MANAGER_SLUG),
'saved' => __('Saved!', PC_FAQ_MANAGER_SLUG),
'error' => __('Error occurred. Please try again.', PC_FAQ_MANAGER_SLUG),
));
}
}
/**
* Render main admin page
*/
public function render_main_page() {
if (!PC_FAQ_Manager_Helper::can_manage_faqs()) {
wp_die(__('You do not have sufficient permissions to access this page.', PC_FAQ_MANAGER_SLUG));
}
$faqs = PC_FAQ_Manager_Helper::get_faqs();
$faq_count = PC_FAQ_Manager_Helper::get_faq_count();
include PC_FAQ_MANAGER_PLUGIN_DIR . 'admin/templates/main-page.php';
}
/**
* Render add new FAQ page
*/
public function render_add_page() {
if (!PC_FAQ_Manager_Helper::can_manage_faqs()) {
wp_die(__('You do not have sufficient permissions to access this page.', PC_FAQ_MANAGER_SLUG));
}
include PC_FAQ_MANAGER_PLUGIN_DIR . 'admin/templates/add-page.php';
}
/**
* Handle AJAX reorder request
*/
public function handle_ajax_reorder() {
check_ajax_referer('pc_faq_manager_nonce', 'nonce');
if (!PC_FAQ_Manager_Helper::can_manage_faqs()) {
wp_die(__('Permission denied.', PC_FAQ_MANAGER_SLUG));
}
if (isset($_POST['faq_ids']) && is_array($_POST['faq_ids'])) {
$result = PC_FAQ_Manager_Helper::reorder_faqs($_POST['faq_ids']);
if ($result) {
wp_send_json_success(array('message' => __('FAQs reordered successfully.', PC_FAQ_MANAGER_SLUG)));
} else {
wp_send_json_error(array('message' => __('Failed to reorder FAQs.', PC_FAQ_MANAGER_SLUG)));
}
}
wp_send_json_error(array('message' => __('Invalid request.', PC_FAQ_MANAGER_SLUG)));
}
/**
* Handle AJAX add new FAQ
*/
public function handle_ajax_add_new() {
check_ajax_referer('pc_faq_manager_nonce', 'nonce');
if (!PC_FAQ_Manager_Helper::can_manage_faqs()) {
wp_die(__('Permission denied.', PC_FAQ_MANAGER_SLUG));
}
$title = sanitize_text_field($_POST['title'] ?? '');
$content = wp_kses_post($_POST['content'] ?? '');
if (empty($title) || empty($content)) {
wp_send_json_error(array('message' => __('Title and content are required.', PC_FAQ_MANAGER_SLUG)));
}
$faq_data = array(
'post_title' => $title,
'post_content' => $content,
'post_status' => 'publish',
'post_type' => 'pc_faq',
);
$faq_id = wp_insert_post($faq_data);
if ($faq_id && !is_wp_error($faq_id)) {
$max_order = PC_FAQ_Manager_Helper::get_max_order();
update_post_meta($faq_id, '_pc_faq_order', $max_order + 1);
wp_send_json_success(array(
'message' => __('FAQ added successfully.', PC_FAQ_MANAGER_SLUG),
'faq_id' => $faq_id,
));
} else {
wp_send_json_error(array('message' => __('Failed to add FAQ.', PC_FAQ_MANAGER_SLUG)));
}
}
/**
* Handle AJAX edit FAQ
*/
public function handle_ajax_edit() {
check_ajax_referer('pc_faq_manager_nonce', 'nonce');
if (!PC_FAQ_Manager_Helper::can_manage_faqs()) {
wp_die(__('Permission denied.', PC_FAQ_MANAGER_SLUG));
}
$faq_id = intval($_POST['faq_id'] ?? 0);
$title = sanitize_text_field($_POST['title'] ?? '');
$content = wp_kses_post($_POST['content'] ?? '');
if (!$faq_id || empty($title) || empty($content)) {
wp_send_json_error(array('message' => __('Invalid data provided.', PC_FAQ_MANAGER_SLUG)));
}
$faq_data = array(
'ID' => $faq_id,
'post_title' => $title,
'post_content' => $content,
);
$result = wp_update_post($faq_data);
if ($result && !is_wp_error($result)) {
wp_send_json_success(array('message' => __('FAQ updated successfully.', PC_FAQ_MANAGER_SLUG)));
} else {
wp_send_json_error(array('message' => __('Failed to update FAQ.', PC_FAQ_MANAGER_SLUG)));
}
}
/**
* Handle AJAX delete FAQ
*/
public function handle_ajax_delete() {
check_ajax_referer('pc_faq_manager_nonce', 'nonce');
if (!PC_FAQ_Manager_Helper::can_manage_faqs()) {
wp_die(__('Permission denied.', PC_FAQ_MANAGER_SLUG));
}
$faq_id = intval($_POST['faq_id'] ?? 0);
if (!$faq_id) {
wp_send_json_error(array('message' => __('Invalid FAQ ID.', PC_FAQ_MANAGER_SLUG)));
}
$result = wp_delete_post($faq_id, true);
if ($result) {
wp_send_json_success(array('message' => __('FAQ deleted successfully.', PC_FAQ_MANAGER_SLUG)));
} else {
wp_send_json_error(array('message' => __('Failed to delete FAQ.', PC_FAQ_MANAGER_SLUG)));
}
}
}

View File

@@ -0,0 +1,439 @@
/* FAQ Manager Admin Styles */
.pc-faq-manager-admin {
max-width: 1200px;
margin: 0 auto;
}
/* Header Section */
.pc-faq-manager-header {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 20px;
margin: 20px 0;
padding: 20px;
background: #f9f9f9;
border-radius: 8px;
border: 1px solid #e5e5e5;
}
.pc-faq-manager-stats .stat-card {
background: #fff;
padding: 20px;
border-radius: 6px;
border: 1px solid #ddd;
text-align: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.stat-number {
font-size: 2.5em;
font-weight: 600;
color: #0073aa;
line-height: 1;
margin-bottom: 5px;
}
.stat-label {
color: #666;
font-size: 0.9em;
}
.pc-faq-manager-instructions h3 {
margin-top: 0;
margin-bottom: 10px;
color: #23282d;
}
.pc-faq-manager-instructions ul {
margin: 0;
padding-left: 20px;
}
.pc-faq-manager-instructions li {
margin-bottom: 5px;
color: #444;
}
/* FAQ List */
.pc-faq-manager-list {
background: #fff;
border: 1px solid #e5e5e5;
border-radius: 6px;
overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
}
.pc-faq-manager-list-header {
display: grid;
grid-template-columns: 40px 1fr 1fr 150px;
background: #fcfcfc;
border-bottom: 1px solid #e5e5e5;
padding: 12px 16px;
font-weight: 600;
color: #23282d;
}
.pc-faq-manager-items {
max-height: 600px;
overflow-y: auto;
}
.pc-faq-item {
display: grid;
grid-template-columns: 40px 1fr 1fr 150px;
padding: 16px;
border-bottom: 1px solid #f0f0f0;
align-items: start;
transition: background-color 0.2s ease;
}
.pc-faq-item:hover {
background-color: #f9f9f9;
}
.pc-faq-item:last-child {
border-bottom: none;
}
.sort-handle {
display: flex;
align-items: center;
justify-content: center;
cursor: move;
color: #666;
font-size: 14px;
}
.sort-handle:hover {
color: #0073aa;
}
.faq-content h4 {
margin: 0 0 5px 0;
font-size: 14px;
font-weight: 600;
color: #23282d;
line-height: 1.3;
}
.faq-excerpt {
font-size: 12px;
color: #666;
line-height: 1.4;
}
.faq-content {
color: #444;
font-size: 13px;
line-height: 1.4;
}
.faq-actions {
display: flex;
flex-direction: column;
gap: 8px;
}
.faq-actions .button {
padding: 6px 12px;
font-size: 12px;
line-height: 1.4;
height: auto;
white-space: nowrap;
}
.faq-actions .dashicons {
font-size: 14px;
margin-right: 4px;
vertical-align: middle;
}
/* Draggable state */
.pc-faq-manager-items.ui-sortable .pc-faq-item {
cursor: move;
}
.pc-faq-manager-items.ui-sortable-helper {
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
opacity: 0.9;
}
.pc-faq-manager-items.ui-sortable-placeholder {
background: #f0f6fc;
border: 2px dashed #0073aa;
margin: 4px 16px;
border-radius: 4px;
}
/* Footer */
.pc-faq-manager-footer {
display: flex;
align-items: center;
gap: 15px;
margin-top: 20px;
padding: 15px 0;
}
.pc-faq-save-status {
font-size: 13px;
color: #666;
}
.pc-faq-save-status.saving {
color: #f0ad4e;
}
.pc-faq-save-status.saved {
color: #5cb85c;
}
.pc-faq-save-status.error {
color: #d9534f;
}
/* Empty State */
.pc-faq-manager-empty {
text-align: center;
padding: 60px 20px;
background: #fff;
border: 1px solid #e5e5e5;
border-radius: 6px;
}
.pc-faq-manager-empty-icon {
font-size: 48px;
color: #666;
margin-bottom: 20px;
}
.pc-faq-manager-empty h2 {
margin: 0 0 15px 0;
color: #23282d;
}
.pc-faq-manager-empty p {
margin: 0 0 20px 0;
color: #666;
font-size: 14px;
}
/* Add Form */
.pc-faq-manager-add-form {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 30px;
}
.pc-faq-form-container {
background: #fff;
padding: 25px;
border: 1px solid #e5e5e5;
border-radius: 6px;
}
.pc-faq-form-container h2 {
margin-top: 0;
margin-bottom: 25px;
color: #23282d;
}
.form-field {
margin-bottom: 20px;
}
.form-field label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #23282d;
}
.form-field input[type="text"],
.form-field textarea {
width: 100%;
}
.form-field .description {
margin-top: 8px;
font-size: 12px;
color: #666;
font-style: italic;
}
.wp-editor-container {
border: 1px solid #ddd;
}
/* Help Section */
.pc-faq-manager-help {
display: flex;
flex-direction: column;
gap: 20px;
}
.help-card {
background: #fff;
padding: 20px;
border: 1px solid #e5e5e5;
border-radius: 6px;
}
.help-card h3 {
margin-top: 0;
margin-bottom: 15px;
color: #23282d;
}
.help-card ul {
margin: 0;
padding-left: 20px;
}
.help-card li {
margin-bottom: 8px;
color: #444;
font-size: 14px;
}
.help-card code {
background: #f5f5f5;
padding: 4px 8px;
border-radius: 3px;
font-family: 'Courier New', monospace;
font-size: 13px;
}
/* Modal */
.pc-faq-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 100000;
}
.pc-faq-modal-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 99999;
}
.pc-faq-modal-content {
position: relative;
background: #fff;
margin: 50px auto;
max-width: 700px;
max-height: 80vh;
overflow-y: auto;
border-radius: 8px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
z-index: 100001;
}
.pc-faq-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 25px;
border-bottom: 1px solid #e5e5e5;
background: #fcfcfc;
border-radius: 8px 8px 0 0;
}
.pc-faq-modal-header h2 {
margin: 0;
color: #23282d;
}
.pc-faq-modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
padding: 0;
line-height: 1;
}
.pc-faq-modal-close:hover {
color: #d9534f;
}
.pc-faq-modal-body {
padding: 25px;
}
.pc-faq-modal-footer {
display: flex;
justify-content: flex-end;
gap: 15px;
padding: 20px 25px;
border-top: 1px solid #e5e5e5;
background: #fcfcfc;
border-radius: 0 0 8px 8px;
}
/* Notice styling */
.notice.is-dismissible {
margin-top: 20px;
}
/* Responsive Design */
@media screen and (max-width: 1200px) {
.pc-faq-manager-admin {
max-width: 100%;
}
}
@media screen and (max-width: 980px) {
.pc-faq-manager-header {
grid-template-columns: 1fr;
}
.pc-faq-manager-add-form {
grid-template-columns: 1fr;
}
.pc-faq-manager-list-header,
.pc-faq-item {
grid-template-columns: 40px 1fr 80px;
}
.faq-answer {
display: none;
}
}
@media screen and (max-width: 600px) {
.pc-faq-manager-list-header,
.pc-faq-item {
grid-template-columns: 30px 1fr;
}
.faq-actions {
grid-column: 1 / -1;
flex-direction: row;
margin-top: 10px;
}
.sort-handle {
font-size: 12px;
}
.pc-faq-modal-content {
margin: 10px;
max-height: 90vh;
}
.pc-faq-modal-header,
.pc-faq-modal-body,
.pc-faq-modal-footer {
padding: 15px 20px;
}
}

View File

@@ -0,0 +1,250 @@
jQuery(document).ready(function($) {
// Sortable functionality
$('#pc-faq-sortable').sortable({
handle: '.sort-handle',
placeholder: 'pc-faq-sortable-placeholder',
helper: 'clone',
opacity: 0.9,
cursor: 'move',
tolerance: 'pointer',
start: function(event, ui) {
ui.placeholder.height(ui.item.outerHeight());
},
update: function(event, ui) {
$('#pc-faq-save-order').addClass('highlight');
}
});
// Save order
$('#pc-faq-save-order').on('click', function() {
var $button = $(this);
var $status = $('.pc-faq-save-status');
if ($button.prop('disabled')) {
return;
}
var faqIds = [];
$('#pc-faq-sortable .pc-faq-item').each(function() {
faqIds.push($(this).data('faq-id'));
});
$button.prop('disabled', true);
$status.text(pc_faq_manager.saving).removeClass('saved error').addClass('saving');
$.post(pc_faq_manager.ajax_url, {
action: 'pc_faq_reorder',
nonce: pc_faq_manager.nonce,
faq_ids: faqIds
})
.done(function(response) {
if (response.success) {
$status.text(pc_faq_manager.saved).removeClass('saving error').addClass('saved');
$button.removeClass('highlight');
} else {
$status.text(pc_faq_manager.error).removeClass('saving saved').addClass('error');
}
})
.fail(function() {
$status.text(pc_faq_manager.error).removeClass('saving saved').addClass('error');
})
.always(function() {
$button.prop('disabled', false);
setTimeout(function() {
$status.text('').removeClass('saving saved error');
}, 3000);
});
});
// Edit FAQ modal
var $modal = $('#pc-faq-edit-modal');
var $backdrop = $('.pc-faq-modal-backdrop');
var currentEditingId = null;
function openEditModal(faqId) {
var $item = $('.pc-faq-item[data-faq-id="' + faqId + '"]');
var title = $item.find('.faq-question h4').text();
var content = $item.find('.faq-content').text();
$('#pc-faq-edit-title').val(title);
$('#pc-faq-edit-content').val(content);
currentEditingId = faqId;
$modal.show();
$backdrop.show();
$('body').css('overflow', 'hidden');
}
function closeEditModal() {
$modal.hide();
$backdrop.hide();
$('body').css('overflow', '');
currentEditingId = null;
}
$('.pc-faq-edit').on('click', function() {
var faqId = $(this).data('faq-id');
openEditModal(faqId);
});
$('.pc-faq-modal-close, .pc-faq-modal-cancel, .pc-faq-modal-backdrop').on('click', function() {
closeEditModal();
});
// Save edited FAQ
$('.pc-faq-modal-save').on('click', function() {
if (!currentEditingId) {
return;
}
var $button = $(this);
var title = $('#pc-faq-edit-title').val().trim();
var content = $('#pc-faq-edit-content').val();
if (!title || !content) {
alert('Please fill in both the question and answer.');
return;
}
if ($button.prop('disabled')) {
return;
}
$button.prop('disabled', true);
$.post(pc_faq_manager.ajax_url, {
action: 'pc_faq_edit',
nonce: pc_faq_manager.nonce,
faq_id: currentEditingId,
title: title,
content: content
})
.done(function(response) {
if (response.success) {
// Update the item in the list
var $item = $('.pc-faq-item[data-faq-id="' + currentEditingId + '"]');
$item.find('.faq-question h4').text(title);
$item.find('.faq-content').text(content.replace(/<[^>]*>/g, '').substring(0, 100) + '...');
$item.find('.faq-excerpt').text(content.replace(/<[^>]*>/g, '').substring(0, 50) + '...');
closeEditModal();
} else {
alert('Failed to update FAQ: ' + (response.data.message || 'Unknown error'));
}
})
.fail(function() {
alert('Failed to update FAQ. Please try again.');
})
.always(function() {
$button.prop('disabled', false);
});
});
// Delete FAQ
$('.pc-faq-delete').on('click', function() {
var $button = $(this);
var faqId = $button.data('faq-id');
var $item = $button.closest('.pc-faq-item');
if (!confirm(pc_faq_manager.confirm_delete)) {
return;
}
if ($button.prop('disabled')) {
return;
}
$button.prop('disabled', true);
$.post(pc_faq_manager.ajax_url, {
action: 'pc_faq_delete',
nonce: pc_faq_manager.nonce,
faq_id: faqId
})
.done(function(response) {
if (response.success) {
$item.fadeOut(300, function() {
$(this).remove();
// Check if we need to show empty state
var remainingItems = $('#pc-faq-sortable .pc-faq-item').length;
if (remainingItems === 0) {
location.reload();
}
});
} else {
alert('Failed to delete FAQ: ' + (response.data.message || 'Unknown error'));
}
})
.fail(function() {
alert('Failed to delete FAQ. Please try again.');
})
.always(function() {
$button.prop('disabled', false);
});
});
// Add new FAQ form
$('#pc-faq-add-form').on('submit', function(e) {
e.preventDefault();
var $form = $(this);
var $button = $form.find('button[type="submit"]');
var title = $('#pc-faq-title').val().trim();
var content = $('#pc-faq-content').val();
if (!title || !content) {
showMessage('Please fill in both the question and answer.', 'error');
return;
}
if ($button.prop('disabled')) {
return;
}
$button.prop('disabled', true);
$.post(pc_faq_manager.ajax_url, {
action: 'pc_faq_add_new',
nonce: pc_faq_manager.nonce,
title: title,
content: content
})
.done(function(response) {
if (response.success) {
showMessage('FAQ created successfully! Redirecting...', 'success');
setTimeout(function() {
window.location.href = 'admin.php?page=pc-faq-manager';
}, 1500);
} else {
showMessage('Failed to create FAQ: ' + (response.data.message || 'Unknown error'), 'error');
}
})
.fail(function() {
showMessage('Failed to create FAQ. Please try again.', 'error');
})
.always(function() {
$button.prop('disabled', false);
});
});
function showMessage(message, type) {
var $message = $('#pc-faq-add-message');
$message.removeClass('notice-success notice-error')
.addClass('notice-' + type)
.html('<p>' + message + '</p>')
.show();
setTimeout(function() {
$message.fadeOut();
}, 5000);
}
// ESC key to close modal
$(document).on('keydown', function(e) {
if (e.keyCode === 27 && $modal.is(':visible')) {
closeEditModal();
}
});
});

View File

@@ -0,0 +1,74 @@
<?php
/**
* Add New FAQ admin page template
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
?>
<div class="wrap pc-faq-manager-admin">
<h1 class="wp-heading-inline">
<?php esc_html_e('Add New FAQ', PC_FAQ_MANAGER_SLUG); ?>
</h1>
<a href="<?php echo admin_url('admin.php?page=pc-faq-manager'); ?>" class="page-title-action">
<?php esc_html_e('View All FAQs', PC_FAQ_MANAGER_SLUG); ?>
</a>
<hr class="wp-header-end">
<div class="pc-faq-manager-add-form">
<div class="pc-faq-form-container">
<h2><?php esc_html_e('Create New FAQ', PC_FAQ_MANAGER_SLUG); ?></h2>
<form id="pc-faq-add-form">
<div class="form-field">
<label for="pc-faq-title"><?php esc_html_e('Question', PC_FAQ_MANAGER_SLUG); ?></label>
<input type="text" id="pc-faq-title" class="widefat" placeholder="<?php esc_attr_e('Enter your question here...', PC_FAQ_MANAGER_SLUG); ?>" required>
<p class="description"><?php esc_html_e('This will be displayed as the question on your FAQ page.', PC_FAQ_MANAGER_SLUG); ?></p>
</div>
<div class="form-field">
<label for="pc-faq-content"><?php esc_html_e('Answer', PC_FAQ_MANAGER_SLUG); ?></label>
<textarea id="pc-faq-content" name="content" class="widefat" rows="10" placeholder="<?php esc_attr_e('Enter your answer here...', PC_FAQ_MANAGER_SLUG); ?>" required></textarea>
<p class="description"><?php esc_html_e('Provide a detailed answer to the question above.', PC_FAQ_MANAGER_SLUG); ?></p>
</div>
<div class="form-field">
<button type="submit" class="button button-primary button-large">
<span class="dashicons dashicons-plus-alt"></span>
<?php esc_html_e('Create FAQ', PC_FAQ_MANAGER_SLUG); ?>
</button>
<a href="<?php echo admin_url('admin.php?page=pc-faq-manager'); ?>" class="button button-large">
<?php esc_html_e('Cancel', PC_FAQ_MANAGER_SLUG); ?>
</a>
</div>
</form>
</div>
<div class="pc-faq-manager-help">
<div class="help-card">
<h3><?php esc_html_e('Pro Tips', PC_FAQ_MANAGER_SLUG); ?></h3>
<ul>
<li><?php esc_html_e('Keep questions clear and concise', PC_FAQ_MANAGER_SLUG); ?></li>
<li><?php esc_html_e('Provide comprehensive answers', PC_FAQ_MANAGER_SLUG); ?></li>
<li><?php esc_html_e('Use formatting to improve readability', PC_FAQ_MANAGER_SLUG); ?></li>
<li><?php esc_html_e('Consider adding images or links if helpful', PC_FAQ_MANAGER_SLUG); ?></li>
</ul>
</div>
<div class="help-card">
<h3><?php esc_html_e('FAQ Page', PC_FAQ_MANAGER_SLUG); ?></h3>
<p><?php esc_html_e('Your FAQs will automatically appear on the FAQ page. You can find it at:', PC_FAQ_MANAGER_SLUG); ?></p>
<p><code><?php echo home_url('/faq/'); ?></code></p>
<p><a href="<?php echo home_url('/faq/'); ?>" target="_blank" class="button"><?php esc_html_e('View FAQ Page', PC_FAQ_MANAGER_SLUG); ?></a></p>
</div>
</div>
</div>
<!-- Success/Error Messages -->
<div id="pc-faq-add-message" class="notice is-dismissible" style="display: none;"></div>
</div>

View File

@@ -0,0 +1,140 @@
<?php
/**
* Main FAQ Manager admin page template
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
?>
<div class="wrap pc-faq-manager-admin">
<h1 class="wp-heading-inline">
<?php esc_html_e('FAQ Manager', PC_FAQ_MANAGER_SLUG); ?>
</h1>
<a href="<?php echo admin_url('admin.php?page=pc-faq-manager-add'); ?>" class="page-title-action">
<?php esc_html_e('Add New FAQ', PC_FAQ_MANAGER_SLUG); ?>
</a>
<hr class="wp-header-end">
<?php if ($faq_count > 0): ?>
<div class="pc-faq-manager-header">
<div class="pc-faq-manager-stats">
<div class="stat-card">
<div class="stat-number"><?php echo esc_html($faq_count); ?></div>
<div class="stat-label"><?php esc_html_e('Total FAQs', PC_FAQ_MANAGER_SLUG); ?></div>
</div>
</div>
<div class="pc-faq-manager-instructions">
<h3><?php esc_html_e('How to use', PC_FAQ_MANAGER_SLUG); ?></h3>
<ul>
<li><?php esc_html_e('Drag and drop FAQs to reorder them', PC_FAQ_MANAGER_SLUG); ?></li>
<li><?php esc_html_e('Click on any FAQ to edit it inline', PC_FAQ_MANAGER_SLUG); ?></li>
<li><?php esc_html_e('Use the delete button to remove an FAQ', PC_FAQ_MANAGER_SLUG); ?></li>
<li><?php esc_html_e('The FAQ page will automatically update on your website', PC_FAQ_MANAGER_SLUG); ?></li>
</ul>
</div>
</div>
<div class="pc-faq-manager-list">
<div class="pc-faq-manager-list-header">
<div class="sort-handle"></div>
<div class="faq-question"><?php esc_html_e('Question', PC_FAQ_MANAGER_SLUG); ?></div>
<div class="faq-answer"><?php esc_html_e('Answer', PC_FAQ_MANAGER_SLUG); ?></div>
<div class="faq-actions"><?php esc_html_e('Actions', PC_FAQ_MANAGER_SLUG); ?></div>
</div>
<div class="pc-faq-manager-items" id="pc-faq-sortable">
<?php foreach ($faqs as $faq): ?>
<div class="pc-faq-item" data-faq-id="<?php echo esc_attr($faq->ID); ?>">
<div class="sort-handle">
<span class="dashicons dashicons-menu"></span>
</div>
<div class="faq-question">
<div class="faq-content">
<h4><?php echo esc_html($faq->post_title); ?></h4>
<div class="faq-excerpt">
<?php echo wp_trim_words($faq->post_content, 15, '...'); ?>
</div>
</div>
</div>
<div class="faq-answer">
<div class="faq-content">
<?php echo wp_trim_words($faq->post_content, 20, '...'); ?>
</div>
</div>
<div class="faq-actions">
<button type="button" class="button button-secondary pc-faq-edit" data-faq-id="<?php echo esc_attr($faq->ID); ?>">
<span class="dashicons dashicons-edit"></span>
<?php esc_html_e('Edit', PC_FAQ_MANAGER_SLUG); ?>
</button>
<button type="button" class="button pc-faq-delete" data-faq-id="<?php echo esc_attr($faq->ID); ?>">
<span class="dashicons dashicons-trash"></span>
<?php esc_html_e('Delete', PC_FAQ_MANAGER_SLUG); ?>
</button>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<div class="pc-faq-manager-footer">
<button type="button" class="button button-primary" id="pc-faq-save-order">
<span class="dashicons dashicons-yes"></span>
<?php esc_html_e('Save Order', PC_FAQ_MANAGER_SLUG); ?>
</button>
<span class="pc-faq-save-status"></span>
</div>
<?php else: ?>
<div class="pc-faq-manager-empty">
<div class="pc-faq-manager-empty-icon">
<span class="dashicons dashicons-editor-help"></span>
</div>
<h2><?php esc_html_e('No FAQs yet', PC_FAQ_MANAGER_SLUG); ?></h2>
<p><?php esc_html_e('Create your first FAQ to get started with the FAQ Manager.', PC_FAQ_MANAGER_SLUG); ?></p>
<p>
<a href="<?php echo admin_url('admin.php?page=pc-faq-manager-add'); ?>" class="button button-primary">
<?php esc_html_e('Create Your First FAQ', PC_FAQ_MANAGER_SLUG); ?>
</a>
</p>
</div>
<?php endif; ?>
</div>
<!-- Edit Modal -->
<div id="pc-faq-edit-modal" class="pc-faq-modal" style="display: none;">
<div class="pc-faq-modal-content">
<div class="pc-faq-modal-header">
<h2><?php esc_html_e('Edit FAQ', PC_FAQ_MANAGER_SLUG); ?></h2>
<button type="button" class="pc-faq-modal-close">&times;</button>
</div>
<div class="pc-faq-modal-body">
<div class="form-field">
<label for="pc-faq-edit-title"><?php esc_html_e('Question', PC_FAQ_MANAGER_SLUG); ?></label>
<input type="text" id="pc-faq-edit-title" class="widefat" placeholder="<?php esc_attr_e('Enter your question here...', PC_FAQ_MANAGER_SLUG); ?>">
</div>
<div class="form-field">
<label for="pc-faq-edit-content"><?php esc_html_e('Answer', PC_FAQ_MANAGER_SLUG); ?></label>
<?php
wp_editor('', 'pc-faq-edit-content', array(
'textarea_name' => 'content',
'textarea_rows' => 10,
'media_buttons' => false,
'teeny' => true,
));
?>
</div>
</div>
<div class="pc-faq-modal-footer">
<button type="button" class="button button-secondary pc-faq-modal-cancel"><?php esc_html_e('Cancel', PC_FAQ_MANAGER_SLUG); ?></button>
<button type="button" class="button button-primary pc-faq-modal-save"><?php esc_html_e('Update FAQ', PC_FAQ_MANAGER_SLUG); ?></button>
</div>
</div>
</div>
<div class="pc-faq-modal-backdrop" style="display: none;"></div>

View File

@@ -0,0 +1,106 @@
<?php
/**
* FAQ Manager Helper Class
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
class PC_FAQ_Manager_Helper {
/**
* Get all FAQs ordered by order meta
*/
public static function get_faqs($posts_per_page = -1) {
$args = array(
'post_type' => 'pc_faq',
'post_status' => 'publish',
'posts_per_page' => $posts_per_page,
'orderby' => 'meta_value_num',
'meta_key' => '_pc_faq_order',
'order' => 'ASC',
);
return get_posts($args);
}
/**
* Get FAQ count
*/
public static function get_faq_count() {
$args = array(
'post_type' => 'pc_faq',
'post_status' => 'publish',
'posts_per_page' => 1,
);
$query = new WP_Query($args);
return $query->found_posts;
}
/**
* Reorder FAQs
*/
public static function reorder_faqs($faq_ids) {
if (!is_array($faq_ids) || empty($faq_ids)) {
return false;
}
foreach ($faq_ids as $order => $faq_id) {
$faq_id = intval($faq_id);
$order = intval($order);
if ($faq_id > 0) {
update_post_meta($faq_id, '_pc_faq_order', $order);
}
}
return true;
}
/**
* Get max order value
*/
public static function get_max_order() {
global $wpdb;
$max_order = $wpdb->get_var($wpdb->prepare(
"SELECT MAX(CAST(meta_value AS SIGNED))
FROM {$wpdb->postmeta}
WHERE meta_key = %s",
'_pc_faq_order'
));
return $max_order ? intval($max_order) : 0;
}
/**
* Sanitize and validate FAQ data
*/
public static function sanitize_faq_data($data) {
$sanitized = array();
if (isset($data['title'])) {
$sanitized['title'] = sanitize_text_field($data['title']);
}
if (isset($data['content'])) {
$sanitized['content'] = wp_kses_post($data['content']);
}
if (isset($data['order'])) {
$sanitized['order'] = intval($data['order']);
}
return $sanitized;
}
/**
* Check user capabilities
*/
public static function can_manage_faqs() {
return current_user_can('manage_options') || current_user_can('edit_posts');
}
}

View File

@@ -0,0 +1,137 @@
<?php
/**
* FAQ Post Type Handler
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
class PC_FAQ_Manager_Post_Type {
/**
* Constructor
*/
public function __construct() {
add_action('init', array($this, 'register_post_type'));
add_action('add_meta_boxes', array($this, 'add_meta_boxes'));
add_action('save_post_pc_faq', array($this, 'save_faq_meta'));
add_action('manage_pc_faq_posts_custom_column', array($this, 'custom_columns'), 10, 2);
add_filter('manage_pc_faq_posts_columns', array($this, 'set_custom_columns'));
}
/**
* Register FAQ post type
*/
public function register_post_type() {
$labels = array(
'name' => __('FAQs', PC_FAQ_MANAGER_SLUG),
'singular_name' => __('FAQ', PC_FAQ_MANAGER_SLUG),
'menu_name' => __('FAQ Manager', PC_FAQ_MANAGER_SLUG),
'add_new' => __('Add New FAQ', PC_FAQ_MANAGER_SLUG),
'add_new_item' => __('Add New FAQ', PC_FAQ_MANAGER_SLUG),
'edit_item' => __('Edit FAQ', PC_FAQ_MANAGER_SLUG),
'new_item' => __('New FAQ', PC_FAQ_MANAGER_SLUG),
'view_item' => __('View FAQ', PC_FAQ_MANAGER_SLUG),
'search_items' => __('Search FAQs', PC_FAQ_MANAGER_SLUG),
'not_found' => __('No FAQs found', PC_FAQ_MANAGER_SLUG),
'not_found_in_trash' => __('No FAQs found in Trash', PC_FAQ_MANAGER_SLUG),
'all_items' => __('All FAQs', PC_FAQ_MANAGER_SLUG),
);
$args = array(
'labels' => $labels,
'public' => false,
'has_archive' => false,
'publicly_queryable' => false,
'query_var' => false,
'rewrite' => false,
'capability_type' => 'post',
'hierarchical' => false,
'menu_position' => 25,
'menu_icon' => 'dashicons-editor-help',
'supports' => array('title', 'editor'),
'show_in_rest' => false,
'show_ui' => true,
'show_in_menu' => false,
'exclude_from_search' => true,
);
register_post_type('pc_faq', $args);
}
/**
* Add meta boxes
*/
public function add_meta_boxes() {
add_meta_box(
'pc_faq_order_meta',
__('FAQ Order', PC_FAQ_MANAGER_SLUG),
array($this, 'order_meta_box'),
'pc_faq',
'side',
'default'
);
}
/**
* Order meta box callback
*/
public function order_meta_box($post) {
wp_nonce_field('pc_faq_save_meta', 'pc_faq_nonce');
$order = get_post_meta($post->ID, '_pc_faq_order', true) ?: 0;
echo '<label for="pc_faq_order">' . __('Display Order:', PC_FAQ_MANAGER_SLUG) . '</label>';
echo '<input type="number" id="pc_faq_order" name="pc_faq_order" value="' . esc_attr($order) . '" min="0" style="width: 100%; margin-top: 5px;" />';
echo '<p class="description">' . __('Lower numbers appear first. Leave at 0 for automatic ordering.', PC_FAQ_MANAGER_SLUG) . '</p>';
}
/**
* Save FAQ meta
*/
public function save_faq_meta($post_id) {
if (!isset($_POST['pc_faq_nonce']) || !wp_verify_nonce($_POST['pc_faq_nonce'], 'pc_faq_save_meta')) {
return;
}
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
if (!current_user_can('edit_post', $post_id)) {
return;
}
if (isset($_POST['pc_faq_order'])) {
$order = sanitize_text_field($_POST['pc_faq_order']);
update_post_meta($post_id, '_pc_faq_order', $order);
}
}
/**
* Set custom columns
*/
public function set_custom_columns($columns) {
$new_columns = array();
$new_columns['cb'] = $columns['cb'];
$new_columns['title'] = $columns['title'];
$new_columns['order'] = __('Order', PC_FAQ_MANAGER_SLUG);
$new_columns['date'] = $columns['date'];
return $new_columns;
}
/**
* Display custom columns
*/
public function custom_columns($column, $post_id) {
switch ($column) {
case 'order':
$order = get_post_meta($post_id, '_pc_faq_order', true) ?: 0;
echo esc_html($order);
break;
}
}
}

View File

@@ -0,0 +1,10 @@
{
"$schema": "https://opencode.ai/config.json",
"permission": {
"external_directory": {
"*": "deny",
"*/faq-fzzb/*": "allow",
"apps/c7f9e5c6-e7c2-4258-a583-ccffcf9791c8/faq-fzzb/*": "allow"
}
}
}

View File

@@ -0,0 +1,147 @@
<?php
/**
* Plugin Name: FAQ Manager
* Plugin URI: https://plugincompass.com/plugins/pc-faq-manager-abc123
* Description: Easily create and manage FAQ pages with drag-and-drop reordering functionality.
* Version: 1.0.0
* Author: Plugin Compass
* Author URI: https://plugincompass.com
* License: GPLv2 or later
* Text Domain: pc-faq-manager-abc123
* Domain Path: /languages
* Update URI: false
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
// Plugin constants
define('PC_FAQ_MANAGER_VERSION', '1.0.0');
define('PC_FAQ_MANAGER_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('PC_FAQ_MANAGER_PLUGIN_URL', plugin_dir_url(__FILE__));
define('PC_FAQ_MANAGER_SLUG', 'pc-faq-manager-abc123');
// Prevent WordPress.org update checks
add_filter('site_transient_update_plugins', function($value) {
$plugin_file = plugin_basename(__FILE__);
if (isset($value->response[$plugin_file])) {
unset($value->response[$plugin_file]);
}
return $value;
});
/**
* Main plugin class
*/
class PC_FAQ_Manager {
private static $instance = null;
/**
* Get singleton instance
*/
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
add_action('init', array($this, 'init'));
register_activation_hook(__FILE__, array($this, 'activate'));
register_deactivation_hook(__FILE__, array($this, 'deactivate'));
}
/**
* Initialize plugin
*/
public function init() {
// Load text domain
load_plugin_textdomain(PC_FAQ_MANAGER_SLUG, false, dirname(plugin_basename(__FILE__)) . '/languages');
// Include required files
$this->include_files();
// Initialize admin functionality
if (is_admin()) {
new PC_FAQ_Manager_Admin();
}
// Initialize frontend functionality
new PC_FAQ_Manager_Public();
}
/**
* Include required files
*/
private function include_files() {
require_once PC_FAQ_MANAGER_PLUGIN_DIR . 'includes/class-pc-faq-manager-post-type.php';
require_once PC_FAQ_MANAGER_PLUGIN_DIR . 'includes/class-pc-faq-manager-helper.php';
if (is_admin()) {
require_once PC_FAQ_MANAGER_PLUGIN_DIR . 'admin/class-pc-faq-manager-admin.php';
}
require_once PC_FAQ_MANAGER_PLUGIN_DIR . 'public/class-pc-faq-manager-public.php';
}
/**
* Plugin activation
*/
public function activate() {
// Flush rewrite rules
flush_rewrite_rules();
// Create FAQ page if it doesn't exist
$this->create_faq_page();
// Set default options
add_option('pc_faq_manager_version', PC_FAQ_MANAGER_VERSION);
}
/**
* Plugin deactivation
*/
public function deactivate() {
// Flush rewrite rules
flush_rewrite_rules();
}
/**
* Create FAQ page automatically
*/
private function create_faq_page() {
$page_title = __('FAQ', PC_FAQ_MANAGER_SLUG);
$page_content = '[pc_faq_page]';
$page_template = '';
// Check if page already exists
$existing_page = get_page_by_title($page_title);
if (!$existing_page) {
$page_data = array(
'post_title' => $page_title,
'post_content' => $page_content,
'post_status' => 'publish',
'post_author' => get_current_user_id(),
'post_type' => 'page',
'post_name' => 'faq'
);
$page_id = wp_insert_post($page_data);
if ($page_id && !is_wp_error($page_id)) {
update_post_meta($page_id, '_wp_page_template', $page_template);
}
}
}
}
// Initialize the plugin
PC_FAQ_Manager::get_instance();

View File

@@ -0,0 +1,79 @@
<?php
/**
* FAQ Manager Public Interface
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
class PC_FAQ_Manager_Public {
/**
* Constructor
*/
public function __construct() {
add_action('wp_enqueue_scripts', array($this, 'enqueue_public_scripts'));
add_shortcode('pc_faq_page', array($this, 'render_faq_page'));
}
/**
* Enqueue public scripts and styles
*/
public function enqueue_public_scripts() {
wp_enqueue_style('pc-faq-manager-public', PC_FAQ_MANAGER_PLUGIN_URL . 'public/css/public-style.css', array(), PC_FAQ_MANAGER_VERSION);
wp_enqueue_script('pc-faq-manager-public', PC_FAQ_MANAGER_PLUGIN_URL . 'public/js/public-script.js', array('jquery'), PC_FAQ_MANAGER_VERSION, true);
}
/**
* Render FAQ page shortcode
*/
public function render_faq_page($atts) {
$atts = shortcode_atts(array(
'title' => __('Frequently Asked Questions', PC_FAQ_MANAGER_SLUG),
'show_count' => true,
), $atts);
$faqs = PC_FAQ_Manager_Helper::get_faqs();
if (empty($faqs)) {
return '<div class="pc-faq-empty">' . esc_html__('No FAQs available at this time.', PC_FAQ_MANAGER_SLUG) . '</div>';
}
ob_start();
if ($atts['show_count']) {
echo '<div class="pc-faq-header">';
echo '<h1 class="pc-faq-title">' . esc_html($atts['title']) . '</h1>';
echo '<div class="pc-faq-count">' . sprintf(esc_html__('%d FAQs available', PC_FAQ_MANAGER_SLUG), count($faqs)) . '</div>';
echo '</div>';
}
echo '<div class="pc-faq-container">';
echo '<div class="pc-faq-items">';
foreach ($faqs as $index => $faq) {
echo '<div class="pc-faq-item" data-faq-id="' . esc_attr($faq->ID) . '">';
echo '<div class="pc-faq-question">';
echo '<button class="pc-faq-toggle" aria-expanded="false" aria-controls="pc-faq-answer-' . esc_attr($faq->ID) . '">';
echo '<span class="pc-faq-question-text">' . esc_html($faq->post_title) . '</span>';
echo '<span class="pc-faq-toggle-icon"><span class="pc-faq-plus"></span><span class="pc-faq-minus"></span></span>';
echo '</button>';
echo '</div>';
echo '<div class="pc-faq-answer" id="pc-faq-answer-' . esc_attr($faq->ID) . '" style="display: none;">';
echo '<div class="pc-faq-answer-content">';
echo wp_kses_post($faq->post_content);
echo '</div>';
echo '</div>';
echo '</div>';
}
echo '</div>';
echo '</div>';
return ob_get_clean();
}
}

View File

@@ -0,0 +1,442 @@
/* FAQ Manager Public Styles */
/* Custom CSS Variables */
:root {
--pc-faq-primary-color: #0073aa;
--pc-faq-secondary-color: #f1f1f1;
--pc-faq-text-color: #333;
--pc-faq-text-light: #666;
--pc-faq-border-color: #ddd;
--pc-faq-hover-color: #005a87;
--pc-faq-success-color: #46b450;
--pc-faq-background: #fff;
--pc-faq-spacing: 20px;
--pc-faq-border-radius: 6px;
--pc-faq-transition: all 0.3s ease;
--pc-faq-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
--pc-faq-shadow-hover: 0 4px 12px rgba(0, 0, 0, 0.15);
}
/* Dark Mode Support */
@media (prefers-color-scheme: dark) {
:root {
--pc-faq-primary-color: #4fb3d9;
--pc-faq-secondary-color: #2c2c2c;
--pc-faq-text-color: #f0f0f0;
--pc-faq-text-light: #ccc;
--pc-faq-border-color: #555;
--pc-faq-hover-color: #6cc0e3;
--pc-faq-background: #1a1a1a;
--pc-faq-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
--pc-faq-shadow-hover: 0 4px 12px rgba(0, 0, 0, 0.5);
}
}
/* FAQ Container */
.pc-faq-container {
max-width: 900px;
margin: 0 auto;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
line-height: 1.6;
color: var(--pc-faq-text-color);
}
/* FAQ Header */
.pc-faq-header {
text-align: center;
margin-bottom: calc(var(--pc-faq-spacing) * 2);
padding: calc(var(--pc-faq-spacing) * 1.5);
background: linear-gradient(135deg, var(--pc-faq-primary-color) 0%, var(--pc-faq-hover-color) 100%);
color: white;
border-radius: var(--pc-faq-border-radius);
box-shadow: var(--pc-faq-shadow);
}
.pc-faq-title {
font-size: 2.5rem;
font-weight: 700;
margin: 0 0 10px 0;
line-height: 1.2;
}
.pc-faq-count {
font-size: 1.1rem;
opacity: 0.9;
font-weight: 300;
}
/* FAQ Items */
.pc-faq-items {
display: flex;
flex-direction: column;
gap: 15px;
}
.pc-faq-item {
background: var(--pc-faq-background);
border: 1px solid var(--pc-faq-border-color);
border-radius: var(--pc-faq-border-radius);
overflow: hidden;
transition: var(--pc-faq-transition);
box-shadow: var(--pc-faq-shadow);
}
.pc-faq-item:hover {
transform: translateY(-2px);
box-shadow: var(--pc-faq-shadow-hover);
border-color: var(--pc-faq-primary-color);
}
/* FAQ Question */
.pc-faq-question {
margin: 0;
}
.pc-faq-toggle {
width: 100%;
padding: 20px 25px;
background: none;
border: none;
text-align: left;
cursor: pointer;
font-size: 1.1rem;
font-weight: 600;
color: var(--pc-faq-text-color);
display: flex;
justify-content: space-between;
align-items: center;
transition: var(--pc-faq-transition);
position: relative;
}
.pc-faq-toggle:hover {
background: var(--pc-faq-secondary-color);
color: var(--pc-faq-primary-color);
}
.pc-faq-toggle:focus {
outline: 2px solid var(--pc-faq-primary-color);
outline-offset: -2px;
}
.pc-faq-question-text {
flex: 1;
padding-right: 15px;
line-height: 1.4;
}
/* Toggle Icon */
.pc-faq-toggle-icon {
position: relative;
width: 24px;
height: 24px;
flex-shrink: 0;
}
.pc-faq-plus,
.pc-faq-minus {
position: absolute;
background: var(--pc-faq-primary-color);
transition: var(--pc-faq-transition);
}
.pc-faq-plus {
width: 100%;
height: 2px;
top: 50%;
left: 0;
transform: translateY(-50%);
}
.pc-faq-minus {
width: 2px;
height: 100%;
top: 0;
left: 50%;
transform: translateX(-50%);
}
.pc-faq-item.is-open .pc-faq-minus {
opacity: 0;
transform: translateX(-50%) rotate(90deg);
}
.pc-faq-item.is-open .pc-faq-plus {
transform: translateY(-50%) rotate(90deg);
}
/* FAQ Answer */
.pc-faq-answer {
max-height: 0;
overflow: hidden;
transition: max-height 0.4s ease, padding 0.4s ease;
background: var(--pc-faq-secondary-color);
border-top: 1px solid var(--pc-faq-border-color);
}
.pc-faq-item.is-open .pc-faq-answer {
max-height: 1000px;
padding: 25px;
}
.pc-faq-answer-content {
color: var(--pc-faq-text-light);
font-size: 1rem;
line-height: 1.7;
}
.pc-faq-answer-content p:last-child {
margin-bottom: 0;
}
.pc-faq-answer-content h1,
.pc-faq-answer-content h2,
.pc-faq-answer-content h3,
.pc-faq-answer-content h4,
.pc-faq-answer-content h5,
.pc-faq-answer-content h6 {
color: var(--pc-faq-text-color);
margin-top: 20px;
margin-bottom: 15px;
}
.pc-faq-answer-content h1 { font-size: 1.8em; }
.pc-faq-answer-content h2 { font-size: 1.6em; }
.pc-faq-answer-content h3 { font-size: 1.4em; }
.pc-faq-answer-content h4 { font-size: 1.2em; }
.pc-faq-answer-content h5 { font-size: 1.1em; }
.pc-faq-answer-content h6 { font-size: 1em; }
.pc-faq-answer-content ul,
.pc-faq-answer-content ol {
margin: 15px 0;
padding-left: 25px;
}
.pc-faq-answer-content li {
margin-bottom: 8px;
}
.pc-faq-answer-content a {
color: var(--pc-faq-primary-color);
text-decoration: none;
border-bottom: 1px solid transparent;
transition: border-bottom-color 0.2s ease;
}
.pc-faq-answer-content a:hover {
border-bottom-color: var(--pc-faq-primary-color);
}
.pc-faq-answer-content blockquote {
margin: 20px 0;
padding: 15px 20px;
background: var(--pc-faq-background);
border-left: 4px solid var(--pc-faq-primary-color);
font-style: italic;
border-radius: 0 var(--pc-faq-border-radius) var(--pc-faq-border-radius) 0;
}
.pc-faq-answer-content code {
background: var(--pc-faq-background);
padding: 3px 6px;
border-radius: 3px;
font-family: 'Courier New', Courier, monospace;
font-size: 0.9em;
border: 1px solid var(--pc-faq-border-color);
}
.pc-faq-answer-content pre {
background: var(--pc-faq-background);
padding: 15px;
border-radius: var(--pc-faq-border-radius);
overflow-x: auto;
margin: 20px 0;
border: 1px solid var(--pc-faq-border-color);
}
.pc-faq-answer-content pre code {
background: none;
padding: 0;
border: none;
font-size: 0.9em;
}
.pc-faq-answer-content img {
max-width: 100%;
height: auto;
border-radius: var(--pc-faq-border-radius);
margin: 15px 0;
box-shadow: var(--pc-faq-shadow);
}
.pc-faq-answer-content table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
background: var(--pc-faq-background);
border-radius: var(--pc-faq-border-radius);
overflow: hidden;
}
.pc-faq-answer-content th,
.pc-faq-answer-content td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid var(--pc-faq-border-color);
}
.pc-faq-answer-content th {
background: var(--pc-faq-primary-color);
color: white;
font-weight: 600;
}
.pc-faq-answer-content tr:last-child td {
border-bottom: none;
}
/* Empty State */
.pc-faq-empty {
text-align: center;
padding: 60px 20px;
color: var(--pc-faq-text-light);
font-size: 1.1rem;
font-style: italic;
}
/* Accessibility */
.pc-faq-toggle[aria-expanded="true"] {
color: var(--pc-faq-primary-color);
font-weight: 700;
}
/* Focus visible for keyboard navigation */
.pc-faq-toggle:focus-visible {
outline: 3px solid var(--pc-faq-primary-color);
outline-offset: 2px;
}
/* Print Styles */
@media print {
.pc-faq-container {
max-width: none;
}
.pc-faq-item {
break-inside: avoid;
box-shadow: none;
border: 1px solid #333;
}
.pc-faq-answer {
display: block !important;
max-height: none !important;
padding: 20px !important;
}
.pc-faq-toggle-icon {
display: none;
}
}
/* Responsive Design */
@media screen and (max-width: 768px) {
.pc-faq-container {
margin: 0 15px;
}
.pc-faq-title {
font-size: 2rem;
}
.pc-faq-toggle {
padding: 15px 20px;
font-size: 1rem;
}
.pc-faq-item.is-open .pc-faq-answer {
padding: 20px;
}
.pc-faq-answer-content {
font-size: 0.95rem;
}
}
@media screen and (max-width: 480px) {
.pc-faq-header {
padding: 20px 15px;
margin-bottom: 30px;
}
.pc-faq-title {
font-size: 1.75rem;
}
.pc-faq-toggle {
padding: 12px 15px;
font-size: 0.95rem;
}
.pc-faq-question-text {
padding-right: 10px;
}
.pc-faq-toggle-icon {
width: 20px;
height: 20px;
}
.pc-faq-item.is-open .pc-faq-answer {
padding: 15px;
}
.pc-faq-answer-content {
font-size: 0.9rem;
}
}
/* Animation Enhancements */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.pc-faq-item {
animation: fadeIn 0.4s ease-out;
}
.pc-faq-item:nth-child(1) { animation-delay: 0.1s; }
.pc-faq-item:nth-child(2) { animation-delay: 0.2s; }
.pc-faq-item:nth-child(3) { animation-delay: 0.3s; }
.pc-faq-item:nth-child(4) { animation-delay: 0.4s; }
.pc-faq-item:nth-child(5) { animation-delay: 0.5s; }
/* RTL Support */
[dir="rtl"] .pc-faq-toggle {
text-align: right;
}
[dir="rtl"] .pc-faq-question-text {
padding-right: 0;
padding-left: 15px;
}
[dir="rtl"] .pc-faq-answer-content ul,
[dir="rtl"] .pc-faq-answer-content ol {
padding-left: 0;
padding-right: 25px;
}
[dir="rtl"] .pc-faq-answer-content blockquote {
border-left: none;
border-right: 4px solid var(--pc-faq-primary-color);
border-radius: var(--pc-faq-border-radius) 0 0 var(--pc-faq-border-radius);
}

View File

@@ -0,0 +1,250 @@
jQuery(document).ready(function($) {
// FAQ Toggle functionality
$('.pc-faq-toggle').on('click', function(e) {
e.preventDefault();
var $button = $(this);
var $faqItem = $button.closest('.pc-faq-item');
var $answer = $faqItem.find('.pc-faq-answer');
var isExpanded = $button.attr('aria-expanded') === 'true';
// Close all other FAQs (optional accordion behavior)
$('.pc-faq-item').each(function() {
if ($(this)[0] !== $faqItem[0]) {
$(this).removeClass('is-open');
$(this).find('.pc-faq-answer').slideUp(300);
$(this).find('.pc-faq-toggle').attr('aria-expanded', 'false');
}
});
// Toggle current FAQ
if (isExpanded) {
$faqItem.removeClass('is-open');
$answer.slideUp(300);
$button.attr('aria-expanded', 'false');
} else {
$faqItem.addClass('is-open');
$answer.slideDown(300);
$button.attr('aria-expanded', 'true');
// Smooth scroll to the FAQ
setTimeout(function() {
$('html, body').animate({
scrollTop: $faqItem.offset().top - 20
}, 300);
}, 100);
}
});
// Keyboard navigation
$('.pc-faq-toggle').on('keydown', function(e) {
var $button = $(this);
var $faqItem = $button.closest('.pc-faq-item');
var $allItems = $('.pc-faq-item');
var currentIndex = $allItems.index($faqItem);
switch (e.keyCode) {
case 38: // Up arrow
e.preventDefault();
if (currentIndex > 0) {
$allItems.eq(currentIndex - 1).find('.pc-faq-toggle').focus();
}
break;
case 40: // Down arrow
e.preventDefault();
if (currentIndex < $allItems.length - 1) {
$allItems.eq(currentIndex + 1).find('.pc-faq-toggle').focus();
}
break;
case 36: // Home
e.preventDefault();
$allItems.first().find('.pc-faq-toggle').focus();
break;
case 35: // End
e.preventDefault();
$allItems.last().find('.pc-faq-toggle').focus();
break;
}
});
// Search functionality (if search box is added)
function initSearch() {
var $searchBox = $('#pc-faq-search');
if ($searchBox.length === 0) return;
$searchBox.on('input', function() {
var searchTerm = $(this).val().toLowerCase();
var $faqs = $('.pc-faq-item');
var visibleCount = 0;
$faqs.each(function() {
var $faqItem = $(this);
var question = $faqItem.find('.pc-faq-question-text').text().toLowerCase();
var answer = $faqItem.find('.pc-faq-answer-content').text().toLowerCase();
if (question.includes(searchTerm) || answer.includes(searchTerm)) {
$faqItem.show();
visibleCount++;
} else {
$faqItem.hide();
}
});
// Show no results message
var $noResults = $('.pc-faq-no-results');
if (visibleCount === 0 && searchTerm !== '') {
if ($noResults.length === 0) {
$noResults = $('<div class="pc-faq-no-results" style="text-align: center; padding: 40px 20px; color: #666; font-style: italic;"></div>');
$('.pc-faq-items').append($noResults);
}
$noResults.text('No FAQs found matching "' + searchTerm + '"');
} else {
$noResults.remove();
}
// Update count
var $count = $('.pc-faq-count');
if ($count.length > 0) {
if (searchTerm === '') {
$count.text($faqs.length + ' FAQs available');
} else {
$count.text(visibleCount + ' of ' + $faqs.length + ' FAQs matching "' + searchTerm + '"');
}
}
});
}
// Initialize search if present
initSearch();
// Expand/Collapse All functionality
function initExpandCollapseAll() {
var $expandAll = $('#pc-faq-expand-all');
var $collapseAll = $('#pc-faq-collapse-all');
if ($expandAll.length === 0 && $collapseAll.length === 0) return;
$expandAll.on('click', function() {
$('.pc-faq-item').addClass('is-open');
$('.pc-faq-answer').slideDown(300);
$('.pc-faq-toggle').attr('aria-expanded', 'true');
});
$collapseAll.on('click', function() {
$('.pc-faq-item').removeClass('is-open');
$('.pc-faq-answer').slideUp(300);
$('.pc-faq-toggle').attr('aria-expanded', 'false');
});
}
initExpandCollapseAll();
// Print functionality
function initPrintButton() {
var $printButton = $('#pc-faq-print');
if ($printButton.length === 0) return;
$printButton.on('click', function(e) {
e.preventDefault();
window.print();
});
}
initPrintButton();
// URL hash functionality
function initHashNavigation() {
if (window.location.hash) {
var hash = window.location.hash.substring(1);
var $targetFaq = $('#pc-faq-' + hash);
if ($targetFaq.length > 0) {
// Close all FAQs first
$('.pc-faq-item').removeClass('is-open');
$('.pc-faq-answer').slideUp(300);
$('.pc-faq-toggle').attr('aria-expanded', 'false');
// Open the target FAQ
setTimeout(function() {
$targetFaq.find('.pc-faq-toggle').trigger('click');
}, 500);
}
}
}
initHashNavigation();
// Update hash when FAQ is opened
$('.pc-faq-toggle').on('click', function() {
var $faqItem = $(this).closest('.pc-faq-item');
var faqId = $faqItem.attr('id');
if (faqId && $faqItem.hasClass('is-open')) {
history.pushState(null, null, '#' + faqId.replace('pc-faq-', ''));
} else {
if (window.location.hash) {
history.pushState(null, null, window.location.pathname);
}
}
});
// Analytics tracking (if available)
function trackFAQInteraction(action, faqId) {
if (typeof gtag !== 'undefined') {
gtag('event', 'faq_' + action, {
'faq_id': faqId
});
} else if (typeof ga !== 'undefined') {
ga('send', 'event', 'FAQ', action, faqId);
}
}
// Track FAQ opens
$('.pc-faq-toggle').on('click', function() {
var $faqItem = $(this).closest('.pc-faq-item');
var faqId = $faqItem.data('faq-id') || $faqItem.attr('id');
var question = $faqItem.find('.pc-faq-question-text').text();
trackFAQInteraction('open', faqId);
trackFAQInteraction('open_question', question);
});
// Performance optimization: Lazy load images in answers
function lazyLoadImages() {
if ('IntersectionObserver' in window) {
var imageObserver = new IntersectionObserver(function(entries, observer) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
var img = entry.target;
if (img.dataset.src) {
img.src = img.dataset.src;
img.removeAttribute('data-src');
observer.unobserve(img);
}
}
});
});
$('.pc-faq-answer-content img[data-src]').each(function() {
imageObserver.observe(this);
});
}
}
lazyLoadImages();
// Smooth scroll behavior for anchor links within answers
$('.pc-faq-answer-content a[href^="#"]').on('click', function(e) {
var target = $(this.getAttribute('href'));
if (target.length) {
e.preventDefault();
$('html, body').animate({
scrollTop: target.offset().top - 20
}, 300);
}
});
});

View File

@@ -0,0 +1,69 @@
<?php
/**
* FAQ Manager Uninstall Script
*
* This script runs when the plugin is uninstalled and cleans up all plugin data.
*/
// Prevent direct access
if (!defined('WP_UNINSTALL_PLUGIN')) {
exit;
}
// Plugin constants
define('PC_FAQ_MANAGER_SLUG', 'pc-faq-manager-abc123');
// Delete all FAQ posts
function pc_faq_manager_delete_faqs() {
$args = array(
'post_type' => 'pc_faq',
'post_status' => 'any',
'posts_per_page' => -1,
'fields' => 'ids',
);
$faq_ids = get_posts($args);
if (!empty($faq_ids)) {
foreach ($faq_ids as $faq_id) {
// Delete post permanently
wp_delete_post($faq_id, true);
}
}
}
// Delete FAQ page created by plugin
function pc_faq_manager_delete_faq_page() {
$page = get_page_by_title('FAQ');
if ($page) {
// Check if page contains our shortcode before deleting
$content = $page->post_content;
if (strpos($content, '[pc_faq_page]') !== false) {
wp_delete_post($page->ID, true);
}
}
}
// Delete plugin options
function pc_faq_manager_delete_options() {
delete_option('pc_faq_manager_version');
}
// Clean up post meta
function pc_faq_manager_cleanup_post_meta() {
global $wpdb;
// Delete FAQ order meta
$wpdb->delete(
$wpdb->postmeta,
array('meta_key' => '_pc_faq_order'),
array('%s')
);
}
// Run cleanup functions
pc_faq_manager_delete_faqs();
pc_faq_manager_delete_faq_page();
pc_faq_manager_delete_options();
pc_faq_manager_cleanup_post_meta();

View File

@@ -0,0 +1,204 @@
<?php
/**
* Admin Menu Class
*
* Handles admin menu registration and page routing.
*
* @package PCFormBuilder
*/
class PCFB_Admin_Menu {
private static $instance = null;
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
add_action( 'admin_menu', array( $this, 'add_admin_menu' ) );
add_action( 'admin_init', array( $this, 'handle_form_actions' ) );
add_action( 'wp_ajax_pcfb_add_field', array( $this, 'ajax_add_field' ) );
}
public function add_admin_menu() {
add_menu_page(
__( 'Form Builder', 'pc-form-builder-xyz123' ),
__( 'Form Builder', 'pc-form-builder-xyz123' ),
'manage_options',
'pcfb-form-builder',
array( $this, 'render_form_builder_page' ),
'dashicons-feedback',
30
);
add_submenu_page(
'pcfb-form-builder',
__( 'Form Builder', 'pc-form-builder-xyz123' ),
__( 'Form Builder', 'pc-form-builder-xyz123' ),
'manage_options',
'pcfb-form-builder',
array( $this, 'render_form_builder_page' )
);
add_submenu_page(
'pcfb-form-builder',
__( 'All Forms', 'pc-form-builder-xyz123' ),
__( 'All Forms', 'pc-form-builder-xyz123' ),
'manage_options',
'pcfb-all-forms',
array( $this, 'render_all_forms_page' )
);
add_submenu_page(
'pcfb-form-builder',
__( 'Responses', 'pc-form-builder-xyz123' ),
__( 'Responses', 'pc-form-builder-xyz123' ),
'manage_options',
'pcfb-responses',
array( $this, 'render_responses_page' )
);
}
public function render_form_builder_page() {
require_once PCFB_PLUGIN_DIR . 'admin/class-admin-helper.php';
$helper = PCFB_Admin_Helper::get_instance();
$form_id = isset( $_GET['form_id'] ) ? intval( $_GET['form_id'] ) : 0;
if ( $form_id > 0 ) {
include PCFB_PLUGIN_DIR . 'admin/page-form-builder.php';
} else {
include PCFB_PLUGIN_DIR . 'admin/page-form-builder.php';
}
}
public function render_all_forms_page() {
include PCFB_PLUGIN_DIR . 'admin/page-all-forms.php';
}
public function render_responses_page() {
$form_id = isset( $_GET['form_id'] ) ? intval( $_GET['form_id'] ) : 0;
$response_id = isset( $_GET['response_id'] ) ? intval( $_GET['response_id'] ) : 0;
if ( $response_id > 0 ) {
include PCFB_PLUGIN_DIR . 'admin/page-response-detail.php';
} elseif ( $form_id > 0 ) {
include PCFB_PLUGIN_DIR . 'admin/page-form-responses.php';
} else {
include PCFB_PLUGIN_DIR . 'admin/page-responses.php';
}
}
public function handle_form_actions() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
if ( ! isset( $_POST['pcfb_action'] ) ) {
return;
}
if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'pcfb_admin_action' ) ) {
return;
}
$action = sanitize_text_field( $_POST['pcfb_action'] );
switch ( $action ) {
case 'save_form':
$this->handle_save_form();
break;
case 'delete_form':
$this->handle_delete_form();
break;
case 'toggle_status':
$this->handle_toggle_status();
break;
}
}
private function handle_save_form() {
$form_data = isset( $_POST['form'] ) ? $_POST['form'] : array();
$fields = isset( $_POST['fields'] ) ? $_POST['fields'] : array();
$handler = PC_Form_Handler::get_instance();
$form_id = $handler->save_form( $form_data, $fields );
if ( $form_id ) {
$edit_url = admin_url( 'admin.php?page=pcfb-form-builder&form_id=' . $form_id );
$shortcode = '[pcfb-form id="' . $form_id . '"]';
add_action( 'admin_notices', function() use ( $edit_url, $shortcode ) {
echo '<div class="notice notice-success is-dismissible"><p>';
echo sprintf( __( 'Form saved successfully!', 'pc-form-builder-xyz123' ) );
echo '</p><p><strong>' . esc_html__( 'Shortcode:', 'pc-form-builder-xyz123' ) . '</strong> <code onclick="this.select(); document.execCommand(\'copy\');" style="background: #f0f0f1; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 14px;">' . esc_html( $shortcode ) . '</code></p>';
echo '<p><a href="' . esc_url( $edit_url ) . '" class="button button-primary" style="margin-top: 10px;">' . esc_html__( 'Go to Form Builder', 'pc-form-builder-xyz123' ) . '</a></p></div>';
} );
wp_redirect( admin_url( 'admin.php?page=pcfb-form-builder&form_id=' . $form_id ) );
exit;
} else {
add_action( 'admin_notices', function() {
echo '<div class="notice notice-error is-dismissible"><p>';
echo esc_html__( 'Failed to save form. Please try again.', 'pc-form-builder-xyz123' );
echo '</p></div>';
} );
}
}
private function handle_delete_form() {
$form_id = isset( $_POST['form_id'] ) ? intval( $_POST['form_id'] ) : 0;
if ( $form_id > 0 && PC_Form_Handler::delete_form( $form_id ) ) {
add_action( 'admin_notices', function() {
echo '<div class="notice notice-success is-dismissible"><p>';
echo esc_html__( 'Form deleted successfully.', 'pc-form-builder-xyz123' );
echo '</p></div>';
} );
} else {
add_action( 'admin_notices', function() {
echo '<div class="notice notice-error is-dismissible"><p>';
echo esc_html__( 'Failed to delete form. Please try again.', 'pc-form-builder-xyz123' );
echo '</p></div>';
} );
}
}
private function handle_toggle_status() {
global $wpdb;
$form_id = isset( $_POST['form_id'] ) ? intval( $_POST['form_id'] ) : 0;
if ( $form_id > 0 ) {
$form = PC_Form_Handler::get_form_by_id( $form_id );
if ( $form ) {
$new_status = 'active' === $form->status ? 'inactive' : 'active';
$table_name = $wpdb->prefix . 'pcfb_forms';
$wpdb->update( $table_name, array( 'status' => $new_status ), array( 'id' => $form_id ) );
}
}
}
public function ajax_add_field() {
check_ajax_referer( 'pcfb_admin_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => __( 'Permission denied.', 'pc-form-builder-xyz123' ) ) );
}
$field_type = isset( $_POST['field_type'] ) ? sanitize_text_field( $_POST['field_type'] ) : 'text';
$field_index = isset( $_POST['field_index'] ) ? intval( $_POST['field_index'] ) : 0;
require_once PCFB_PLUGIN_DIR . 'admin/class-admin-helper.php';
$helper = PCFB_Admin_Helper::get_instance();
ob_start();
$helper->render_new_field_item( $field_type, $field_index );
$html = ob_get_clean();
wp_send_json_success( array( 'html' => $html ) );
}
}

View File

@@ -0,0 +1,181 @@
<?php
/**
* Admin Helper Class
*
* Provides helper methods for admin pages.
*
* @package PCFormBuilder
*/
class PCFB_Admin_Helper {
private static $instance = null;
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
public function get_field_types() {
return array(
'text' => __( 'Text Input', 'pc-form-builder-xyz123' ),
'email' => __( 'Email', 'pc-form-builder-xyz123' ),
'name' => __( 'Name', 'pc-form-builder-xyz123' ),
'response' => __( 'Response', 'pc-form-builder-xyz123' ),
'dropdown' => __( 'Dropdown', 'pc-form-builder-xyz123' ),
'checkbox' => __( 'Checkbox', 'pc-form-builder-xyz123' ),
);
}
public function get_form_fields( $form_id ) {
global $wpdb;
$table_name = $wpdb->prefix . 'pcfb_fields';
return $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$table_name} WHERE form_id = %d ORDER BY sort_order ASC", $form_id ) );
}
public function render_field_item( $field, $index = 0 ) {
$validation_rules = maybe_unserialize( $field->validation_rules );
$options = maybe_unserialize( $field->options );
$field_types = $this->get_field_types();
$field_type_label = isset( $field_types[ $field->field_type ] ) ? $field_types[ $field->field_type ] : $field->field_type;
?>
<div class="pcfb-field-item" data-field-id="<?php echo esc_attr( $field->id ); ?>" data-field-type="<?php echo esc_attr( $field->field_type ); ?>">
<input type="hidden" name="fields[<?php echo esc_attr( $index ); ?>][id]" value="<?php echo esc_attr( $field->id ); ?>">
<input type="hidden" name="fields[<?php echo esc_attr( $index ); ?>][field_type]" value="<?php echo esc_attr( $field->field_type ); ?>">
<input type="hidden" name="fields[<?php echo esc_attr( $index ); ?>][field_name]" value="<?php echo esc_attr( $field->field_name ); ?>">
<div class="pcfb-field-header">
<span class="pcfb-field-type-badge"><?php echo esc_html( $field_type_label ); ?></span>
<span class="pcfb-field-name-display"><?php echo esc_html( $field->field_name ); ?></span>
<div class="pcfb-field-actions">
<button type="button" class="button button-small pcfb-toggle-field">
<span class="dashicons dashicons-arrow-up-alt2"></span>
</button>
</div>
</div>
<div class="pcfb-field-content">
<div class="pcfb-form-group">
<label><?php esc_html_e( 'Field Label', 'pc-form-builder-xyz123' ); ?></label>
<input type="text" name="fields[<?php echo esc_attr( $index ); ?>][field_label]" value="<?php echo esc_attr( $field->field_label ); ?>" class="pcfb-field-label-input" placeholder="<?php esc_attr_e( 'Enter field label', 'pc-form-builder-xyz123' ); ?>">
</div>
<div class="pcfb-form-group">
<label><?php esc_html_e( 'Placeholder Text', 'pc-form-builder-xyz123' ); ?></label>
<input type="text" name="fields[<?php echo esc_attr( $index ); ?>][placeholder]" value="<?php echo esc_attr( $field->placeholder ); ?>" placeholder="<?php esc_attr_e( 'Optional placeholder text', 'pc-form-builder-xyz123' ); ?>">
</div>
<?php if ( 'dropdown' === $field->field_type ) : ?>
<div class="pcfb-form-group">
<label><?php esc_html_e( 'Options', 'pc-form-builder-xyz123' ); ?></label>
<textarea name="fields[<?php echo esc_attr( $index ); ?>][options]" rows="3" placeholder="<?php esc_attr_e( 'One option per line', 'pc-form-builder-xyz123' ); ?>"><?php echo ! empty( $options ) ? esc_textarea( implode( "\n", $options ) ) : ''; ?></textarea>
<p class="description"><?php esc_html_e( 'Enter each option on a new line.', 'pc-form-builder-xyz123' ); ?></p>
</div>
<?php elseif ( 'checkbox' === $field->field_type ) : ?>
<div class="pcfb-form-group">
<label><?php esc_html_e( 'Checkbox Options', 'pc-form-builder-xyz123' ); ?></label>
<textarea name="fields[<?php echo esc_attr( $index ); ?>][options]" rows="3" placeholder="<?php esc_attr_e( 'One option per line', 'pc-form-builder-xyz123' ); ?>"><?php echo ! empty( $options ) ? esc_textarea( implode( "\n", $options ) ) : ''; ?></textarea>
<p class="description"><?php esc_html_e( 'Enter each checkbox option on a new line.', 'pc-form-builder-xyz123' ); ?></p>
</div>
<?php endif; ?>
<div class="pcfb-form-group pcfb-validation-group">
<label class="pcfb-checkbox-label">
<input type="checkbox" name="fields[<?php echo esc_attr( $index ); ?>][validation_rules][required]" value="1" <?php checked( ! empty( $validation_rules['required'] ), true ); ?>>
<?php esc_html_e( 'Required field', 'pc-form-builder-xyz123' ); ?>
</label>
</div>
</div>
<div class="pcfb-field-footer">
<button type="button" class="button button-link-delete pcfb-delete-field"><?php esc_html_e( 'Remove Field', 'pc-form-builder-xyz123' ); ?></button>
</div>
</div>
<?php
}
public function render_new_field_item( $field_type, $index ) {
$field_types = $this->get_field_types();
$field_type_label = isset( $field_types[ $field_type ] ) ? $field_types[ $field_type ] : $field_type;
$default_labels = array(
'text' => __( 'Text Field', 'pc-form-builder-xyz123' ),
'email' => __( 'Email Address', 'pc-form-builder-xyz123' ),
'name' => __( 'Full Name', 'pc-form-builder-xyz123' ),
'response' => __( 'Your Response', 'pc-form-builder-xyz123' ),
'dropdown' => __( 'Select Option', 'pc-form-builder-xyz123' ),
'checkbox' => __( 'Checkboxes', 'pc-form-builder-xyz123' ),
);
$default_label = isset( $default_labels[ $field_type ] ) ? $default_labels[ $field_type ] : __( 'Field', 'pc-form-builder-xyz123' );
$placeholder = '';
if ( 'response' === $field_type ) {
$placeholder = __( 'Enter your response here...', 'pc-form-builder-xyz123' );
}
?>
<div class="pcfb-field-item pcfb-new-field" data-field-id="new" data-field-type="<?php echo esc_attr( $field_type ); ?>">
<input type="hidden" name="fields[<?php echo esc_attr( $index ); ?>][id]" value="">
<input type="hidden" name="fields[<?php echo esc_attr( $index ); ?>][field_type]" value="<?php echo esc_attr( $field_type ); ?>">
<input type="hidden" name="fields[<?php echo esc_attr( $index ); ?>][field_name]" value="">
<div class="pcfb-field-header">
<span class="pcfb-field-type-badge"><?php echo esc_html( $field_type_label ); ?></span>
<span class="pcfb-field-name-display"><?php echo esc_html( sanitize_title( $default_label ) ); ?></span>
<div class="pcfb-field-actions">
<button type="button" class="button button-small pcfb-toggle-field">
<span class="dashicons dashicons-arrow-up-alt2"></span>
</button>
</div>
</div>
<div class="pcfb-field-content">
<div class="pcfb-form-group">
<label><?php esc_html_e( 'Field Label', 'pc-form-builder-xyz123' ); ?></label>
<input type="text" name="fields[<?php echo esc_attr( $index ); ?>][field_label]" value="<?php echo esc_attr( $default_label ); ?>" class="pcfb-field-label-input" placeholder="<?php esc_attr_e( 'Enter field label', 'pc-form-builder-xyz123' ); ?>">
</div>
<div class="pcfb-form-group">
<label><?php esc_html_e( 'Placeholder Text', 'pc-form-builder-xyz123' ); ?></label>
<input type="text" name="fields[<?php echo esc_attr( $index ); ?>][placeholder]" value="<?php echo esc_attr( $placeholder ); ?>" placeholder="<?php esc_attr_e( 'Optional placeholder text', 'pc-form-builder-xyz123' ); ?>">
</div>
<?php if ( 'dropdown' === $field_type ) : ?>
<div class="pcfb-form-group">
<label><?php esc_html_e( 'Options', 'pc-form-builder-xyz123' ); ?></label>
<textarea name="fields[<?php echo esc_attr( $index ); ?>][options]" rows="3" placeholder="<?php esc_attr_e( 'One option per line', 'pc-form-builder-xyz123' ); ?>"></textarea>
<p class="description"><?php esc_html_e( 'Enter each option on a new line.', 'pc-form-builder-xyz123' ); ?></p>
</div>
<?php elseif ( 'checkbox' === $field_type ) : ?>
<div class="pcfb-form-group">
<label><?php esc_html_e( 'Checkbox Options', 'pc-form-builder-xyz123' ); ?></label>
<textarea name="fields[<?php echo esc_attr( $index ); ?>][options]" rows="3" placeholder="<?php esc_attr_e( 'One option per line', 'pc-form-builder-xyz123' ); ?>"></textarea>
<p class="description"><?php esc_html_e( 'Enter each checkbox option on a new line.', 'pc-form-builder-xyz123' ); ?></p>
</div>
<?php endif; ?>
<div class="pcfb-form-group pcfb-validation-group">
<label class="pcfb-checkbox-label">
<input type="checkbox" name="fields[<?php echo esc_attr( $index ); ?>][validation_rules][required]" value="1">
<?php esc_html_e( 'Required field', 'pc-form-builder-xyz123' ); ?>
</label>
</div>
</div>
<div class="pcfb-field-footer">
<button type="button" class="button button-link-delete pcfb-delete-field"><?php esc_html_e( 'Remove Field', 'pc-form-builder-xyz123' ); ?></button>
</div>
</div>
<?php
}
}
if ( ! class_exists( 'PCFB_Field_Icons' ) ) {
class PCFB_Field_Icons {
private static $icons = array(
'text' => 'dashicons-text',
'email' => 'dashicons-email',
'name' => 'dashicons-admin-users',
'response' => 'dashicons-editor-quote',
'dropdown' => 'dashicons-arrow-down-alt2',
'checkbox' => 'dashicons-yes',
);
public static function get_field_icon( $type ) {
return isset( self::$icons[ $type ] ) ? self::$icons[ $type ] : 'dashicons-editor-help';
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,240 @@
/**
* PC Form Builder Admin Scripts
*
* JavaScript functionality for the form builder admin interface.
*
* @package PCFormBuilder
*/
(function($) {
'use strict';
var PCFB_Admin = {
fieldIndex: 0,
deletedFields: [],
init: function() {
this.initFieldTypeButtons();
this.initFieldToggle();
this.initFieldDelete();
this.initDeleteModal();
this.initFormBuilder();
},
initFieldTypeButtons: function() {
var self = this;
$('.pcfb-add-field-btn').on('click', function(e) {
e.preventDefault();
var fieldType = $(this).data('field-type');
self.addField(fieldType);
});
},
addField: function(fieldType) {
var self = this;
var container = $('#pcfb-fields-container');
if (container.find('.pcfb-empty-fields').length > 0) {
container.find('.pcfb-empty-fields').remove();
}
var fieldIndex = this.getNextFieldIndex();
var nonce = pcfbAdminVars.nonce;
$.ajax({
url: pcfbAdminVars.ajaxurl,
type: 'POST',
data: {
action: 'pcfb_add_field',
field_type: fieldType,
field_index: fieldIndex,
nonce: nonce
},
success: function(response) {
if (response.success) {
container.append(response.data.html);
self.reindexFields();
self.initFieldToggle();
self.initFieldDelete();
self.updateLabelOnChange();
}
},
error: function() {
alert(pcfbAdminVars.i18n.confirmDeleteField);
}
});
},
getNextFieldIndex: function() {
var maxIndex = 0;
$('#pcfb-fields-container [name^="fields["]').each(function() {
var match = $(this).attr('name').match(/fields\[(\d+)\]/);
if (match && parseInt(match[1]) > maxIndex) {
maxIndex = parseInt(match[1]);
}
});
return maxIndex + 1;
},
reindexFields: function() {
var fields = $('#pcfb-fields-container .pcfb-field-item');
fields.each(function(index) {
var field = $(this);
field.find('[name^="fields["]').each(function() {
var name = $(this).attr('name');
var newName = name.replace(/fields\[\d+\]/, 'fields[' + index + ']');
$(this).attr('name', newName);
});
});
},
initFieldToggle: function() {
$('.pcfb-toggle-field').off('click').on('click', function(e) {
e.preventDefault();
$(this).closest('.pcfb-field-item').toggleClass('collapsed');
});
},
initFieldDelete: function() {
var self = this;
$('.pcfb-delete-field').off('click').on('click', function(e) {
e.preventDefault();
var fieldItem = $(this).closest('.pcfb-field-item');
var fieldId = fieldItem.data('field-id');
if (confirm(pcfbAdminVars.i18n.confirmDeleteField)) {
if (fieldId && fieldId !== 'new') {
self.deletedFields.push(fieldId);
$('<input type="hidden" name="deleted_fields[]" value="' + fieldId + '">').appendTo('#pcfb-form-builder');
}
fieldItem.fadeOut(300, function() {
$(this).remove();
self.reindexFields();
self.checkEmptyFields();
});
}
});
},
checkEmptyFields: function() {
var container = $('#pcfb-fields-container');
if (container.find('.pcfb-field-item').length === 0) {
container.html('<div class="pcfb-empty-fields"><p>' + pcfbAdminVars.i18n.fieldLabel + '</p></div>');
}
},
updateLabelOnChange: function() {
$('.pcfb-field-label-input').off('input').on('input', function() {
var label = $(this).val();
var fieldItem = $(this).closest('.pcfb-field-item');
var fieldName = fieldItem.find('.pcfb-field-name-display');
var baseName = this.getAttribute('name').match(/fields\[(\d+)\]\[field_label\]/);
if (baseName) {
var slug = label.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '');
if (slug === '') {
slug = 'field_' + baseName[1];
}
fieldName.text(slug);
var hiddenName = fieldItem.find('input[name^="fields["][name$="[field_name]"]');
hiddenName.val(slug);
}
});
},
initDeleteModal: function() {
var self = this;
$(document).on('click', '.pcfb-delete-form, .pcfb-delete-form-btn, .pcfb-delete-form-link', function(e) {
if (e.target.tagName === 'BUTTON' || $(this).hasClass('pcfb-delete-form') || $(this).hasClass('pcfb-delete-form-btn') || $(this).hasClass('pcfb-delete-form-link')) {
e.preventDefault();
var formId = $(this).data('form-id');
var formName = $(this).data('form-name');
$('#pcfb-delete-form-id').val(formId);
$('#pcfb-delete-form-name').text(formName);
$('#pcfb-delete-modal').addClass('active').show();
}
});
$('.pcfb-modal-close, .pcfb-modal-cancel').on('click', function(e) {
e.preventDefault();
$('#pcfb-delete-modal').removeClass('active').hide();
});
$('#pcfb-delete-modal').on('click', function(e) {
if ($(e.target).is('#pcfb-delete-modal')) {
$(this).removeClass('active').hide();
}
});
},
initFormBuilder: function() {
var self = this;
if ($('#pcfb-fields-container').length > 0) {
$('#pcfb-fields-container').sortable({
handle: '.pcfb-field-header',
placeholder: 'pcfb-field-placeholder',
tolerance: 'pointer',
update: function() {
self.reindexFields();
}
});
}
if ($('#pcfb-sections-container').length > 0) {
$('#pcfb-sections-container').sortable({
handle: '.pcfb-section-header',
placeholder: 'pcfb-section-placeholder',
tolerance: 'pointer'
});
}
$('#pcfb-add-section').on('click', function(e) {
e.preventDefault();
self.addSection();
});
$('#pcfb-form-builder').on('submit', function(e) {
var formName = $('#form-name').val().trim();
if (!formName) {
e.preventDefault();
alert(pcfbAdminVars.i18n.fieldLabel);
$('#form-name').focus();
return false;
}
var hasRequiredFields = false;
$('.pcfb-field-item').each(function() {
if ($(this).find('[name*="[validation_rules][required]"]').is(':checked')) {
hasRequiredFields = true;
}
});
});
},
addSection: function() {
var container = $('#pcfb-sections-container');
var sectionCount = container.find('.pcfb-section-box').length;
var sectionHtml = '<div class="pcfb-section-box" data-section-id="new-' + sectionCount + '">' +
'<div class="pcfb-section-header">' +
'<span class="dashicons dashicons-menu"></span>' +
'<span class="pcfb-section-title">Section ' + (sectionCount + 1) + '</span>' +
'</div>' +
'<div class="pcfb-section-content">' +
'<input type="hidden" name="sections[' + sectionCount + '][id]" value="">' +
'<input type="hidden" name="sections[' + sectionCount + '][title]" value="Section ' + (sectionCount + 1) + '">' +
'<p class="description">Drag fields into this section or click to configure.</p>' +
'</div>' +
'</div>';
container.append(sectionHtml);
}
};
$(document).ready(function() {
PCFB_Admin.init();
});
})(jQuery);

View File

@@ -0,0 +1,195 @@
<div class="wrap pcfb-wrap">
<h1 class="wp-heading-inline">
<span class="pcfb-heading-icon dashicons dashicons-forms"></span>
<?php esc_html_e( 'All Forms', 'pc-form-builder-xyz123' ); ?>
</h1>
<a href="<?php echo admin_url( 'admin.php?page=pcfb-form-builder' ); ?>" class="page-title-action">
<span class="dashicons dashicons-plus"></span>
<?php esc_html_e( 'Add New Form', 'pc-form-builder-xyz123' ); ?>
</a>
<hr class="wp-header-end">
<?php
$forms = PC_Form_Handler::get_forms( array( 'limit' => 50, 'status' => '' ) );
$active_count = PC_Form_Handler::get_form_count( 'active' );
$inactive_count = PC_Form_Handler::get_form_count( 'inactive' );
$total_responses = 0;
foreach ( $forms as $form ) {
$total_responses += PC_Form_Handler::get_form_response_count( $form->id );
}
?>
<div class="pcfb-all-forms-page">
<div class="pcfb-stats-row">
<div class="pcfb-stat-card">
<div class="pcfb-stat-icon pcfb-stat-icon-blue">
<span class="dashicons dashicons-forms"></span>
</div>
<div class="pcfb-stat-content">
<span class="pcfb-stat-number"><?php echo esc_html( count( $forms ) ); ?></span>
<span class="pcfb-stat-label"><?php esc_html_e( 'Total Forms', 'pc-form-builder-xyz123' ); ?></span>
</div>
</div>
<div class="pcfb-stat-card">
<div class="pcfb-stat-icon pcfb-stat-icon-green">
<span class="dashicons dashicons-yes-alt"></span>
</div>
<div class="pcfb-stat-content">
<span class="pcfb-stat-number"><?php echo esc_html( $active_count ); ?></span>
<span class="pcfb-stat-label"><?php esc_html_e( 'Active Forms', 'pc-form-builder-xyz123' ); ?></span>
</div>
</div>
<div class="pcfb-stat-card">
<div class="pcfb-stat-icon pcfb-stat-icon-orange">
<span class="dashicons dashicons-hidden"></span>
</div>
<div class="pcfb-stat-content">
<span class="pcfb-stat-number"><?php echo esc_html( $inactive_count ); ?></span>
<span class="pcfb-stat-label"><?php esc_html_e( 'Inactive Forms', 'pc-form-builder-xyz123' ); ?></span>
</div>
</div>
<div class="pcfb-stat-card">
<div class="pcfb-stat-icon pcfb-stat-icon-purple">
<span class="dashicons dashicons-pressthis"></span>
</div>
<div class="pcfb-stat-content">
<span class="pcfb-stat-number"><?php echo esc_html( $total_responses ); ?></span>
<span class="pcfb-stat-label"><?php esc_html_e( 'Total Signups', 'pc-form-builder-xyz123' ); ?></span>
</div>
</div>
</div>
<?php if ( empty( $forms ) ) : ?>
<div class="pcfb-empty-state">
<span class="dashicons dashicons-forms pcfb-empty-icon"></span>
<h3><?php esc_html_e( 'No forms created yet', 'pc-form-builder-xyz123' ); ?></h3>
<p><?php esc_html_e( 'Create your first form to start collecting responses from your visitors.', 'pc-form-builder-xyz123' ); ?></p>
<a href="<?php echo admin_url( 'admin.php?page=pcfb-form-builder' ); ?>" class="button button-primary">
<?php esc_html_e( 'Create Your First Form', 'pc-form-builder-xyz123' ); ?>
</a>
</div>
<?php else : ?>
<div class="pcfb-forms-table-card">
<div class="pcfb-card-header pcfb-table-header">
<h2><?php esc_html_e( 'Your Forms', 'pc-form-builder-xyz123' ); ?></h2>
</div>
<table class="widefat pcfb-table pcfb-forms-table">
<thead>
<tr>
<th class="column-primary"><?php esc_html_e( 'Form Name', 'pc-form-builder-xyz123' ); ?></th>
<th><?php esc_html_e( 'Shortcode', 'pc-form-builder-xyz123' ); ?></th>
<th><?php esc_html_e( 'Fields', 'pc-form-builder-xyz123' ); ?></th>
<th><?php esc_html_e( 'Signups', 'pc-form-builder-xyz123' ); ?></th>
<th><?php esc_html_e( 'Status', 'pc-form-builder-xyz123' ); ?></th>
<th><?php esc_html_e( 'Created', 'pc-form-builder-xyz123' ); ?></th>
<th class="column-actions"><?php esc_html_e( 'Actions', 'pc-form-builder-xyz123' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $forms as $form ) : ?>
<?php
$field_count = pcfb_get_field_count( $form->id );
$response_count = PC_Form_Handler::get_form_response_count( $form->id );
$edit_url = admin_url( 'admin.php?page=pcfb-form-builder&form_id=' . $form->id );
$responses_url = admin_url( 'admin.php?page=pcfb-responses&form_id=' . $form->id );
$status_class = 'active' === $form->status ? 'status-active' : 'status-inactive';
$status_text = 'active' === $form->status ? __( 'Active', 'pc-form-builder-xyz123' ) : __( 'Inactive', 'pc-form-builder-xyz123' );
?>
<tr>
<td class="column-primary">
<strong>
<a href="<?php echo esc_url( $edit_url ); ?>">
<?php echo esc_html( $form->name ); ?>
</a>
</strong>
<div class="row-actions">
<span class="edit"><a href="<?php echo esc_url( $edit_url ); ?>"><?php esc_html_e( 'Edit', 'pc-form-builder-xyz123' ); ?></a> | </span>
<span class="responses"><a href="<?php echo esc_url( $responses_url ); ?>"><?php esc_html_e( 'Responses', 'pc-form-builder-xyz123' ); ?></a> | </span>
<span class="delete"><button type="button" class="pcfb-delete-form-link" data-form-id="<?php echo esc_attr( $form->id ); ?>" data-form-name="<?php echo esc_attr( $form->name ); ?>"><?php esc_html_e( 'Delete', 'pc-form-builder-xyz123' ); ?></button></span>
</div>
<?php if ( $form->description ) : ?>
<br>
<small class="description"><?php echo esc_html( $form->description ); ?></small>
<?php endif; ?>
</td>
<td data-colname="<?php esc_attr_e( 'Shortcode', 'pc-form-builder-xyz123' ); ?>">
<code class="pcfb-shortcode" onclick="this.select(); document.execCommand('copy');" title="<?php esc_attr_e( 'Click to copy', 'pc-form-builder-xyz123' ); ?>">
[pcfb-form id="<?php echo esc_attr( $form->id ); ?>"]
</code>
</td>
<td data-colname="<?php esc_attr_e( 'Fields', 'pc-form-builder-xyz123' ); ?>">
<span class="pcfb-field-badge"><?php echo esc_html( $field_count ); ?></span>
</td>
<td data-colname="<?php esc_attr_e( 'Signups', 'pc-form-builder-xyz123' ); ?>">
<?php if ( $response_count > 0 ) : ?>
<a href="<?php echo esc_url( $responses_url ); ?>" class="pcfb-signups-link">
<span class="pcfb-signup-count"><?php echo esc_html( $response_count ); ?></span>
</a>
<?php else : ?>
<span class="pcfb-signup-count zero"><?php echo esc_html( $response_count ); ?></span>
<?php endif; ?>
</td>
<td data-colname="<?php esc_attr_e( 'Status', 'pc-form-builder-xyz123' ); ?>">
<span class="pcfb-status <?php echo esc_attr( $status_class ); ?>">
<?php echo esc_html( $status_text ); ?>
</span>
</td>
<td data-colname="<?php esc_attr_e( 'Created', 'pc-form-builder-xyz123' ); ?>">
<?php echo esc_html( date_i18n( get_option( 'date_format' ), strtotime( $form->created_at ) ) ); ?>
</td>
<td class="column-actions" data-colname="<?php esc_attr_e( 'Actions', 'pc-form-builder-xyz123' ); ?>">
<div class="pcfb-actions">
<a href="<?php echo esc_url( $edit_url ); ?>" class="button button-small" title="<?php esc_attr_e( 'Edit Form', 'pc-form-builder-xyz123' ); ?>">
<span class="dashicons dashicons-edit"></span>
</a>
<a href="<?php echo esc_url( $responses_url ); ?>" class="button button-small" title="<?php esc_attr_e( 'View Responses', 'pc-form-builder-xyz123' ); ?>">
<span class="dashicons dashicons-pressthis"></span>
</a>
<button type="button" class="button button-small pcfb-delete-form" data-form-id="<?php echo esc_attr( $form->id ); ?>" data-form-name="<?php echo esc_attr( $form->name ); ?>" title="<?php esc_attr_e( 'Delete Form', 'pc-form-builder-xyz123' ); ?>">
<span class="dashicons dashicons-trash"></span>
</button>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<div id="pcfb-delete-modal" class="pcfb-modal" style="display: none;">
<div class="pcfb-modal-content">
<div class="pcfb-modal-header">
<h3><?php esc_html_e( 'Delete Form', 'pc-form-builder-xyz123' ); ?></h3>
<button type="button" class="pcfb-modal-close">&times;</button>
</div>
<div class="pcfb-modal-body">
<p><?php esc_html_e( 'Are you sure you want to delete this form? All responses will also be deleted. This action cannot be undone.', 'pc-form-builder-xyz123' ); ?></p>
<p><strong id="pcfb-delete-form-name"></strong></p>
</div>
<div class="pcfb-modal-footer">
<form method="post" id="pcfb-delete-form">
<?php wp_nonce_field( 'pcfb_admin_action', '_wpnonce' ); ?>
<input type="hidden" name="pcfb_action" value="delete_form">
<input type="hidden" name="form_id" id="pcfb-delete-form-id" value="">
<button type="button" class="button pcfb-modal-cancel"><?php esc_html_e( 'Cancel', 'pc-form-builder-xyz123' ); ?></button>
<button type="submit" class="button button-primary"><?php esc_html_e( 'Delete Form', 'pc-form-builder-xyz123' ); ?></button>
</form>
</div>
</div>
</div>
</div>
<?php
function pcfb_get_field_count( $form_id ) {
global $wpdb;
$table_name = $wpdb->prefix . 'pcfb_fields';
return $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table_name} WHERE form_id = %d", $form_id ) );
}
?>

View File

@@ -0,0 +1,199 @@
<div class="wrap pcfb-wrap">
<h1 class="wp-heading-inline">
<span class="pcfb-heading-icon dashicons dashicons-feedback"></span>
<?php esc_html_e( 'Form Builder', 'pc-form-builder-xyz123' ); ?>
</h1>
<a href="<?php echo admin_url( 'admin.php?page=pcfb-all-forms' ); ?>" class="page-title-action">
<span class="dashicons dashicons-list-view"></span>
<?php esc_html_e( 'All Forms', 'pc-form-builder-xyz123' ); ?>
</a>
<hr class="wp-header-end">
<?php
require_once PCFB_PLUGIN_DIR . 'admin/class-admin-helper.php';
$helper = PCFB_Admin_Helper::get_instance();
$form_id = isset( $_GET['form_id'] ) ? intval( $_GET['form_id'] ) : 0;
$is_new = $form_id === 0;
if ( ! $is_new ) {
$form = PC_Form_Handler::get_form_by_id( $form_id );
if ( ! $form ) {
echo '<div class="notice notice-error"><p>' . esc_html__( 'Form not found.', 'pc-form-builder-xyz123' ) . '</p></div>';
return;
}
$fields = $helper->get_form_fields( $form_id );
$settings = maybe_unserialize( $form->settings );
} else {
$form = (object) array(
'id' => 0,
'name' => '',
'description' => '',
'status' => 'active',
'settings' => array(),
);
$fields = array();
$settings = array();
}
$field_types = $helper->get_field_types();
?>
<?php if ( ! $is_new ) : ?>
<div class="pcfb-shortcode-notice" style="background: #f0f0f1; border: 1px solid #c3c4c7; border-radius: 6px; padding: 15px 20px; margin-bottom: 20px; display: flex; align-items: center; gap: 15px;">
<span class="dashicons dashicons-shortcode" style="color: #2271b1; font-size: 24px; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center;"></span>
<div style="flex: 1;">
<strong style="display: block; margin-bottom: 4px;"><?php esc_html_e( 'Form Shortcode', 'pc-form-builder-xyz123' ); ?></strong>
<code onclick="this.select(); document.execCommand('copy');" style="background: #fff; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 14px; display: inline-block;">[pcfb-form id="<?php echo esc_attr( $form->id ); ?>"]</code>
<span class="description" style="display: block; margin-top: 4px; font-size: 12px;"><?php esc_html_e( 'Click the shortcode to copy it', 'pc-form-builder-xyz123' ); ?></span>
</div>
</div>
<?php endif; ?>
<form method="post" id="pcfb-form-builder" class="pcfb-form-builder">
<?php wp_nonce_field( 'pcfb_admin_action', '_wpnonce' ); ?>
<input type="hidden" name="pcfb_action" value="save_form">
<input type="hidden" name="form[id]" value="<?php echo esc_attr( $form->id ); ?>">
<div class="pcfb-builder-layout">
<div class="pcfb-builder-main">
<div class="pcfb-card pcfb-form-settings-card">
<div class="pcfb-card-header">
<h2><?php esc_html_e( 'Form Details', 'pc-form-builder-xyz123' ); ?></h2>
</div>
<div class="pcfb-card-body">
<div class="pcfb-form-group">
<label for="form-name"><?php esc_html_e( 'Form Name', 'pc-form-builder-xyz123' ); ?> *</label>
<input type="text" id="form-name" name="form[name]" value="<?php echo esc_attr( $form->name ); ?>" required placeholder="<?php esc_attr_e( 'Enter form name', 'pc-form-builder-xyz123' ); ?>">
</div>
<div class="pcfb-form-group">
<label for="form-description"><?php esc_html_e( 'Description', 'pc-form-builder-xyz123' ); ?></label>
<textarea id="form-description" name="form[description]" rows="2" placeholder="<?php esc_attr_e( 'Brief description of this form', 'pc-form-builder-xyz123' ); ?>"><?php echo esc_textarea( $form->description ); ?></textarea>
</div>
<div class="pcfb-form-group">
<label for="form-status"><?php esc_html_e( 'Status', 'pc-form-builder-xyz123' ); ?></label>
<select id="form-status" name="form[status]">
<option value="active" <?php selected( $form->status, 'active' ); ?>><?php esc_html_e( 'Active', 'pc-form-builder-xyz123' ); ?></option>
<option value="inactive" <?php selected( $form->status, 'inactive' ); ?>><?php esc_html_e( 'Inactive', 'pc-form-builder-xyz123' ); ?></option>
</select>
</div>
</div>
</div>
<div class="pcfb-card pcfb-fields-card">
<div class="pcfb-card-header">
<h2><?php esc_html_e( 'Form Fields', 'pc-form-builder-xyz123' ); ?></h2>
</div>
<div class="pcfb-field-types-section">
<h3><?php esc_html_e( 'Add Fields to Your Form', 'pc-form-builder-xyz123' ); ?></h3>
<p class="description"><?php esc_html_e( 'Click on any field type below to add it to your form. You can then customize the label, placeholder, and other options.', 'pc-form-builder-xyz123' ); ?></p>
<div class="pcfb-field-type-buttons">
<?php foreach ( $field_types as $type => $label ) : ?>
<button type="button" class="pcfb-add-field-btn" data-field-type="<?php echo esc_attr( $type ); ?>">
<span class="dashicons <?php echo esc_attr( PCFB_Field_Icons::get_field_icon( $type ) ); ?>"></span>
<span class="pcfb-field-label"><?php echo esc_html( $label ); ?></span>
</button>
<?php endforeach; ?>
</div>
</div>
<div class="pcfb-form-fields">
<h3><?php esc_html_e( 'Form Structure', 'pc-form-builder-xyz123' ); ?></h3>
<p class="description"><?php esc_html_e( 'Drag and drop fields to reorder them. Click on a field to expand and edit its properties.', 'pc-form-builder-xyz123' ); ?></p>
<div id="pcfb-fields-container" class="pcfb-fields-container">
<?php if ( empty( $fields ) ) : ?>
<div class="pcfb-empty-fields">
<span class="dashicons dashicons-plus-alt2 pcfb-empty-icon"></span>
<p><?php esc_html_e( 'No fields added yet.', 'pc-form-builder-xyz123' ); ?></p>
<p class="description"><?php esc_html_e( 'Click a field type above to start building your form.', 'pc-form-builder-xyz123' ); ?></p>
</div>
<?php else : ?>
<?php foreach ( $fields as $index => $field ) : ?>
<?php echo $helper->render_field_item( $field, $index ); ?>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</div>
<div class="pcfb-card pcfb-sections-card">
<div class="pcfb-card-header">
<h2><?php esc_html_e( 'Form Sections', 'pc-form-builder-xyz123' ); ?></h2>
</div>
<div class="pcfb-card-body">
<p class="description"><?php esc_html_e( 'Organize your form fields into sections for better user experience.', 'pc-form-builder-xyz123' ); ?></p>
<div id="pcfb-sections-container" class="pcfb-sections-container">
<div class="pcfb-section-box pcfb-default-section">
<div class="pcfb-section-header">
<span class="dashicons dashicons-menu"></span>
<span class="pcfb-section-title"><?php esc_html_e( 'Default Section', 'pc-form-builder-xyz123' ); ?></span>
</div>
<div class="pcfb-section-content">
<p class="description"><?php esc_html_e( 'All fields are placed in this default section by default. You can add section breaks between fields.', 'pc-form-builder-xyz123' ); ?></p>
</div>
</div>
</div>
<button type="button" class="button pcfb-add-section-btn" id="pcfb-add-section">
<span class="dashicons dashicons-plus"></span>
<?php esc_html_e( 'Add Section', 'pc-form-builder-xyz123' ); ?>
</button>
</div>
</div>
</div>
<div class="pcfb-builder-sidebar">
<div class="pcfb-card pcfb-publish-card">
<div class="pcfb-card-header">
<h2><?php esc_html_e( 'Publish', 'pc-form-builder-xyz123' ); ?></h2>
</div>
<div class="pcfb-card-body">
<button type="submit" class="button button-primary button-large pcfb-save-btn">
<span class="dashicons dashicons-saved"></span>
<?php esc_html_e( 'Save Form', 'pc-form-builder-xyz123' ); ?>
</button>
<a href="<?php echo admin_url( 'admin.php?page=pcfb-all-forms' ); ?>" class="button button-large">
<?php esc_html_e( 'Cancel', 'pc-form-builder-xyz123' ); ?>
</a>
</div>
</div>
<div class="pcfb-card pcfb-settings-card">
<div class="pcfb-card-header">
<h2><?php esc_html_e( 'Form Settings', 'pc-form-builder-xyz123' ); ?></h2>
</div>
<div class="pcfb-card-body">
<div class="pcfb-form-group">
<label for="form-success-message"><?php esc_html_e( 'Success Message', 'pc-form-builder-xyz123' ); ?></label>
<textarea id="form-success-message" name="form[settings][success_message]" rows="3" placeholder="<?php esc_attr_e( 'Thank you! Your response has been submitted.', 'pc-form-builder-xyz123' ); ?>"><?php echo isset( $settings['success_message'] ) ? esc_textarea( $settings['success_message'] ) : ''; ?></textarea>
</div>
<div class="pcfb-form-group">
<label for="form-submit-text"><?php esc_html_e( 'Submit Button Text', 'pc-form-builder-xyz123' ); ?></label>
<input type="text" id="form-submit-text" name="form[settings][submit_text]" value="<?php echo isset( $settings['submit_text'] ) ? esc_attr( $settings['submit_text'] ) : __( 'Submit', 'pc-form-builder-xyz123' ); ?>">
</div>
</div>
</div>
<div class="pcfb-card pcfb-help-card">
<div class="pcfb-card-header">
<h2><?php esc_html_e( 'Quick Tips', 'pc-form-builder-xyz123' ); ?></h2>
</div>
<div class="pcfb-card-body">
<p><strong><?php esc_html_e( 'Available Field Types:', 'pc-form-builder-xyz123' ); ?></strong></p>
<ul>
<li><span class="dashicons dashicons-text"></span> <strong><?php esc_html_e( 'Text', 'pc-form-builder-xyz123' ); ?></strong> - <?php esc_html_e( 'Single line text input', 'pc-form-builder-xyz123' ); ?></li>
<li><span class="dashicons dashicons-email"></span> <strong><?php esc_html_e( 'Email', 'pc-form-builder-xyz123' ); ?></strong> - <?php esc_html_e( 'Email address with validation', 'pc-form-builder-xyz123' ); ?></li>
<li><span class="dashicons dashicons-admin-users"></span> <strong><?php esc_html_e( 'Name', 'pc-form-builder-xyz123' ); ?></strong> - <?php esc_html_e( 'Full name field', 'pc-form-builder-xyz123' ); ?></li>
<li><span class="dashicons dashicons-editor-quote"></span> <strong><?php esc_html_e( 'Response', 'pc-form-builder-xyz123' ); ?></strong> - <?php esc_html_e( 'Multi-line textarea for responses', 'pc-form-builder-xyz123' ); ?></li>
<li><span class="dashicons dashicons-arrow-down-alt2"></span> <strong><?php esc_html_e( 'Dropdown', 'pc-form-builder-xyz123' ); ?></strong> - <?php esc_html_e( 'Select from predefined options', 'pc-form-builder-xyz123' ); ?></li>
<li><span class="dashicons dashicons-yes"></span> <strong><?php esc_html_e( 'Checkbox', 'pc-form-builder-xyz123' ); ?></strong> - <?php esc_html_e( 'Checkbox options', 'pc-form-builder-xyz123' ); ?></li>
</ul>
<hr>
<p><?php esc_html_e( 'After saving, use the shortcode to embed this form on any page or post.', 'pc-form-builder-xyz123' ); ?></p>
</div>
</div>
</div>
</div>
</form>
</div>

View File

@@ -0,0 +1,111 @@
<div class="wrap pcfb-wrap">
<?php
$form_id = isset( $_GET['form_id'] ) ? intval( $_GET['form_id'] ) : 0;
if ( empty( $form_id ) ) {
echo '<div class="notice notice-error"><p>' . esc_html__( 'Form ID is required.', 'pc-form-builder-xyz123' ) . '</p></div>';
return;
}
$form = PC_Form_Handler::get_form_by_id( $form_id );
if ( ! $form ) {
echo '<div class="notice notice-error"><p>' . esc_html__( 'Form not found.', 'pc-form-builder-xyz123' ) . '</p></div>';
return;
}
$paged = isset( $_GET['paged'] ) ? max( 1, intval( $_GET['paged'] ) ) : 1;
$per_page = 20;
$offset = ( $paged - 1 ) * $per_page;
$responses = PC_Form_Handler::get_form_responses( $form_id, array(
'limit' => $per_page,
'offset' => $offset,
) );
$total_responses = PC_Form_Handler::get_form_response_count( $form_id );
$total_pages = ceil( $total_responses / $per_page );
?>
<h1 class="wp-heading-inline">
<a href="<?php echo admin_url( 'admin.php?page=pcfb-responses' ); ?>" class="pcfb-back-link">
<span class="dashicons dashicons-arrow-left-alt2"></span>
</a>
<span class="pcfb-heading-icon dashicons dashicons-pressthis"></span>
<?php echo esc_html( $form->name ); ?> - <?php esc_html_e( 'Responses', 'pc-form-builder-xyz123' ); ?>
</h1>
<span class="title-ext">
<?php echo esc_html( $total_responses . ' ' . _n( 'Response', 'Responses', $total_responses, 'pc-form-builder-xyz123' ) ); ?>
</span>
<hr class="wp-header-end">
<div class="pcfb-form-responses-page">
<?php if ( empty( $responses ) ) : ?>
<div class="pcfb-empty-state">
<span class="dashicons dashicons-pressthis pcfb-empty-icon"></span>
<h3><?php esc_html_e( 'No responses yet', 'pc-form-builder-xyz123' ); ?></h3>
<p><?php esc_html_e( 'This form has not received any responses yet.', 'pc-form-builder-xyz123' ); ?></p>
</div>
<?php else : ?>
<div class="pcfb-responses-header">
<div class="pcfb-response-count-badge">
<span class="dashicons dashicons-pressthis"></span>
<?php echo esc_html( $total_responses . ' ' . _n( 'Response', 'Responses', $total_responses, 'pc-form-builder-xyz123' ) ); ?>
</div>
</div>
<table class="widefat pcfb-table pcfb-responses-table">
<thead>
<tr>
<th><?php esc_html_e( 'ID', 'pc-form-builder-xyz123' ); ?></th>
<th><?php esc_html_e( 'Submitted', 'pc-form-builder-xyz123' ); ?></th>
<th><?php esc_html_e( 'IP Address', 'pc-form-builder-xyz123' ); ?></th>
<th><?php esc_html_e( 'Status', 'pc-form-builder-xyz123' ); ?></th>
<th><?php esc_html_e( 'Actions', 'pc-form-builder-xyz123' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $responses as $response ) : ?>
<?php
$detail_url = admin_url( 'admin.php?page=pcfb-responses&form_id=' . $form_id . '&response_id=' . $response->id );
$status_class = 'new' === $response->status ? 'status-new' : 'status-read';
$status_label = 'new' === $response->status ? __( 'New', 'pc-form-builder-xyz123' ) : __( 'Read', 'pc-form-builder-xyz123' );
?>
<tr>
<td><?php echo esc_html( $response->id ); ?></td>
<td><?php echo esc_html( date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $response->created_at ) ) ); ?></td>
<td><?php echo esc_html( $response->user_ip ); ?></td>
<td>
<span class="pcfb-status <?php echo esc_attr( $status_class ); ?>">
<?php echo esc_html( $status_label ); ?>
</span>
</td>
<td>
<a href="<?php echo esc_url( $detail_url ); ?>" class="button button-small">
<?php esc_html_e( 'View Details', 'pc-form-builder-xyz123' ); ?>
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php if ( $total_pages > 1 ) : ?>
<div class="pcfb-pagination">
<?php
echo paginate_links( array(
'base' => admin_url( 'admin.php?page=pcfb-responses&form_id=' . $form_id . '&paged=%#%' ),
'format' => '',
'prev_text' => __( '&laquo; Previous', 'pc-form-builder-xyz123' ),
'next_text' => __( 'Next &raquo;', 'pc-form-builder-xyz123' ),
'total' => $total_pages,
'current' => $paged,
) );
?>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
</div>

View File

@@ -0,0 +1,118 @@
<div class="wrap pcfb-wrap">
<?php
$form_id = isset( $_GET['form_id'] ) ? intval( $_GET['form_id'] ) : 0;
$response_id = isset( $_GET['response_id'] ) ? intval( $_GET['response_id'] ) : 0;
if ( empty( $form_id ) || empty( $response_id ) ) {
echo '<div class="notice notice-error"><p>' . esc_html__( 'Invalid request.', 'pc-form-builder-xyz123' ) . '</p></div>';
return;
}
$form = PC_Form_Handler::get_form_by_id( $form_id );
if ( ! $form ) {
echo '<div class="notice notice-error"><p>' . esc_html__( 'Form not found.', 'pc-form-builder-xyz123' ) . '</p></div>';
return;
}
$response_data = PC_Form_Handler::get_response_data( $response_id );
if ( empty( $response_data ) ) {
echo '<div class="notice notice-error"><p>' . esc_html__( 'Response not found.', 'pc-form-builder-xyz123' ) . '</p></div>';
return;
}
$back_url = admin_url( 'admin.php?page=pcfb-responses&form_id=' . $form_id );
$response_meta = pcfb_get_response_meta( $response_id );
?>
<h1 class="wp-heading-inline">
<a href="<?php echo esc_url( $back_url ); ?>" class="pcfb-back-link">
<span class="dashicons dashicons-arrow-left-alt2"></span>
</a>
<span class="pcfb-heading-icon dashicons dashicons-pressthis"></span>
<?php esc_html_e( 'Response Details', 'pc-form-builder-xyz123' ); ?>
</h1>
<span class="title-ext">
<?php esc_html_e( 'Response #', 'pc-form-builder-xyz123' ); ?><?php echo esc_html( $response_id ); ?>
</span>
<hr class="wp-header-end">
<div class="pcfb-response-detail-page">
<div class="pcfb-response-meta">
<div class="pcfb-card">
<div class="pcfb-card-header">
<h2><?php esc_html_e( 'Response Information', 'pc-form-builder-xyz123' ); ?></h2>
</div>
<div class="pcfb-card-body">
<table class="widefat pcfb-meta-table">
<tr>
<th><?php esc_html_e( 'Form:', 'pc-form-builder-xyz123' ); ?></th>
<td><?php echo esc_html( $form->name ); ?></td>
</tr>
<tr>
<th><?php esc_html_e( 'Submitted:', 'pc-form-builder-xyz123' ); ?></th>
<td><?php echo esc_html( date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $response_meta['created_at'] ) ) ); ?></td>
</tr>
<tr>
<th><?php esc_html_e( 'IP Address:', 'pc-form-builder-xyz123' ); ?></th>
<td><?php echo esc_html( $response_meta['user_ip'] ); ?></td>
</tr>
<tr>
<th><?php esc_html_e( 'User Agent:', 'pc-form-builder-xyz123' ); ?></th>
<td><?php echo esc_html( $response_meta['user_agent'] ); ?></td>
</tr>
</table>
</div>
</div>
</div>
<div class="pcfb-response-content">
<div class="pcfb-card">
<div class="pcfb-card-header">
<h2><?php esc_html_e( 'Submitted Data', 'pc-form-builder-xyz123' ); ?></h2>
</div>
<div class="pcfb-card-body">
<?php foreach ( $response_data as $data ) : ?>
<div class="pcfb-response-field">
<div class="pcfb-response-field-label">
<?php echo esc_html( $data->field_label ); ?>
</div>
<div class="pcfb-response-field-value">
<?php
if ( 'checkbox' === $data->field_type ) {
$options = maybe_unserialize( $data->field_value );
if ( is_array( $options ) ) {
echo esc_html( implode( ', ', $options ) );
} else {
echo esc_html( $data->field_value );
}
} elseif ( 'response' === $data->field_type ) {
echo wpautop( esc_html( $data->field_value ) );
} else {
echo esc_html( $data->field_value );
}
?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
</div>
<?php
function pcfb_get_response_meta( $response_id ) {
global $wpdb;
$table_name = $wpdb->prefix . 'pcfb_responses';
$response = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table_name} WHERE id = %d", $response_id ), ARRAY_A );
return $response ? $response : array(
'user_ip' => '',
'user_agent' => '',
'created_at' => '',
);
}
?>

View File

@@ -0,0 +1,74 @@
<div class="wrap pcfb-wrap">
<h1 class="wp-heading-inline">
<span class="pcfb-heading-icon dashicons dashicons-pressthis"></span>
<?php esc_html_e( 'Form Responses', 'pc-form-builder-xyz123' ); ?>
</h1>
<hr class="wp-header-end">
<?php
$forms = PC_Form_Handler::get_forms( array( 'limit' => 100 ) );
?>
<div class="pcfb-responses-page">
<?php if ( empty( $forms ) ) : ?>
<div class="pcfb-empty-state">
<span class="dashicons dashicons-pressthis pcfb-empty-icon"></span>
<h3><?php esc_html_e( 'No forms to show responses', 'pc-form-builder-xyz123' ); ?></h3>
<p><?php esc_html_e( 'Create a form first to start collecting and viewing responses.', 'pc-form-builder-xyz123' ); ?></p>
<a href="<?php echo admin_url( 'admin.php?page=pcfb-form-builder&form_id=new' ); ?>" class="button button-primary">
<?php esc_html_e( 'Create a Form', 'pc-form-builder-xyz123' ); ?>
</a>
</div>
<?php else : ?>
<div class="pcfb-responses-overview">
<h2><?php esc_html_e( 'Select a Form', 'pc-form-builder-xyz123' ); ?></h2>
<p class="description"><?php esc_html_e( 'Choose a form to view its responses.', 'pc-form-builder-xyz123' ); ?></p>
<div class="pcfb-forms-select-list">
<?php foreach ( $forms as $form ) : ?>
<?php
$response_count = PC_Form_Handler::get_form_response_count( $form->id );
$view_url = admin_url( 'admin.php?page=pcfb-responses&form_id=' . $form->id );
$status_class = 'active' === $form->status ? 'status-active' : 'status-inactive';
?>
<div class="pcfb-form-select-item">
<div class="pcfb-form-select-info">
<h3>
<a href="<?php echo esc_url( $view_url ); ?>">
<?php echo esc_html( $form->name ); ?>
</a>
</h3>
<span class="pcfb-status <?php echo esc_attr( $status_class ); ?>">
<?php echo esc_html( 'active' === $form->status ? __( 'Active', 'pc-form-builder-xyz123' ) : __( 'Inactive', 'pc-form-builder-xyz123' ) ); ?>
</span>
</div>
<div class="pcfb-form-select-stats">
<span class="pcfb-response-count">
<strong><?php echo esc_html( $response_count ); ?></strong>
<?php echo _n( 'Response', 'Responses', $response_count, 'pc-form-builder-xyz123' ); ?>
</span>
<a href="<?php echo esc_url( $view_url ); ?>" class="button button-small">
<?php esc_html_e( 'View Responses', 'pc-form-builder-xyz123' ); ?>
</a>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<div class="pcfb-total-responses">
<?php
$total_responses = 0;
foreach ( $forms as $form ) {
$total_responses += PC_Form_Handler::get_form_response_count( $form->id );
}
?>
<div class="pcfb-total-card">
<span class="pcfb-total-number"><?php echo esc_html( $total_responses ); ?></span>
<span class="pcfb-total-label"><?php esc_html_e( 'Total Responses Across All Forms', 'pc-form-builder-xyz123' ); ?></span>
</div>
</div>
<?php endif; ?>
</div>
</div>

View File

@@ -0,0 +1,131 @@
<?php
/**
* Database Migration Class
*
* Handles creation and management of database tables for forms and responses.
*
* @package PCFormBuilder
*/
class PC_DB_Migration {
private static $charset_collate = '';
public static function activate() {
self::set_charset();
self::create_forms_table();
self::create_fields_table();
self::create_responses_table();
self::create_response_data_table();
update_option( 'pcfb_version', PCFB_VERSION );
}
public static function deactivate() {
// Cleanup if needed
}
private static function set_charset() {
global $wpdb;
self::$charset_collate = $wpdb->get_charset_collate();
}
private static function create_forms_table() {
global $wpdb;
$table_name = $wpdb->prefix . 'pcfb_forms';
$sql = "CREATE TABLE {$table_name} (
id mediumint(9) NOT NULL AUTO_INCREMENT,
name varchar(255) NOT NULL,
description text NOT NULL,
settings longtext NOT NULL,
status varchar(20) DEFAULT 'active' NOT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL,
PRIMARY KEY (id)
) " . self::$charset_collate . ";";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( $sql );
}
private static function create_fields_table() {
global $wpdb;
$table_name = $wpdb->prefix . 'pcfb_fields';
$sql = "CREATE TABLE {$table_name} (
id mediumint(9) NOT NULL AUTO_INCREMENT,
form_id mediumint(9) NOT NULL,
field_type varchar(50) NOT NULL,
field_label varchar(255) NOT NULL,
field_name varchar(255) NOT NULL,
placeholder varchar(255) DEFAULT '',
options longtext DEFAULT NULL,
validation_rules longtext DEFAULT NULL,
sort_order mediumint(9) DEFAULT 0 NOT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
PRIMARY KEY (id),
KEY form_id (form_id)
) " . self::$charset_collate . ";";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( $sql );
}
private static function create_responses_table() {
global $wpdb;
$table_name = $wpdb->prefix . 'pcfb_responses';
$sql = "CREATE TABLE {$table_name} (
id mediumint(9) NOT NULL AUTO_INCREMENT,
form_id mediumint(9) NOT NULL,
user_ip varchar(100) DEFAULT '',
user_agent text DEFAULT '',
status varchar(20) DEFAULT 'new' NOT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
PRIMARY KEY (id),
KEY form_id (form_id)
) " . self::$charset_collate . ";";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( $sql );
}
private static function create_response_data_table() {
global $wpdb;
$table_name = $wpdb->prefix . 'pcfb_response_data';
$sql = "CREATE TABLE {$table_name} (
id mediumint(9) NOT NULL AUTO_INCREMENT,
response_id mediumint(9) NOT NULL,
field_id mediumint(9) NOT NULL,
field_value longtext NOT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
PRIMARY KEY (id),
KEY response_id (response_id),
KEY field_id (field_id)
) " . self::$charset_collate . ";";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( $sql );
}
public static function drop_tables() {
global $wpdb;
$tables = array(
$wpdb->prefix . 'pcfb_response_data',
$wpdb->prefix . 'pcfb_responses',
$wpdb->prefix . 'pcfb_fields',
$wpdb->prefix . 'pcfb_forms',
);
foreach ( $tables as $table ) {
$wpdb->query( "DROP TABLE IF EXISTS {$table}" );
}
}
}

View File

@@ -0,0 +1,402 @@
<?php
/**
* Form Handler Class
*
* Handles form submission, validation, and storage.
*
* @package PCFormBuilder
*/
class PC_Form_Handler {
private static $instance = null;
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
add_action( 'wp_ajax_pcfb_submit_form', array( $this, 'handle_form_submission' ) );
add_action( 'wp_ajax_nopriv_pcfb_submit_form', array( $this, 'handle_form_submission' ) );
add_action( 'wp_ajax_pcfb_add_field', array( $this, 'handle_add_field' ) );
add_shortcode( 'pcfb-form', array( $this, 'shortcode_form' ) );
}
public function handle_add_field() {
check_ajax_referer( 'pcfb_admin_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => __( 'Permission denied.', 'pc-form-builder-xyz123' ) ) );
}
$field_type = isset( $_POST['field_type'] ) ? sanitize_text_field( $_POST['field_type'] ) : '';
$field_index = isset( $_POST['field_index'] ) ? intval( $_POST['field_index'] ) : 0;
if ( empty( $field_type ) ) {
wp_send_json_error( array( 'message' => __( 'Field type is required.', 'pc-form-builder-xyz123' ) ) );
}
require_once PCFB_PLUGIN_DIR . 'admin/class-admin-helper.php';
$helper = PCFB_Admin_Helper::get_instance();
ob_start();
$helper->render_new_field_item( $field_type, $field_index );
$html = ob_get_clean();
wp_send_json_success( array( 'html' => $html ) );
}
public function shortcode_form( $atts ) {
$atts = shortcode_atts( array(
'id' => 0,
), $atts );
if ( empty( $atts['id'] ) ) {
return '<p>' . esc_html__( 'Form ID is required.', 'pc-form-builder-xyz123' ) . '</p>';
}
$form = $this->get_form( intval( $atts['id'] ) );
if ( ! $form ) {
return '<p>' . esc_html__( 'Form not found.', 'pc-form-builder-xyz123' ) . '</p>';
}
if ( 'active' !== $form->status ) {
return '<p>' . esc_html__( 'This form is not currently active.', 'pc-form-builder-xyz123' ) . '</p>';
}
$fields = $this->get_form_fields( $form->id );
$settings = maybe_unserialize( $form->settings );
ob_start();
include PCFB_PLUGIN_DIR . 'public/templates/form-display.php';
return ob_get_clean();
}
private function get_form( $form_id ) {
global $wpdb;
$table_name = $wpdb->prefix . 'pcfb_forms';
return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table_name} WHERE id = %d", $form_id ) );
}
private function get_form_fields( $form_id ) {
global $wpdb;
$table_name = $wpdb->prefix . 'pcfb_fields';
return $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$table_name} WHERE form_id = %d ORDER BY sort_order ASC", $form_id ) );
}
public function handle_form_submission() {
check_ajax_referer( 'pcfb_public_nonce', 'nonce' );
$form_id = isset( $_POST['form_id'] ) ? intval( $_POST['form_id'] ) : 0;
$form_data = isset( $_POST['form_data'] ) ? $_POST['form_data'] : array();
if ( empty( $form_id ) ) {
wp_send_json_error( array( 'message' => __( 'Invalid form ID.', 'pc-form-builder-xyz123' ) ) );
}
$form = $this->get_form( $form_id );
if ( ! $form ) {
wp_send_json_error( array( 'message' => __( 'Form not found.', 'pc-form-builder-xyz123' ) ) );
}
$fields = $this->get_form_fields( $form_id );
$errors = array();
$clean_data = array();
foreach ( $fields as $field ) {
$value = isset( $form_data[ $field->field_name ] ) ? trim( $form_data[ $field->field_name ] ) : '';
$validation_rules = maybe_unserialize( $field->validation_rules );
if ( ! empty( $validation_rules['required'] ) && empty( $value ) ) {
$errors[ $field->field_name ] = sprintf(
__( '%s is required.', 'pc-form-builder-xyz123' ),
$field->field_label
);
}
if ( ! empty( $value ) && 'email' === $field->field_type ) {
if ( ! is_email( $value ) ) {
$errors[ $field->field_name ] = __( 'Please enter a valid email address.', 'pc-form-builder-xyz123' );
}
}
$clean_data[ $field->id ] = sanitize_text_field( $value );
}
if ( ! empty( $errors ) ) {
wp_send_json_error( array( 'message' => __( 'Please fix the errors below.', 'pc-form-builder-xyz123' ), 'errors' => $errors ) );
}
$response_id = $this->save_response( $form_id, $clean_data );
if ( $response_id ) {
$success_message = ! empty( $settings['success_message'] )
? $settings['success_message']
: __( 'Thank you! Your response has been submitted.', 'pc-form-builder-xyz123' );
wp_send_json_success( array( 'message' => $success_message, 'response_id' => $response_id ) );
} else {
wp_send_json_error( array( 'message' => __( 'Failed to save your response. Please try again.', 'pc-form-builder-xyz123' ) ) );
}
}
private function save_response( $form_id, $field_data ) {
global $wpdb;
$responses_table = $wpdb->prefix . 'pcfb_responses';
$response_data_table = $wpdb->prefix . 'pcfb_response_data';
$user_ip = $_SERVER['REMOTE_ADDR'] ?? '';
$user_agent = $_SERVER['HTTP_USER_AGENT'] ?? '';
$wpdb->insert(
$responses_table,
array(
'form_id' => $form_id,
'user_ip' => $user_ip,
'user_agent'=> $user_agent,
'status' => 'new',
)
);
if ( ! $wpdb->insert_id ) {
return false;
}
$response_id = $wpdb->insert_id;
foreach ( $field_data as $field_id => $value ) {
$wpdb->insert(
$response_data_table,
array(
'response_id' => $response_id,
'field_id' => $field_id,
'field_value' => $value,
)
);
}
return $response_id;
}
public static function get_forms( $args = array() ) {
global $wpdb;
$defaults = array(
'status' => 'active',
'orderby' => 'created_at',
'order' => 'DESC',
'limit' => -1,
);
$args = wp_parse_args( $args, $defaults );
$table_name = $wpdb->prefix . 'pcfb_forms';
$sql = "SELECT * FROM {$table_name}";
if ( ! empty( $args['status'] ) ) {
$sql .= $wpdb->prepare( " WHERE status = %s", $args['status'] );
}
$sql .= " ORDER BY {$args['orderby']} {$args['order']}";
if ( $args['limit'] > 0 ) {
$sql .= $wpdb->prepare( " LIMIT %d", $args['limit'] );
}
return $wpdb->get_results( $sql );
}
public static function get_form_by_id( $form_id ) {
global $wpdb;
$table_name = $wpdb->prefix . 'pcfb_forms';
return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table_name} WHERE id = %d", $form_id ) );
}
public static function get_form_count( $status = 'active' ) {
global $wpdb;
$table_name = $wpdb->prefix . 'pcfb_forms';
return $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table_name} WHERE status = %s", $status ) );
}
public static function get_form_response_count( $form_id ) {
global $wpdb;
$table_name = $wpdb->prefix . 'pcfb_responses';
return $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table_name} WHERE form_id = %d", $form_id ) );
}
public static function get_form_responses( $form_id, $args = array() ) {
global $wpdb;
$defaults = array(
'limit' => 20,
'offset' => 0,
'orderby' => 'created_at',
'order' => 'DESC',
);
$args = wp_parse_args( $args, $defaults );
$responses_table = $wpdb->prefix . 'pcfb_responses';
$response_data_table = $wpdb->prefix . 'pcfb_response_data';
$fields_table = $wpdb->prefix . 'pcfb_fields';
$sql = $wpdb->prepare(
"SELECT r.* FROM {$responses_table} r
WHERE r.form_id = %d
ORDER BY r.{$args['orderby']} {$args['order']}
LIMIT %d OFFSET %d",
$form_id,
$args['limit'],
$args['offset']
);
return $wpdb->get_results( $sql );
}
public static function get_response_data( $response_id ) {
global $wpdb;
$response_data_table = $wpdb->prefix . 'pcfb_response_data';
$fields_table = $wpdb->prefix . 'pcfb_fields';
return $wpdb->get_results(
$wpdb->prepare(
"SELECT rd.*, f.field_name, f.field_label, f.field_type
FROM {$response_data_table} rd
JOIN {$fields_table} f ON rd.field_id = f.id
WHERE rd.response_id = %d",
$response_id
)
);
}
public static function delete_form( $form_id ) {
global $wpdb;
$forms_table = $wpdb->prefix . 'pcfb_forms';
$fields_table = $wpdb->prefix . 'pcfb_fields';
$responses_table = $wpdb->prefix . 'pcfb_responses';
$response_data_table = $wpdb->prefix . 'pcfb_response_data';
$response_ids = $wpdb->get_col( $wpdb->prepare( "SELECT id FROM {$responses_table} WHERE form_id = %d", $form_id ) );
if ( ! empty( $response_ids ) ) {
$response_ids_placeholder = implode( ',', array_map( 'intval', $response_ids ) );
$wpdb->query( "DELETE FROM {$response_data_table} WHERE response_id IN ({$response_ids_placeholder})" );
$wpdb->query( "DELETE FROM {$responses_table} WHERE form_id = {$form_id}" );
}
$wpdb->query( "DELETE FROM {$fields_table} WHERE form_id = {$form_id}" );
return $wpdb->delete( $forms_table, array( 'id' => $form_id ) );
}
public function save_form( $data, $fields = array() ) {
global $wpdb;
$forms_table = $wpdb->prefix . 'pcfb_forms';
$fields_table = $wpdb->prefix . 'pcfb_fields';
$form_data = array(
'name' => sanitize_text_field( $data['name'] ),
'description' => sanitize_text_field( $data['description'] ),
'settings' => maybe_serialize( isset( $data['settings'] ) ? $data['settings'] : array() ),
'status' => isset( $data['status'] ) ? sanitize_text_field( $data['status'] ) : 'active',
);
if ( ! empty( $data['id'] ) ) {
$form_id = intval( $data['id'] );
$wpdb->update( $forms_table, $form_data, array( 'id' => $form_id ) );
$existing_fields = $wpdb->get_col( $wpdb->prepare( "SELECT id FROM {$fields_table} WHERE form_id = %d", $form_id ) );
$existing_field_ids = ! empty( $existing_fields ) ? array_map( 'intval', $existing_fields ) : array();
$submitted_field_ids = array();
$sort_order = 0;
if ( ! empty( $fields ) && is_array( $fields ) ) {
foreach ( $fields as $field ) {
$field_data = array(
'form_id' => $form_id,
'field_type' => sanitize_text_field( $field['field_type'] ),
'field_label' => sanitize_text_field( $field['field_label'] ),
'field_name' => $this->generate_field_name( $field['field_label'], $form_id, isset( $field['id'] ) ? $field['id'] : 0 ),
'placeholder' => isset( $field['placeholder'] ) ? sanitize_text_field( $field['placeholder'] ) : '',
'options' => isset( $field['options'] ) ? maybe_serialize( $field['options'] ) : '',
'validation_rules' => isset( $field['validation_rules'] ) ? maybe_serialize( $field['validation_rules'] ) : '',
'sort_order' => $sort_order++,
);
if ( ! empty( $field['id'] ) ) {
$submitted_field_ids[] = intval( $field['id'] );
$wpdb->update( $fields_table, $field_data, array( 'id' => intval( $field['id'] ) ) );
} else {
$wpdb->insert( $fields_table, $field_data );
$submitted_field_ids[] = $wpdb->insert_id;
}
}
}
foreach ( $existing_field_ids as $field_id ) {
if ( ! in_array( $field_id, $submitted_field_ids ) ) {
$wpdb->delete( $fields_table, array( 'id' => $field_id ) );
}
}
return $form_id;
} else {
$wpdb->insert( $forms_table, $form_data );
$form_id = $wpdb->insert_id;
if ( $form_id && ! empty( $fields ) && is_array( $fields ) ) {
$sort_order = 0;
foreach ( $fields as $field ) {
$field_data = array(
'form_id' => $form_id,
'field_type' => sanitize_text_field( $field['field_type'] ),
'field_label' => sanitize_text_field( $field['field_label'] ),
'field_name' => $this->generate_field_name( $field['field_label'], $form_id ),
'placeholder' => isset( $field['placeholder'] ) ? sanitize_text_field( $field['placeholder'] ) : '',
'options' => isset( $field['options'] ) ? maybe_serialize( $field['options'] ) : '',
'validation_rules' => isset( $field['validation_rules'] ) ? maybe_serialize( $field['validation_rules'] ) : '',
'sort_order' => $sort_order++,
);
$wpdb->insert( $fields_table, $field_data );
}
}
return $form_id;
}
}
private function generate_field_name( $label, $form_id, $existing_id = 0 ) {
$base_name = sanitize_title( $label );
$base_name = preg_replace( '/[^a-z0-9_]/', '', strtolower( $base_name ) );
if ( empty( $base_name ) ) {
$base_name = 'field_' . time();
}
global $wpdb;
$fields_table = $wpdb->prefix . 'pcfb_fields';
$query = $wpdb->prepare(
"SELECT COUNT(*) FROM {$fields_table} WHERE form_id = %d AND field_name LIKE %s",
$form_id,
$base_name . '%'
);
$count = $wpdb->get_var( $query );
if ( $count > 0 ) {
return $base_name . '_' . ( $count + 1 );
}
return $base_name;
}
}

View File

@@ -0,0 +1,10 @@
{
"$schema": "https://opencode.ai/config.json",
"permission": {
"external_directory": {
"*": "deny",
"*/fir-qa0i/*": "allow",
"apps/c7f9e5c6-e7c2-4258-a583-ccffcf9791c8/fir-qa0i/*": "allow"
}
}
}

View File

@@ -0,0 +1,128 @@
<?php
/**
* Plugin Name: Plugin Compass Form Builder
* Plugin URI: https://plugincompass.com/plugins/pc-form-builder-xyz123
* Update URI: false
* Description: A complete form builder plugin with form builder, all forms management, and responses tracking.
* Version: 1.0.0
* Author: Plugin Compass
* Author URI: https://plugincompass.com
* Text Domain: pc-form-builder-xyz123
* Domain Path: /languages
* License: GPL v2 or later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
*
* @package PCFormBuilder
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
define( 'PCFB_VERSION', '1.0.0' );
define( 'PCFB_PLUGIN_FILE', __FILE__ );
define( 'PCFB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
define( 'PCFB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'PCFB_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
require_once PCFB_PLUGIN_DIR . 'includes/class-db-migration.php';
require_once PCFB_PLUGIN_DIR . 'includes/class-form-handler.php';
require_once PCFB_PLUGIN_DIR . 'admin/admin-menu.php';
class PC_Form_Builder {
private static $instance = null;
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
add_action( 'init', array( $this, 'init' ) );
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_public_assets' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) );
}
public function init() {
load_plugin_textdomain( 'pc-form-builder-xyz123', false, dirname( PCFB_PLUGIN_BASENAME ) . '/languages' );
PC_Form_Handler::get_instance();
PCFB_Admin_Menu::get_instance();
}
public function enqueue_public_assets() {
wp_enqueue_style(
'pcfb-public-style',
PCFB_PLUGIN_URL . 'public/css/public-style.css',
array(),
PCFB_VERSION
);
wp_enqueue_script(
'pcfb-public-script',
PCFB_PLUGIN_URL . 'public/js/public-script.js',
array( 'jquery' ),
PCFB_VERSION,
true
);
wp_localize_script( 'pcfb-public-script', 'pcfbVars', array(
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'pcfb_public_nonce' ),
) );
}
public function enqueue_admin_assets() {
$screen = get_current_screen();
if ( 'toplevel_page_pcfb-form-builder' === $screen->id ||
'form-builder_page_pcfb-all-forms' === $screen->id ||
'form-builder_page_pcfb-responses' === $screen->id ) {
wp_enqueue_style(
'pcfb-admin-style',
PCFB_PLUGIN_URL . 'admin/css/admin-style.css',
array(),
PCFB_VERSION
);
wp_enqueue_script(
'pcfb-admin-script',
PCFB_PLUGIN_URL . 'admin/js/admin-script.js',
array( 'jquery', 'jquery-ui-sortable', 'jquery-ui-draggable' ),
PCFB_VERSION,
true
);
wp_localize_script( 'pcfb-admin-script', 'pcfbAdminVars', array(
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'pcfb_admin_nonce' ),
'i18n' => array(
'confirmDelete' => __( 'Are you sure you want to delete this form?', 'pc-form-builder-xyz123' ),
'confirmDeleteField' => __( 'Are you sure you want to delete this field?', 'pc-form-builder-xyz123' ),
'fieldLabel' => __( 'Field Label', 'pc-form-builder-xyz123' ),
'fieldPlaceholder' => __( 'Placeholder text', 'pc-form-builder-xyz123' ),
'required' => __( 'Required', 'pc-form-builder-xyz123' ),
'delete' => __( 'Delete', 'pc-form-builder-xyz123' ),
),
) );
}
}
}
register_activation_hook( __FILE__, array( 'PC_DB_Migration', 'activate' ) );
register_deactivation_hook( __FILE__, array( 'PC_DB_Migration', 'deactivate' ) );
register_uninstall_hook( __FILE__, array( 'PC_Form_Builder', 'uninstall' ) );
add_action( 'plugins_loaded', array( 'PC_Form_Builder', 'get_instance' ) );
add_filter( 'site_transient_update_plugins', function( $value ) {
$plugin_file = plugin_basename( __FILE__ );
if ( isset( $value->response[ $plugin_file ] ) ) {
unset( $value->response[ $plugin_file ] );
}
return $value;
} );

View File

@@ -0,0 +1,528 @@
/**
* PC Form Builder Public Styles
*
* Frontend styling for the form builder plugin.
*
* @package PCFormBuilder
*/
/* CSS Variables */
:root {
--pcfb-primary: #2271b1;
--pcfb-primary-dark: #135e96;
--pcfb-primary-light: #72aee6;
--pcfb-success: #00a32a;
--pcfb-success-light: #dcfce1;
--pcfb-danger: #d63638;
--pcfb-danger-light: #fbe9e9;
--pcfb-warning: #dba617;
--pcfb-warning-light: #fff9c4;
--pcfb-text: #3c434a;
--pcfb-text-light: #6c7781;
--pcfb-bg: #ffffff;
--pcfb-bg-light: #f6f7f7;
--pcfb-border: #e1e2e3;
--pcfb-border-radius: 6px;
--pcfb-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
--pcfb-transition: all 0.2s ease;
--pcfb-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
}
/* Reset & Base */
.pcfb-form-wrapper {
font-family: var(--pcfb-font-family);
font-size: 16px;
line-height: 1.5;
color: var(--pcfb-text);
box-sizing: border-box;
}
.pcfb-form-wrapper *,
.pcfb-form-wrapper *::before,
.pcfb-form-wrapper *::after {
box-sizing: border-box;
}
/* Container */
.pcfb-form-container {
background: var(--pcfb-bg);
border: 1px solid var(--pcfb-border);
border-radius: var(--pcfb-border-radius);
box-shadow: var(--pcfb-box-shadow);
padding: 30px;
max-width: 100%;
}
/* Form Header */
.pcfb-form-header {
margin-bottom: 25px;
padding-bottom: 20px;
border-bottom: 1px solid var(--pcfb-border);
}
.pcfb-form-title {
margin: 0 0 10px;
font-size: 24px;
font-weight: 700;
color: var(--pcfb-text);
line-height: 1.3;
}
.pcfb-form-description {
margin: 0;
font-size: 15px;
color: var(--pcfb-text-light);
}
/* Form Fields */
.pcfb-form-fields {
display: flex;
flex-direction: column;
gap: 20px;
}
.pcfb-form-group {
display: flex;
flex-direction: column;
}
.pcfb-label {
font-weight: 600;
font-size: 14px;
color: var(--pcfb-text);
margin-bottom: 8px;
}
.pcfb-required {
color: var(--pcfb-danger);
margin-left: 3px;
}
/* Input Styles */
.pcfb-input,
.pcfb-textarea,
.pcfb-select {
width: 100%;
padding: 12px 15px;
font-family: inherit;
font-size: 15px;
line-height: 1.5;
color: var(--pcfb-text);
background-color: var(--pcfb-bg);
border: 1px solid var(--pcfb-border);
border-radius: var(--pcfb-border-radius);
transition: var(--pcfb-transition);
appearance: none;
}
.pcfb-input:focus,
.pcfb-textarea:focus,
.pcfb-select:focus {
outline: none;
border-color: var(--pcfb-primary);
box-shadow: 0 0 0 3px rgba(34, 113, 177, 0.15);
}
.pcfb-input::placeholder,
.pcfb-textarea::placeholder {
color: var(--pcfb-text-light);
opacity: 1;
}
.pcfb-textarea {
resize: vertical;
min-height: 100px;
max-height: 400px;
}
.pcfb-select {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236c7781' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 15px center;
padding-right: 40px;
cursor: pointer;
}
/* Checkbox Styles */
.pcfb-checkbox-group {
display: flex;
flex-direction: column;
gap: 10px;
}
.pcfb-checkbox-label {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
font-weight: 400;
font-size: 15px;
color: var(--pcfb-text);
padding: 5px 0;
}
.pcfb-checkbox-label:hover .pcfb-checkbox-text {
color: var(--pcfb-primary);
}
.pcfb-checkbox {
width: 18px;
height: 18px;
margin: 0;
cursor: pointer;
accent-color: var(--pcfb-primary);
flex-shrink: 0;
}
.pcfb-checkbox:checked + .pcfb-checkbox-text {
color: var(--pcfb-text);
}
.pcfb-checkbox-text {
transition: var(--pcfb-transition);
}
/* Form Footer */
.pcfb-form-footer {
margin-top: 30px;
padding-top: 25px;
border-top: 1px solid var(--pcfb-border);
}
/* Submit Button */
.pcfb-submit-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 14px 30px;
font-family: inherit;
font-size: 16px;
font-weight: 600;
line-height: 1;
color: #fff;
background: var(--pcfb-primary);
border: none;
border-radius: var(--pcfb-border-radius);
cursor: pointer;
transition: var(--pcfb-transition);
text-decoration: none;
min-width: 160px;
}
.pcfb-submit-button:hover {
background: var(--pcfb-primary-dark);
transform: translateY(-1px);
}
.pcfb-submit-button:active {
transform: translateY(0);
}
.pcfb-submit-button:disabled {
opacity: 0.7;
cursor: not-allowed;
transform: none;
}
/* Spinner */
.pcfb-spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top-color: #fff;
border-radius: 50%;
animation: pcfb-spin 0.8s linear infinite;
}
@keyframes pcfb-spin {
to {
transform: rotate(360deg);
}
}
/* Error Messages */
.pcfb-field-error {
display: none;
margin-top: 6px;
font-size: 13px;
color: var(--pcfb-danger);
}
.pcfb-form-group.error .pcfb-input,
.pcfb-form-group.error .pcfb-textarea,
.pcfb-form-group.error .pcfb-select {
border-color: var(--pcfb-danger);
background-color: var(--pcfb-danger-light);
}
.pcfb-form-group.error .pcfb-field-error {
display: block;
}
/* Success/Error Messages */
.pcfb-form-message {
margin-top: 20px;
padding: 15px 20px;
border-radius: var(--pcfb-border-radius);
animation: pcfb-fadeIn 0.3s ease;
}
@keyframes pcfb-fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.pcfb-success-message {
background-color: var(--pcfb-success-light);
border: 1px solid var(--pcfb-success);
}
.pcfb-error-message {
background-color: var(--pcfb-danger-light);
border: 1px solid var(--pcfb-danger);
}
.pcfb-message-content {
display: flex;
align-items: flex-start;
gap: 12px;
}
.pcfb-message-icon {
font-size: 24px;
width: 24px;
height: 24px;
flex-shrink: 0;
}
.pcfb-success-message .pcfb-message-icon {
color: var(--pcfb-success);
}
.pcfb-error-message .pcfb-message-icon {
color: var(--pcfb-danger);
}
.pcfb-message-text {
margin: 0;
font-size: 15px;
color: var(--pcfb-text);
}
/* No Fields Message */
.pcfb-no-fields {
text-align: center;
color: var(--pcfb-text-light);
padding: 40px 20px;
background: var(--pcfb-bg-light);
border-radius: var(--pcfb-border-radius);
margin: 0;
}
/* Responsive Design */
@media screen and (max-width: 600px) {
.pcfb-form-container {
padding: 20px;
}
.pcfb-form-title {
font-size: 20px;
}
.pcfb-form-fields {
gap: 15px;
}
.pcfb-input,
.pcfb-textarea,
.pcfb-select {
padding: 10px 12px;
font-size: 16px;
}
.pcfb-submit-button {
width: 100%;
padding: 14px 20px;
}
.pcfb-checkbox-label {
font-size: 14px;
}
}
/* Dark Mode Support */
@media (prefers-color-scheme: dark) {
.pcfb-form-wrapper {
--pcfb-text: #f0f0f1;
--pcfb-text-light: #a0a5aa;
--pcfb-bg: #1d2327;
--pcfb-bg-light: #2c3338;
--pcfb-border: #3c434a;
--pcfb-border-radius: 6px;
--pcfb-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
.pcfb-form-container {
background: var(--pcfb-bg);
border-color: var(--pcfb-border);
}
.pcfb-form-header {
border-bottom-color: var(--pcfb-border);
}
.pcfb-form-title {
color: var(--pcfb-text);
}
.pcfb-form-description {
color: var(--pcfb-text-light);
}
.pcfb-label {
color: var(--pcfb-text);
}
.pcfb-input,
.pcfb-textarea,
.pcfb-select {
background-color: var(--pcfb-bg-light);
border-color: var(--pcfb-border);
color: var(--pcfb-text);
}
.pcfb-input::placeholder,
.pcfb-textarea::placeholder {
color: var(--pcfb-text-light);
}
.pcfb-checkbox-label {
color: var(--pcfb-text);
}
.pcfb-form-footer {
border-top-color: var(--pcfb-border);
}
.pcfb-message-text {
color: var(--pcfb-text);
}
.pcfb-no-fields {
background: var(--pcfb-bg-light);
color: var(--pcfb-text-light);
}
}
/* High Contrast Mode */
@media (prefers-contrast: high) {
.pcfb-form-container {
border-width: 2px;
}
.pcfb-input,
.pcfb-textarea,
.pcfb-select {
border-width: 2px;
}
.pcfb-submit-button {
border: 2px solid #fff;
}
}
/* Reduced Motion */
@media (prefers-reduced-motion: reduce) {
.pcfb-form-wrapper *,
.pcfb-form-wrapper *::before,
.pcfb-form-wrapper *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* Print Styles */
@media print {
.pcfb-form-wrapper {
box-shadow: none;
border: 1px solid #000;
}
.pcfb-form-container {
box-shadow: none;
}
.pcfb-submit-button {
display: none;
}
}
/* Accessibility Focus Styles */
.pcfb-input:focus-visible,
.pcfb-textarea:focus-visible,
.pcfb-select:focus-visible,
.pcfb-checkbox:focus-visible {
outline: 2px solid var(--pcfb-primary);
outline-offset: 2px;
}
/* Validation Styles */
.pcfb-input:invalid:not(:placeholder-shown),
.pcfb-input.pattern-violation:not(:placeholder-shown) {
border-color: var(--pcfb-danger);
}
/* Loading State */
.pcfb-form.loading .pcfb-submit-text {
display: none;
}
.pcfb-form.loading .pcfb-submit-loader {
display: inline-flex !important;
align-items: center;
gap: 8px;
}
.pcfb-form.loading .pcfb-submit-button {
cursor: wait;
}
/* Form Layout Variations */
.pcfb-form-wrapper.pcfb-form-aligned .pcfb-form-group {
flex-direction: row;
align-items: flex-start;
gap: 20px;
}
.pcfb-form-wrapper.pcfb-form-aligned .pcfb-label {
flex: 0 0 150px;
padding-top: 12px;
margin-bottom: 0;
}
.pcfb-form-wrapper.pcfb-form-aligned .pcfb-input,
.pcfb-form-wrapper.pcfb-form-aligned .pcfb-textarea,
.pcfb-form-wrapper.pcfb-form-aligned .pcfb-select {
flex: 1;
}
@media screen and (max-width: 768px) {
.pcfb-form-wrapper.pcfb-form-aligned .pcfb-form-group {
flex-direction: column;
gap: 8px;
}
.pcfb-form-wrapper.pcfb-form-aligned .pcfb-label {
flex: none;
padding-top: 0;
}
}

View File

@@ -0,0 +1,136 @@
/**
* PC Form Builder Public Scripts
*
* Frontend JavaScript functionality for form handling.
*
* @package PCFormBuilder
*/
(function($) {
'use strict';
var PCFB_Public = {
init: function() {
this.initForms();
},
initForms: function() {
var self = this;
$(document).on('submit', '.pcfb-form', function(e) {
e.preventDefault();
self.handleFormSubmit($(this));
});
},
handleFormSubmit: function($form) {
var self = this;
var formId = $form.data('form-id');
var $submitButton = $form.find('.pcfb-submit-button');
var $submitText = $form.find('.pcfb-submit-text');
var $submitLoader = $form.find('.pcfb-submit-loader');
var $successMessage = $form.closest('.pcfb-form-container').find('.pcfb-success-message');
var $errorMessage = $form.closest('.pcfb-form-container').find('.pcfb-error-message');
$form.find('.pcfb-form-group').removeClass('error');
$form.find('.pcfb-field-error').text('');
$errorMessage.hide().find('.pcfb-message-text').text('');
$successMessage.hide();
var formData = $form.serialize();
var ajaxData = {
action: 'pcfb_submit_form',
form_id: formId,
form_data: {},
nonce: pcfbVars.nonce
};
$form.find('input, textarea, select').each(function() {
var $field = $(this);
var name = $field.attr('name');
if (name && name !== 'action' && name !== 'form_id' && name !== 'pcfb_nonce') {
if ($field.is(':checkbox')) {
if ($field.is(':checked')) {
if (!ajaxData.form_data[name]) {
ajaxData.form_data[name] = [];
}
ajaxData.form_data[name].push($field.val());
}
} else if ($field.is(':radio')) {
if ($field.is(':checked')) {
ajaxData.form_data[name] = $field.val();
}
} else {
ajaxData.form_data[name] = $field.val();
}
}
});
$form.addClass('loading');
$submitButton.prop('disabled', true);
$submitText.hide();
$submitLoader.show();
$.ajax({
url: pcfbVars.ajaxurl,
type: 'POST',
data: ajaxData,
dataType: 'json',
success: function(response) {
$form.removeClass('loading');
$submitButton.prop('disabled', false);
$submitText.show();
$submitLoader.hide();
if (response.success) {
$successMessage.find('.pcfb-message-text').text(response.data.message || 'Thank you!');
$successMessage.show();
$form.hide();
if (response.data.response_id) {
console.log('Response ID:', response.data.response_id);
}
} else {
if (response.data.errors) {
$.each(response.data.errors, function(fieldName, errorMessage) {
var $fieldGroup = $form.find('[name="' + fieldName + '"]').closest('.pcfb-form-group');
if ($fieldGroup.length === 0) {
$fieldGroup = $form.find('[name^="' + fieldName + '"]').closest('.pcfb-form-group');
}
if ($fieldGroup.length > 0) {
$fieldGroup.addClass('error');
$fieldGroup.find('.pcfb-field-error').text(errorMessage);
}
});
}
var errorText = response.data.message || 'An error occurred. Please try again.';
$errorMessage.find('.pcfb-message-text').text(errorText);
$errorMessage.show();
}
},
error: function(xhr, status, error) {
$form.removeClass('loading');
$submitButton.prop('disabled', false);
$submitText.show();
$submitLoader.hide();
var errorText = 'An error occurred. Please try again.';
if (xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) {
errorText = xhr.responseJSON.data.message;
} else if (xhr.statusText) {
errorText = xhr.statusText;
}
$errorMessage.find('.pcfb-message-text').text(errorText);
$errorMessage.show();
}
});
}
};
$(document).ready(function() {
PCFB_Public.init();
});
})(jQuery);

View File

@@ -0,0 +1,166 @@
<?php
/**
* Form Display Template
*
* Frontend template for displaying forms.
*
* @package PCFormBuilder
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
$form_id = ! empty( $form->id ) ? $form->id : 0;
$form_name = ! empty( $form->name ) ? $form->name : '';
$form_description = ! empty( $form->description ) ? $form->description : '';
$submit_text = ! empty( $settings['submit_text'] ) ? $settings['submit_text'] : __( 'Submit', 'pc-form-builder-xyz123' );
$success_message = ! empty( $settings['success_message'] ) ? $settings['success_message'] : __( 'Thank you! Your response has been submitted.', 'pc-form-builder-xyz123' );
?>
<div class="pcfb-form-wrapper" id="pcfb-form-wrapper-<?php echo esc_attr( $form_id ); ?>">
<div class="pcfb-form-container">
<?php if ( ! empty( $form_name ) ) : ?>
<div class="pcfb-form-header">
<h2 class="pcfb-form-title"><?php echo esc_html( $form_name ); ?></h2>
<?php if ( ! empty( $form_description ) ) : ?>
<p class="pcfb-form-description"><?php echo esc_html( $form_description ); ?></p>
<?php endif; ?>
</div>
<?php endif; ?>
<form class="pcfb-form" id="pcfb-form-<?php echo esc_attr( $form_id ); ?>" data-form-id="<?php echo esc_attr( $form_id ); ?>">
<?php wp_nonce_field( 'pcfb_public_nonce', 'pcfb_nonce' ); ?>
<input type="hidden" name="action" value="pcfb_submit_form">
<input type="hidden" name="form_id" value="<?php echo esc_attr( $form_id ); ?>">
<div class="pcfb-form-fields">
<?php if ( ! empty( $fields ) && is_array( $fields ) ) : ?>
<?php foreach ( $fields as $field ) : ?>
<?php
$field_id = ! empty( $field->id ) ? $field->id : 0;
$field_name = ! empty( $field->field_name ) ? $field->field_name : '';
$field_label = ! empty( $field->field_label ) ? $field->field_label : '';
$field_type = ! empty( $field->field_type ) ? $field->field_type : 'text';
$placeholder = ! empty( $field->placeholder ) ? $field->placeholder : '';
$options = ! empty( $field->options ) ? maybe_unserialize( $field->options ) : array();
$validation_rules = ! empty( $field->validation_rules ) ? maybe_unserialize( $field->validation_rules ) : array();
$is_required = ! empty( $validation_rules['required'] );
?>
<div class="pcfb-form-group pcfb-field-<?php echo esc_attr( $field_type ); ?>">
<label class="pcfb-label" for="<?php echo esc_attr( 'pcfb-' . $form_id . '-' . $field_name ); ?>">
<?php echo esc_html( $field_label ); ?>
<?php if ( $is_required ) : ?>
<span class="pcfb-required">*</span>
<?php endif; ?>
</label>
<?php switch ( $field_type ) :
case 'text':
case 'email':
case 'name':
?>
<input
type="<?php echo esc_attr( 'email' === $field_type ? 'email' : 'text' ); ?>"
id="<?php echo esc_attr( 'pcfb-' . $form_id . '-' . $field_name ); ?>"
name="<?php echo esc_attr( $field_name ); ?>"
class="pcfb-input"
placeholder="<?php echo esc_attr( $placeholder ); ?>"
<?php if ( $is_required ) : ?>required<?php endif; ?>
>
<?php
break;
case 'response':
?>
<textarea
id="<?php echo esc_attr( 'pcfb-' . $form_id . '-' . $field_name ); ?>"
name="<?php echo esc_attr( $field_name ); ?>"
class="pcfb-textarea"
rows="5"
placeholder="<?php echo esc_attr( $placeholder ); ?>"
<?php if ( $is_required ) : ?>required<?php endif; ?>
></textarea>
<?php
break;
case 'dropdown':
?>
<select
id="<?php echo esc_attr( 'pcfb-' . $form_id . '-' . $field_name ); ?>"
name="<?php echo esc_attr( $field_name ); ?>"
class="pcfb-select"
<?php if ( $is_required ) : ?>required<?php endif; ?>
>
<option value=""><?php echo esc_html( $placeholder ? $placeholder : __( '-- Select --', 'pc-form-builder-xyz123' ) ); ?></option>
<?php if ( ! empty( $options ) && is_array( $options ) ) : ?>
<?php foreach ( $options as $option ) : ?>
<?php if ( ! empty( trim( $option ) ) ) : ?>
<option value="<?php echo esc_attr( trim( $option ) ); ?>"><?php echo esc_html( trim( $option ) ); ?></option>
<?php endif; ?>
<?php endforeach; ?>
<?php endif; ?>
</select>
<?php
break;
case 'checkbox':
if ( ! empty( $options ) && is_array( $options ) ) :
?>
<div class="pcfb-checkbox-group">
<?php foreach ( $options as $index => $option ) : ?>
<?php if ( ! empty( trim( $option ) ) ) : ?>
<?php $checkbox_id = 'pcfb-' . $form_id . '-' . $field_name . '-' . $index; ?>
<label class="pcfb-checkbox-label" for="<?php echo esc_attr( $checkbox_id ); ?>">
<input
type="checkbox"
id="<?php echo esc_attr( $checkbox_id ); ?>"
name="<?php echo esc_attr( $field_name ); ?>[]"
value="<?php echo esc_attr( trim( $option ) ); ?>"
class="pcfb-checkbox"
>
<span class="pcfb-checkbox-text"><?php echo esc_html( trim( $option ) ); ?></span>
</label>
<?php endif; ?>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php
break;
endswitch; ?>
<span class="pcfb-field-error" id="<?php echo esc_attr( 'pcfb-error-' . $form_id . '-' . $field_name ); ?>"></span>
</div>
<?php endforeach; ?>
<?php else : ?>
<p class="pcfb-no-fields"><?php esc_html_e( 'This form has no fields yet.', 'pc-form-builder-xyz123' ); ?></p>
<?php endif; ?>
</div>
<div class="pcfb-form-footer">
<button type="submit" class="pcfb-submit-button">
<span class="pcfb-submit-text"><?php echo esc_html( $submit_text ); ?></span>
<span class="pcfb-submit-loader" style="display: none;">
<span class="pcfb-spinner"></span>
<?php esc_html_e( 'Submitting...', 'pc-form-builder-xyz123' ); ?>
</span>
</button>
</div>
</form>
<div class="pcfb-form-message pcfb-success-message" style="display: none;">
<div class="pcfb-message-content">
<span class="pcfb-message-icon dashicons dashicons-yes-alt"></span>
<p class="pcfb-message-text"><?php echo esc_html( $success_message ); ?></p>
</div>
</div>
<div class="pcfb-form-message pcfb-error-message" style="display: none;">
<div class="pcfb-message-content">
<span class="pcfb-message-icon dashicons dashicons-warning"></span>
<p class="pcfb-message-text"></p>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,50 @@
#!/bin/bash
#
# PC Form Builder Plugin Validation Script
#
# Lints all PHP files and verifies plugin header.
#
set -e
PLUGIN_DIR="$1"
if [ -z "$PLUGIN_DIR" ]; then
echo "Usage: ./scripts/validate-wordpress-plugin.sh <plugin-root-directory>"
exit 1
fi
if [ ! -d "$PLUGIN_DIR" ]; then
echo "Error: Directory $PLUGIN_DIR does not exist"
exit 1
fi
echo "Validating WordPress Plugin: $PLUGIN_DIR"
echo "=========================================="
ERRORS=0
for php_file in $(find "$PLUGIN_DIR" -name "*.php" -type f); do
echo "Checking: $php_file"
output=$(php -l "$php_file" 2>&1)
if echo "$output" | grep -q "No syntax errors detected"; then
echo " ✓ Syntax OK"
else
echo " ✗ Syntax Error:"
echo "$output"
ERRORS=$((ERRORS + 1))
fi
done
echo ""
echo "=========================================="
if [ $ERRORS -gt 0 ]; then
echo "Found $ERRORS error(s)"
exit 1
else
echo "All PHP files passed syntax validation!"
exit 0
fi

View File

@@ -0,0 +1,36 @@
<?php
/**
* PC Form Builder Uninstall
*
* Cleans up plugin data when uninstalled.
*
* @package PCFormBuilder
*/
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit;
}
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! current_user_can( 'delete_plugins' ) ) {
exit;
}
delete_option( 'pcfb_version' );
delete_option( 'pcfb_activated' );
global $wpdb;
$tables = array(
$wpdb->prefix . 'pcfb_forms',
$wpdb->prefix . 'pcfb_fields',
$wpdb->prefix . 'pcfb_responses',
$wpdb->prefix . 'pcfb_response_data',
);
foreach ( $tables as $table ) {
$wpdb->query( "DROP TABLE IF EXISTS {$table}" );
}

View File

@@ -0,0 +1,725 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class PC_Membership_Admin {
public static function init() {
add_action( 'admin_menu', array( __CLASS__, 'add_admin_menu' ) );
add_action( 'admin_init', array( __CLASS__, 'register_settings' ) );
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ) );
add_action( 'wp_ajax_pc_membership_save_plan', array( __CLASS__, 'ajax_save_plan' ) );
add_action( 'wp_ajax_pc_membership_delete_plan', array( __CLASS__, 'ajax_delete_plan' ) );
add_action( 'wp_ajax_pc_membership_get_plan', array( __CLASS__, 'ajax_get_plan' ) );
add_action( 'wp_ajax_pc_membership_get_stats', array( __CLASS__, 'ajax_get_stats' ) );
add_action( 'wp_ajax_pc_membership_save_pages', array( __CLASS__, 'ajax_save_pages' ) );
add_action( 'wp_ajax_pc_membership_save_access_rule', array( __CLASS__, 'ajax_save_access_rule' ) );
add_action( 'wp_ajax_pc_membership_delete_access_rule', array( __CLASS__, 'ajax_delete_access_rule' ) );
add_action( 'wp_ajax_pc_membership_get_content_for_rule', array( __CLASS__, 'ajax_get_content_for_rule' ) );
}
public static function add_admin_menu() {
add_menu_page(
__( 'Membership', 'pc-membership-abc123' ),
__( 'Membership', 'pc-membership-abc123' ),
'manage_options',
'pc-membership',
array( __CLASS__, 'dashboard_page' ),
'dashicons-id',
81
);
add_submenu_page( 'pc-membership', __( 'Dashboard', 'pc-membership-abc123' ), __( 'Dashboard', 'pc-membership-abc123' ), 'manage_options', 'pc-membership', array( __CLASS__, 'dashboard_page' ) );
add_submenu_page( 'pc-membership', __( 'Plans', 'pc-membership-abc123' ), __( 'Plans', 'pc-membership-abc123' ), 'manage_options', 'pc-membership-plans', array( __CLASS__, 'plans_page' ) );
add_submenu_page( 'pc-membership', __( 'Pages', 'pc-membership-abc123' ), __( 'Pages', 'pc-membership-abc123' ), 'manage_options', 'pc-membership-pages', array( __CLASS__, 'pages_page' ) );
add_submenu_page( 'pc-membership', __( 'Access Rules', 'pc-membership-abc123' ), __( 'Access Rules', 'pc-membership-abc123' ), 'manage_options', 'pc-membership-access', array( __CLASS__, 'access_page' ) );
add_submenu_page( 'pc-membership', __( 'Settings', 'pc-membership-abc123' ), __( 'Settings', 'pc-membership-abc123' ), 'manage_options', 'pc-membership-settings', array( __CLASS__, 'settings_page' ) );
}
public static function register_settings() {
register_setting( 'pc_membership_options', 'pc_membership_options', array( __CLASS__, 'sanitize_options' ) );
add_settings_section( 'pc_membership_stripe', __( 'Stripe Configuration', 'pc-membership-abc123' ), '__return_false', 'pc-membership-settings' );
add_settings_field( 'test_publishable_key', __( 'Test Publishable Key', 'pc-membership-abc123' ), array( __CLASS__, 'field_publishable_key' ), 'pc-membership-settings', 'pc_membership_stripe', array( 'key_type' => 'test' ) );
add_settings_field( 'test_secret_key', __( 'Test Secret Key', 'pc-membership-abc123' ), array( __CLASS__, 'field_secret_key' ), 'pc-membership-settings', 'pc_membership_stripe', array( 'key_type' => 'test' ) );
add_settings_field( 'live_publishable_key', __( 'Live Publishable Key', 'pc-membership-abc123' ), array( __CLASS__, 'field_publishable_key' ), 'pc-membership-settings', 'pc_membership_stripe', array( 'key_type' => 'live' ) );
add_settings_field( 'live_secret_key', __( 'Live Secret Key', 'pc-membership-abc123' ), array( __CLASS__, 'field_secret_key' ), 'pc-membership-settings', 'pc_membership_stripe', array( 'key_type' => 'live' ) );
add_settings_field( 'webhook_secret', __( 'Webhook Secret', 'pc-membership-abc123' ), array( __CLASS__, 'field_webhook_secret' ), 'pc-membership-settings', 'pc_membership_stripe' );
add_settings_field( 'mode', __( 'Mode', 'pc-membership-abc123' ), array( __CLASS__, 'field_mode' ), 'pc-membership-settings', 'pc_membership_stripe' );
add_settings_section( 'pc_membership_general', __( 'General Settings', 'pc-membership-abc123' ), '__return_false', 'pc-membership-settings' );
add_settings_field( 'currency', __( 'Currency', 'pc-membership-abc123' ), array( __CLASS__, 'field_currency' ), 'pc-membership-settings', 'pc_membership_general' );
}
public static function sanitize_options( $input ) {
$output = array();
foreach ( array( 'test_publishable_key', 'test_secret_key', 'live_publishable_key', 'live_secret_key', 'webhook_secret' ) as $key ) {
if ( isset( $input[ $key ] ) ) {
$output[ $key ] = sanitize_text_field( $input[ $key ] );
}
}
if ( isset( $input['mode'] ) && in_array( $input['mode'], array( 'test', 'live' ) ) ) {
$output['mode'] = $input['mode'];
}
if ( isset( $input['currency'] ) && in_array( $input['currency'], array( 'usd', 'eur', 'gbp' ) ) ) {
$output['currency'] = $input['currency'];
}
return $output;
}
public static function field_publishable_key( $args ) {
$options = get_option( 'pc_membership_options' );
$key = $args['key_type'] . '_publishable_key';
$value = isset( $options[ $key ] ) ? esc_attr( $options[ $key ] ) : '';
echo '<input type="text" name="pc_membership_options[' . esc_attr( $key ) . ']" value="' . $value . '" class="regular-text" placeholder="pk_test_..." />';
echo '<p class="description">' . esc_html__( 'Your Stripe publishable key from the Stripe dashboard.', 'pc-membership-abc123' ) . '</p>';
}
public static function field_secret_key( $args ) {
$options = get_option( 'pc_membership_options' );
$key = $args['key_type'] . '_secret_key';
$value = isset( $options[ $key ] ) ? esc_attr( $options[ $key ] ) : '';
echo '<input type="password" name="pc_membership_options[' . esc_attr( $key ) . ']" value="' . $value . '" class="regular-text" autocomplete="new-password" placeholder="sk_test_..." />';
echo '<p class="description">' . esc_html__( 'Your Stripe secret key from the Stripe dashboard.', 'pc-membership-abc123' ) . '</p>';
}
public static function field_webhook_secret() {
$options = get_option( 'pc_membership_options' );
$value = isset( $options['webhook_secret'] ) ? esc_attr( $options['webhook_secret'] ) : '';
echo '<input type="text" name="pc_membership_options[webhook_secret]" value="' . $value . '" class="regular-text" placeholder="whsec_..." />';
echo '<p class="description">' . esc_html__( 'Webhook secret for handling Stripe events.', 'pc-membership-abc123' ) . '</p>';
}
public static function field_mode() {
$options = get_option( 'pc_membership_options' );
$mode = isset( $options['mode'] ) ? $options['mode'] : 'test';
echo '<select name="pc_membership_options[mode]">';
echo '<option value="test"' . selected( $mode, 'test', false ) . '>' . esc_html__( 'Test Mode', 'pc-membership-abc123' ) . '</option>';
echo '<option value="live"' . selected( $mode, 'live', false ) . '>' . esc_html__( 'Live Mode', 'pc-membership-abc123' ) . '</option>';
echo '</select>';
echo '<p class="description">' . esc_html__( 'Use test mode for development. Switch to live mode when ready for production.', 'pc-membership-abc123' ) . '</p>';
}
public static function field_currency() {
$options = get_option( 'pc_membership_options' );
$currency = isset( $options['currency'] ) ? $options['currency'] : 'usd';
echo '<select name="pc_membership_options[currency]">';
echo '<option value="usd"' . selected( $currency, 'usd', false ) . '>USD ($)</option>';
echo '<option value="eur"' . selected( $currency, 'eur', false ) . '>EUR (€)</option>';
echo '<option value="gbp"' . selected( $currency, 'gbp', false ) . '>GBP (£)</option>';
echo '</select>';
}
public static function dashboard_page() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( __( 'Unauthorized', 'pc-membership-abc123' ) );
}
?>
<div class="wrap pc-membership-admin-wrap">
<h1><?php esc_html_e( 'Membership Dashboard', 'pc-membership-abc123' ); ?></h1>
<div class="pc-membership-dashboard-cards">
<div class="pc-membership-card">
<div class="pc-membership-card-header">
<span class="dashicons dashicons-groups"></span>
<h3><?php esc_html_e( 'Active Members', 'pc-membership-abc123' ); ?></h3>
</div>
<div class="pc-membership-card-body">
<div class="pc-membership-stat" id="pc-members-count">-</div>
</div>
</div>
<div class="pc-membership-card">
<div class="pc-membership-card-header">
<span class="dashicons dashicons-cart"></span>
<h3><?php esc_html_e( 'Active Subscriptions', 'pc-membership-abc123' ); ?></h3>
</div>
<div class="pc-membership-card-body">
<div class="pc-membership-stat" id="pc-subscriptions-count">-</div>
</div>
</div>
<div class="pc-membership-card">
<div class="pc-membership-card-header">
<span class="dashicons dashicons-chart-area"></span>
<h3><?php esc_html_e( 'Revenue This Month', 'pc-membership-abc123' ); ?></h3>
</div>
<div class="pc-membership-card-body">
<div class="pc-membership-stat" id="pc-revenue">-</div>
</div>
</div>
<div class="pc-membership-card">
<div class="pc-membership-card-header">
<span class="dashicons dashicons-pressthis"></span>
<h3><?php esc_html_e( 'Total Plans', 'pc-membership-abc123' ); ?></h3>
</div>
<div class="pc-membership-card-body">
<div class="pc-membership-stat" id="pc-plans-count">-</div>
</div>
</div>
</div>
<div class="pc-membership-dashboard-section">
<h2><?php esc_html_e( 'Recent Subscriptions', 'pc-membership-abc123' ); ?></h2>
<div class="pc-membership-table-wrapper">
<table class="widefat fixed striped pc-membership-table" id="pc-recent-subscriptions">
<thead>
<tr>
<th><?php esc_html_e( 'User', 'pc-membership-abc123' ); ?></th>
<th><?php esc_html_e( 'Plan', 'pc-membership-abc123' ); ?></th>
<th><?php esc_html_e( 'Status', 'pc-membership-abc123' ); ?></th>
<th><?php esc_html_e( 'Started', 'pc-membership-abc123' ); ?></th>
</tr>
</thead>
<tbody>
<tr><td colspan="4"><?php esc_html_e( 'Loading...', 'pc-membership-abc123' ); ?></td></tr>
</tbody>
</table>
</div>
</div>
</div>
<?php
}
public static function plans_page() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( __( 'Unauthorized', 'pc-membership-abc123' ) );
}
global $wpdb;
$plans = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}pc_membership_plans ORDER BY id DESC" );
?>
<div class="wrap pc-membership-admin-wrap">
<h1><?php esc_html_e( 'Membership Plans', 'pc-membership-abc123' ); ?>
<a href="#" class="page-title-action pc-membership-add-plan-btn"><?php esc_html_e( 'Add New Plan', 'pc-membership-abc123' ); ?></a>
</h1>
<?php if ( empty( $plans ) ) : ?>
<div class="pc-membership-empty-state">
<span class="dashicons dashicons-pressthis" style="font-size: 48px; height: 48px; width: 48px;"></span>
<h3><?php esc_html_e( 'No plans yet', 'pc-membership-abc123' ); ?></h3>
<p><?php esc_html_e( 'Create your first membership plan to get started.', 'pc-membership-abc123' ); ?></p>
<button class="button button-primary pc-membership-add-plan-btn"><?php esc_html_e( 'Add First Plan', 'pc-membership-abc123' ); ?></button>
</div>
<?php else : ?>
<table class="widefat fixed striped pc-membership-table">
<thead>
<tr>
<th><?php esc_html_e( 'ID', 'pc-membership-abc123' ); ?></th>
<th><?php esc_html_e( 'Name', 'pc-membership-abc123' ); ?></th>
<th><?php esc_html_e( 'Type', 'pc-membership-abc123' ); ?></th>
<th><?php esc_html_e( 'Price', 'pc-membership-abc123' ); ?></th>
<th><?php esc_html_e( 'Billing', 'pc-membership-abc123' ); ?></th>
<th><?php esc_html_e( 'Members', 'pc-membership-abc123' ); ?></th>
<th><?php esc_html_e( 'Actions', 'pc-membership-abc123' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $plans as $plan ) :
$type_label = $plan->is_subscription ? __( 'Subscription', 'pc-membership-abc123' ) : __( 'One-time', 'pc-membership-abc123' );
$billing_label = $plan->is_subscription ? sprintf( '%s / %s', pc_membership_format_price( $plan->price ), $plan->billing_interval ) : pc_membership_format_price( $plan->price );
$members_count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}pc_membership_subscriptions WHERE plan_id = %d AND status = 'active'", $plan->id ) );
?>
<tr>
<td><?php echo esc_html( $plan->id ); ?></td>
<td><strong><?php echo esc_html( $plan->name ); ?></strong></td>
<td><?php echo esc_html( $type_label ); ?></td>
<td><?php echo esc_html( pc_membership_format_price( $plan->price ) ); ?></td>
<td><?php echo esc_html( $billing_label ); ?></td>
<td><?php echo esc_html( (string) $members_count ); ?></td>
<td>
<button class="button pc-membership-edit-plan" data-plan-id="<?php echo esc_attr( $plan->id ); ?>"><?php esc_html_e( 'Edit', 'pc-membership-abc123' ); ?></button>
<button class="button pc-membership-delete-plan" data-plan-id="<?php echo esc_attr( $plan->id ); ?>"><?php esc_html_e( 'Delete', 'pc-membership-abc123' ); ?></button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<div id="pc-membership-plan-modal" class="pc-membership-modal" style="display: none;">
<div class="pc-membership-modal-content">
<div class="pc-membership-modal-header">
<h2><?php esc_html_e( 'Membership Plan', 'pc-membership-abc123' ); ?></h2>
<button class="pc-membership-modal-close">&times;</button>
</div>
<form id="pc-membership-plan-form" method="post">
<?php wp_nonce_field( 'pc_membership_save_plan', 'pc_membership_plan_nonce' ); ?>
<input type="hidden" name="plan_id" id="plan_id" value="">
<div class="pc-membership-form-row">
<label for="plan_name"><?php esc_html_e( 'Plan Name', 'pc-membership-abc123' ); ?> *</label>
<input type="text" name="name" id="plan_name" required class="regular-text">
</div>
<div class="pc-membership-form-row">
<label for="plan_description"><?php esc_html_e( 'Description', 'pc-membership-abc123' ); ?></label>
<textarea name="description" id="plan_description" rows="3" class="regular-text"></textarea>
</div>
<div class="pc-membership-form-row">
<label for="plan_type"><?php esc_html_e( 'Pricing Type', 'pc-membership-abc123' ); ?></label>
<select name="is_subscription" id="plan_type">
<option value="0"><?php esc_html_e( 'One-time Payment', 'pc-membership-abc123' ); ?></option>
<option value="1"><?php esc_html_e( 'Subscription', 'pc-membership-abc123' ); ?></option>
</select>
</div>
<div class="pc-membership-form-row">
<label for="plan_price"><?php esc_html_e( 'Price', 'pc-membership-abc123' ); ?> *</label>
<input type="number" name="price" id="plan_price" step="0.01" min="0" required class="regular-text">
</div>
<div class="pc-membership-form-row" id="billing_interval_row" style="display: none;">
<label for="plan_billing_interval"><?php esc_html_e( 'Billing Interval', 'pc-membership-abc123' ); ?></label>
<select name="billing_interval" id="plan_billing_interval">
<option value="day"><?php esc_html_e( 'Daily', 'pc-membership-abc123' ); ?></option>
<option value="week"><?php esc_html_e( 'Weekly', 'pc-membership-abc123' ); ?></option>
<option value="month" selected><?php esc_html_e( 'Monthly', 'pc-membership-abc123' ); ?></option>
<option value="year"><?php esc_html_e( 'Yearly', 'pc-membership-abc123' ); ?></option>
</select>
</div>
<div class="pc-membership-form-row">
<label for="plan_trial_days"><?php esc_html_e( 'Trial Period (Days)', 'pc-membership-abc123' ); ?></label>
<input type="number" name="trial_days" id="plan_trial_days" min="0" value="0" class="regular-text">
<p class="description"><?php esc_html_e( 'Number of free trial days before billing starts. Set to 0 for no trial.', 'pc-membership-abc123' ); ?></p>
</div>
<div class="pc-membership-form-row">
<label for="plan_benefits"><?php esc_html_e( 'Benefits', 'pc-membership-abc123' ); ?></label>
<textarea name="benefits" id="plan_benefits" rows="5" class="regular-text" placeholder="- Unlimited access&#10;- Priority support"></textarea>
<p class="description"><?php esc_html_e( 'Enter one benefit per line.', 'pc-membership-abc123' ); ?></p>
</div>
<div class="pc-membership-form-row">
<label for="plan_role"><?php esc_html_e( 'User Role', 'pc-membership-abc123' ); ?></label>
<select name="role" id="plan_role">
<?php foreach ( self::get_available_roles() as $role_key => $role_name ) : ?>
<option value="<?php echo esc_attr( $role_key ); ?>"><?php echo esc_html( $role_name ); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="pc-membership-form-actions">
<button type="submit" class="button button-primary"><?php esc_html_e( 'Save Plan', 'pc-membership-abc123' ); ?></button>
<button type="button" class="button pc-membership-modal-cancel"><?php esc_html_e( 'Cancel', 'pc-membership-abc123' ); ?></button>
</div>
</form>
</div>
</div>
</div>
<?php
}
private static function get_available_roles() {
$roles = get_editable_roles();
$options = array();
foreach ( $roles as $role_key => $role_data ) {
$options[ $role_key ] = translate_user_role( $role_data['name'] );
}
return $options;
}
public static function ajax_save_plan() {
check_ajax_referer( 'pc_membership_save_plan', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( __( 'Unauthorized', 'pc-membership-abc123' ) );
}
global $wpdb;
$plan_id = isset( $_POST['plan_id'] ) ? absint( $_POST['plan_id'] ) : 0;
$name = sanitize_text_field( wp_unslash( $_POST['name'] ) );
$description = isset( $_POST['description'] ) ? sanitize_textarea_field( wp_unslash( $_POST['description'] ) ) : '';
$price = isset( $_POST['price'] ) ? floatval( $_POST['price'] ) : 0;
$is_subscription = isset( $_POST['is_subscription'] ) ? absint( $_POST['is_subscription'] ) : 0;
$billing_interval = isset( $_POST['billing_interval'] ) ? sanitize_text_field( wp_unslash( $_POST['billing_interval'] ) ) : 'month';
$trial_days = isset( $_POST['trial_days'] ) ? absint( $_POST['trial_days'] ) : 0;
$benefits = isset( $_POST['benefits'] ) ? sanitize_textarea_field( wp_unslash( $_POST['benefits'] ) ) : '';
$role = isset( $_POST['role'] ) ? sanitize_text_field( wp_unslash( $_POST['role'] ) ) : 'subscriber';
if ( empty( $name ) || $price < 0 ) {
wp_send_json_error( __( 'Invalid input data', 'pc-membership-abc123' ) );
}
$data = array(
'name' => $name,
'description' => $description,
'price' => $price,
'is_subscription' => $is_subscription,
'billing_interval' => $is_subscription ? $billing_interval : '',
'trial_days' => $trial_days,
'benefits' => $benefits,
'role' => $role,
);
if ( $plan_id ) {
$result = $wpdb->update( $wpdb->prefix . 'pc_membership_plans', $data, array( 'id' => $plan_id ) );
} else {
$result = $wpdb->insert( $wpdb->prefix . 'pc_membership_plans', $data );
$plan_id = $wpdb->insert_id;
}
if ( $result === false ) {
wp_send_json_error( __( 'Failed to save plan', 'pc-membership-abc123' ) );
}
wp_send_json_success( array( 'plan_id' => $plan_id ) );
}
public static function ajax_get_plan() {
check_ajax_referer( 'pc_membership_save_plan', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( __( 'Unauthorized', 'pc-membership-abc123' ) );
}
$plan_id = isset( $_POST['plan_id'] ) ? absint( $_POST['plan_id'] ) : 0;
if ( ! $plan_id ) {
wp_send_json_error( __( 'Invalid plan ID', 'pc-membership-abc123' ) );
}
global $wpdb;
$plan = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}pc_membership_plans WHERE id = %d", $plan_id ) );
if ( ! $plan ) {
wp_send_json_error( __( 'Plan not found', 'pc-membership-abc123' ) );
}
wp_send_json_success( array( 'plan' => $plan ) );
}
public static function ajax_delete_plan() {
check_ajax_referer( 'pc_membership_save_plan', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( __( 'Unauthorized', 'pc-membership-abc123' ) );
}
$plan_id = isset( $_POST['plan_id'] ) ? absint( $_POST['plan_id'] ) : 0;
if ( ! $plan_id ) {
wp_send_json_error( __( 'Invalid plan ID', 'pc-membership-abc123' ) );
}
global $wpdb;
$wpdb->delete( $wpdb->prefix . 'pc_membership_subscriptions', array( 'plan_id' => $plan_id ) );
$result = $wpdb->delete( $wpdb->prefix . 'pc_membership_plans', array( 'id' => $plan_id ) );
if ( $result === false ) {
wp_send_json_error( __( 'Failed to delete plan', 'pc-membership-abc123' ) );
}
wp_send_json_success();
}
public static function ajax_get_stats() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( __( 'Unauthorized', 'pc-membership-abc123' ) );
}
global $wpdb;
$active_members = $wpdb->get_var( "SELECT COUNT(DISTINCT user_id) FROM {$wpdb->prefix}pc_membership_subscriptions WHERE status = 'active'" );
$active_subscriptions = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}pc_membership_subscriptions WHERE status = 'active'" );
$total_plans = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}pc_membership_plans" );
$current_month = date( 'Y-m-01' );
$revenue = $wpdb->get_var( $wpdb->prepare( "SELECT COALESCE(SUM(amount), 0) FROM {$wpdb->prefix}pc_membership_payments WHERE status = 'succeeded' AND created_at >= %s", $current_month ) );
$recent_subs = $wpdb->get_results( "SELECT s.*, u.display_name, u.user_login, p.name as plan_name FROM {$wpdb->prefix}pc_membership_subscriptions s LEFT JOIN {$wpdb->users} u ON s.user_id = u.ID LEFT JOIN {$wpdb->prefix}pc_membership_plans p ON s.plan_id = p.id ORDER BY s.created_at DESC LIMIT 10" );
wp_send_json_success( array(
'active_members' => $active_members,
'active_subscriptions' => $active_subscriptions,
'total_plans' => $total_plans,
'revenue' => pc_membership_format_price( $revenue ),
'recent_subscriptions' => $recent_subs,
) );
}
public static function ajax_save_pages() {
check_ajax_referer( 'pc_membership_save_pages', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( __( 'Unauthorized', 'pc-membership-abc123' ) );
}
if ( isset( $_POST['pc_membership_options'] ) ) {
$options = get_option( 'pc_membership_options', array() );
foreach ( $_POST['pc_membership_options'] as $key => $value ) {
$options[ $key ] = absint( $value );
}
update_option( 'pc_membership_options', $options );
}
wp_send_json_success();
}
public static function pages_page() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( __( 'Unauthorized', 'pc-membership-abc123' ) );
}
$options = get_option( 'pc_membership_options', array() );
$page_fields = array(
'checkout' => __( 'Checkout Page', 'pc-membership-abc123' ),
'login' => __( 'Login Page', 'pc-membership-abc123' ),
'register' => __( 'Registration Page', 'pc-membership-abc123' ),
'account' => __( 'Account Page', 'pc-membership-abc123' ),
'success' => __( 'Success Page', 'pc-membership-abc123' ),
'cancel' => __( 'Cancel Page', 'pc-membership-abc123' ),
);
$pages = get_pages( array( 'post_status' => 'publish' ) );
?>
<div class="wrap pc-membership-admin-wrap">
<h1><?php esc_html_e( 'Membership Pages', 'pc-membership-abc123' ); ?></h1>
<form method="post" action="<?php echo esc_url( admin_url( 'admin.php?page=pc-membership-pages' ) ); ?>">
<?php wp_nonce_field( 'pc_membership_save_pages', 'pc_membership_pages_nonce' ); ?>
<div class="pc-membership-page-settings">
<?php foreach ( $page_fields as $key => $label ) :
$selected = isset( $options[ $key . '_page_id' ] ) ? absint( $options[ $key . '_page_id' ] ) : 0;
?>
<div class="pc-membership-page-setting">
<label for="<?php echo esc_attr( 'page_' . $key ); ?>"><strong><?php echo esc_html( $label ); ?></strong></label>
<select name="pc_membership_options[<?php echo esc_attr( $key . '_page_id' ); ?>]" id="<?php echo esc_attr( 'page_' . $key ); ?>">
<option value="0"><?php esc_html_e( '-- Select --', 'pc-membership-abc123' ); ?></option>
<?php foreach ( $pages as $page ) : ?>
<option value="<?php echo esc_attr( $page->ID ); ?>" <?php selected( $selected, $page->ID ); ?>>
<?php echo esc_html( $page->post_title ); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<?php endforeach; ?>
</div>
<?php submit_button( __( 'Save Page Settings', 'pc-membership-abc123' ) ); ?>
</form>
<hr>
<h2><?php esc_html_e( 'Quick Create Pages', 'pc-membership-abc123' ); ?></h2>
<form method="post" action="<?php echo esc_url( admin_url( 'admin.php?page=pc-membership-pages' ) ); ?>">
<?php wp_nonce_field( 'pc_membership_create_pages', 'pc_membership_create_pages_nonce' ); ?>
<input type="hidden" name="pc_membership_create_pages" value="1">
<?php submit_button( __( 'Create All Pages', 'pc-membership-abc123' ), 'secondary' ); ?>
</form>
</div>
<?php
if ( isset( $_POST['pc_membership_create_pages'] ) && check_admin_referer( 'pc_membership_create_pages', 'pc_membership_create_pages_nonce' ) ) {
self::create_default_pages();
}
}
private static function create_default_pages() {
$pages = array(
'checkout' => array( 'title' => __( 'Membership Checkout', 'pc-membership-abc123' ), 'content' => '[pc_membership_checkout]' ),
'login' => array( 'title' => __( 'Member Login', 'pc-membership-abc123' ), 'content' => '[pc_membership_login]' ),
'register' => array( 'title' => __( 'Member Registration', 'pc-membership-abc123' ), 'content' => '[pc_membership_register]' ),
'account' => array( 'title' => __( 'My Account', 'pc-membership-abc123' ), 'content' => '[pc_membership_account]' ),
'success' => array( 'title' => __( 'Payment Successful', 'pc-membership-abc123' ), 'content' => '[pc_membership_success]' ),
'cancel' => array( 'title' => __( 'Payment Cancelled', 'pc-membership-abc123' ), 'content' => '[pc_membership_cancel]' ),
);
$options = get_option( 'pc_membership_options', array() );
foreach ( $pages as $key => $page_data ) {
$existing_id = isset( $options[ $key . '_page_id' ] ) ? absint( $options[ $key . '_page_id' ] ) : 0;
if ( ! $existing_id || ! get_post( $existing_id ) ) {
$page_id = wp_insert_post( array( 'post_title' => $page_data['title'], 'post_content' => $page_data['content'], 'post_status' => 'publish', 'post_type' => 'page' ) );
if ( $page_id && ! is_wp_error( $page_id ) ) {
$options[ $key . '_page_id' ] = $page_id;
}
}
}
update_option( 'pc_membership_options', $options );
echo '<div class="notice notice-success"><p>' . esc_html__( 'Membership pages created successfully!', 'pc-membership-abc123' ) . '</p></div>';
}
public static function access_page() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( __( 'Unauthorized', 'pc-membership-abc123' ) );
}
global $wpdb;
$plans = $wpdb->get_results( "SELECT id, name FROM {$wpdb->prefix}pc_membership_plans" );
$rules = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}pc_membership_access_rules ORDER BY id DESC" );
?>
<div class="wrap pc-membership-admin-wrap">
<h1><?php esc_html_e( 'Access Rules', 'pc-membership-abc123' ); ?></h1>
<div class="pc-membership-access-rules">
<h2><?php esc_html_e( 'Add New Rule', 'pc-membership-abc123' ); ?></h2>
<form id="pc-membership-access-form" class="pc-membership-access-form">
<?php wp_nonce_field( 'pc_membership_save_access_rule', 'pc_membership_access_nonce' ); ?>
<div class="pc-membership-form-row">
<label for="access_content_type"><?php esc_html_e( 'Content Type', 'pc-membership-abc123' ); ?></label>
<select name="content_type" id="access_content_type" required>
<option value="page"><?php esc_html_e( 'Page', 'pc-membership-abc123' ); ?></option>
<option value="post"><?php esc_html_e( 'Post', 'pc-membership-abc123' ); ?></option>
<option value="category"><?php esc_html_e( 'Category', 'pc-membership-abc123' ); ?></option>
</select>
</div>
<div class="pc-membership-form-row">
<label for="access_content_id"><?php esc_html_e( 'Select Content', 'pc-membership-abc123' ); ?></label>
<select name="content_id" id="access_content_id" required>
<option value=""><?php esc_html_e( '-- Select --', 'pc-membership-abc123' ); ?></option>
</select>
</div>
<div class="pc-membership-form-row">
<label for="access_plan_ids"><?php esc_html_e( 'Required Plans', 'pc-membership-abc123' ); ?></label>
<select name="plan_ids[]" id="access_plan_ids" multiple required>
<?php foreach ( $plans as $plan ) : ?>
<option value="<?php echo esc_attr( $plan->id ); ?>"><?php echo esc_html( $plan->name ); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="pc-membership-form-row">
<label for="access_redirect"><?php esc_html_e( 'Redirect Non-Members To', 'pc-membership-abc123' ); ?></label>
<select name="redirect" id="access_redirect">
<option value="checkout"><?php esc_html_e( 'Checkout Page', 'pc-membership-abc123' ); ?></option>
<option value="login"><?php esc_html_e( 'Login Page', 'pc-membership-abc123' ); ?></option>
<option value="custom"><?php esc_html_e( 'Custom URL', 'pc-membership-abc123' ); ?></option>
</select>
</div>
<div class="pc-membership-form-row" id="custom_redirect_row" style="display: none;">
<label for="access_custom_url"><?php esc_html_e( 'Custom URL', 'pc-membership-abc123' ); ?></label>
<input type="url" name="custom_url" id="access_custom_url" class="regular-text" placeholder="https://...">
</div>
<div class="pc-membership-form-actions">
<button type="submit" class="button button-primary"><?php esc_html_e( 'Add Rule', 'pc-membership-abc123' ); ?></button>
</div>
</form>
<h2><?php esc_html_e( 'Existing Rules', 'pc-membership-abc123' ); ?></h2>
<table class="widefat fixed striped pc-membership-table" id="pc-access-rules-table">
<thead>
<tr>
<th><?php esc_html_e( 'Content', 'pc-membership-abc123' ); ?></th>
<th><?php esc_html_e( 'Required Plans', 'pc-membership-abc123' ); ?></th>
<th><?php esc_html_e( 'Redirect', 'pc-membership-abc123' ); ?></th>
<th><?php esc_html_e( 'Actions', 'pc-membership-abc123' ); ?></th>
</tr>
</thead>
<tbody>
<?php if ( empty( $rules ) ) : ?>
<tr><td colspan="4"><?php esc_html_e( 'No access rules defined yet.', 'pc-membership-abc123' ); ?></td></tr>
<?php else : ?>
<?php foreach ( $rules as $rule ) :
$content_title = '';
if ( $rule->content_type === 'category' ) {
$cat = get_term( $rule->content_id );
$content_title = $cat ? $cat->name : sprintf( __( 'Category #%d', 'pc-membership-abc123' ), $rule->content_id );
} else {
$post = get_post( $rule->content_id );
$content_title = $post ? $post->post_title : sprintf( __( '%s #%d', 'pc-membership-abc123' ), $rule->content_type, $rule->content_id );
}
$plan_ids = maybe_unserialize( $rule->plan_ids );
if ( ! is_array( $plan_ids ) ) { $plan_ids = array( $plan_ids ); }
$plan_names = array();
foreach ( $plan_ids as $plan_id ) {
$plan = $wpdb->get_var( $wpdb->prepare( "SELECT name FROM {$wpdb->prefix}pc_membership_plans WHERE id = %d", $plan_id ) );
if ( $plan ) { $plan_names[] = $plan; }
}
$redirect_text = $rule->redirect_type === 'custom' ? esc_html__( 'Custom URL', 'pc-membership-abc123' ) : ( $rule->redirect_type === 'checkout' ? esc_html__( 'Checkout', 'pc-membership-abc123' ) : esc_html__( 'Login', 'pc-membership-abc123' ) );
?>
<tr>
<td><?php echo esc_html( $content_title ) . ' <span class="description">(' . esc_html( $rule->content_type ) . ')</span>'; ?></td>
<td><?php echo esc_html( implode( ', ', $plan_names ) ); ?></td>
<td><?php echo $redirect_text; ?></td>
<td><button class="button pc-membership-delete-rule" data-rule-id="<?php echo esc_attr( $rule->id ); ?>"><?php esc_html_e( 'Delete', 'pc-membership-abc123' ); ?></button></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php
}
public static function ajax_save_access_rule() {
check_ajax_referer( 'pc_membership_save_access_rule', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( __( 'Unauthorized', 'pc-membership-abc123' ) );
}
$content_type = sanitize_text_field( wp_unslash( $_POST['content_type'] ) );
$content_id = absint( $_POST['content_id'] );
$plan_ids = array_map( 'absint', (array) $_POST['plan_ids'] );
$redirect = sanitize_text_field( wp_unslash( $_POST['redirect'] ) );
$custom_url = isset( $_POST['custom_url'] ) ? esc_url_raw( wp_unslash( $_POST['custom_url'] ) ) : '';
if ( ! in_array( $content_type, array( 'page', 'post', 'category' ) ) || empty( $plan_ids ) ) {
wp_send_json_error( __( 'Invalid input', 'pc-membership-abc123' ) );
}
global $wpdb;
$existing = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}pc_membership_access_rules WHERE content_type = %s AND content_id = %d", $content_type, $content_id ) );
if ( $existing ) {
wp_send_json_error( __( 'A rule for this content already exists.', 'pc-membership-abc123' ) );
}
$result = $wpdb->insert( $wpdb->prefix . 'pc_membership_access_rules', array(
'content_type' => $content_type,
'content_id' => $content_id,
'plan_ids' => maybe_serialize( $plan_ids ),
'redirect_type' => $redirect,
'custom_url' => $redirect === 'custom' ? $custom_url : '',
) );
if ( ! $result ) {
wp_send_json_error( __( 'Failed to save rule', 'pc-membership-abc123' ) );
}
wp_send_json_success();
}
public static function ajax_delete_access_rule() {
check_ajax_referer( 'pc_membership_save_access_rule', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( __( 'Unauthorized', 'pc-membership-abc123' ) );
}
$rule_id = absint( $_POST['rule_id'] );
global $wpdb;
$result = $wpdb->delete( $wpdb->prefix . 'pc_membership_access_rules', array( 'id' => $rule_id ) );
if ( ! $result ) {
wp_send_json_error( __( 'Failed to delete rule', 'pc-membership-abc123' ) );
}
wp_send_json_success();
}
public static function ajax_get_content_for_rule() {
check_ajax_referer( 'pc_membership_admin_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( __( 'Unauthorized', 'pc-membership-abc123' ) );
}
$content_type = isset( $_POST['content_type'] ) ? sanitize_text_field( wp_unslash( $_POST['content_type'] ) ) : 'page';
$items = array();
if ( $content_type === 'page' ) {
$pages = get_pages( array( 'post_status' => 'publish' ) );
foreach ( $pages as $page ) { $items[] = array( 'id' => $page->ID, 'title' => $page->post_title ); }
} elseif ( $content_type === 'post' ) {
$posts = get_posts( array( 'post_status' => 'publish', 'posts_per_page' => -1 ) );
foreach ( $posts as $post ) { $items[] = array( 'id' => $post->ID, 'title' => $post->post_title ); }
} elseif ( $content_type === 'category' ) {
$categories = get_categories( array( 'hide_empty' => false ) );
foreach ( $categories as $cat ) { $items[] = array( 'id' => $cat->term_id, 'title' => $cat->name ); }
}
wp_send_json_success( array( 'items' => $items ) );
}
public static function settings_page() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( __( 'Unauthorized', 'pc-membership-abc123' ) );
}
?>
<div class="wrap pc-membership-admin-wrap">
<h1><?php esc_html_e( 'Membership Settings', 'pc-membership-abc123' ); ?></h1>
<form method="post" action="options.php">
<?php settings_fields( 'pc_membership_options' ); do_settings_sections( 'pc-membership-settings' ); submit_button(); ?>
</form>
</div>
<?php
}
public static function enqueue_assets( $hook ) {
if ( strpos( $hook, 'pc-membership' ) === false ) {
return;
}
wp_enqueue_style( 'pc-membership-admin-style', PC_MEMBERSHIP_PLUGIN_URL . 'admin/css/admin-style.css', array(), PC_MEMBERSHIP_VERSION );
wp_enqueue_script( 'pc-membership-admin-script', PC_MEMBERSHIP_PLUGIN_URL . 'admin/js/admin-script.js', array( 'jquery', 'jquery-ui-dialog' ), PC_MEMBERSHIP_VERSION, true );
wp_localize_script( 'pc-membership-admin-script', 'pcMembershipAdmin', array(
'ajax_url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'pc_membership_admin_nonce' ),
'i18n' => array(
'save' => __( 'Save', 'pc-membership-abc123' ),
'cancel' => __( 'Cancel', 'pc-membership-abc123' ),
'delete' => __( 'Delete', 'pc-membership-abc123' ),
'confirmDelete' => __( 'Are you sure you want to delete this plan?', 'pc-membership-abc123' ),
'loading' => __( 'Loading...', 'pc-membership-abc123' ),
'success' => __( 'Saved successfully', 'pc-membership-abc123' ),
'error' => __( 'An error occurred', 'pc-membership-abc123' ),
),
) );
}
}

View File

@@ -0,0 +1,222 @@
.pc-membership-admin-wrap {
padding: 20px 0;
}
.pc-membership-admin-wrap h1 {
margin-bottom: 20px;
}
.pc-membership-dashboard-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.pc-membership-card {
background: #fff;
border: 1px solid #c3c4c7;
border-radius: 4px;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
overflow: hidden;
}
.pc-membership-card-header {
background: #f0f0f1;
padding: 12px 15px;
border-bottom: 1px solid #c3c4c7;
display: flex;
align-items: center;
gap: 10px;
}
.pc-membership-card-header .dashicons {
color: #2271b1;
}
.pc-membership-card-header h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
}
.pc-membership-card-body {
padding: 20px;
text-align: center;
}
.pc-membership-stat {
font-size: 32px;
font-weight: 700;
color: #2271b1;
}
.pc-membership-dashboard-section {
margin-top: 30px;
background: #fff;
border: 1px solid #c3c4c7;
border-radius: 4px;
padding: 20px;
}
.pc-membership-table-wrapper {
margin-top: 15px;
}
.pc-membership-empty-state {
text-align: center;
padding: 60px 20px;
background: #fff;
border: 1px solid #c3c4c7;
border-radius: 4px;
}
.pc-membership-empty-state h3 {
margin: 15px 0 10px;
}
.pc-membership-empty-state p {
color: #646970;
margin-bottom: 20px;
}
.pc-membership-page-settings {
background: #fff;
border: 1px solid #c3c4c7;
border-radius: 4px;
padding: 20px;
max-width: 600px;
}
.pc-membership-page-setting {
margin-bottom: 20px;
}
.pc-membership-page-setting label {
display: block;
margin-bottom: 5px;
}
.pc-membership-page-setting select {
width: 100%;
max-width: 400px;
}
.pc-membership-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999999;
display: flex;
align-items: center;
justify-content: center;
}
.pc-membership-modal-content {
background: #fff;
border-radius: 4px;
width: 90%;
max-width: 600px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
}
.pc-membership-modal-header {
padding: 15px 20px;
border-bottom: 1px solid #c3c4c7;
display: flex;
justify-content: space-between;
align-items: center;
background: #f0f0f1;
}
.pc-membership-modal-header h2 {
margin: 0;
font-size: 18px;
}
.pc-membership-modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #646970;
padding: 0;
line-height: 1;
}
.pc-membership-modal-close:hover {
color: #23282d;
}
#pc-membership-plan-form,
.pc-membership-access-form {
padding: 20px;
}
.pc-membership-form-row {
margin-bottom: 20px;
}
.pc-membership-form-row label {
display: block;
font-weight: 600;
margin-bottom: 5px;
}
.pc-membership-form-row input[type="text"],
.pc-membership-form-row input[type="number"],
.pc-membership-form-row input[type="url"],
.pc-membership-form-row textarea,
.pc-membership-form-row select {
width: 100%;
max-width: 400px;
}
.pc-membership-form-actions {
padding-top: 15px;
border-top: 1px solid #e5e5e5;
margin-top: 20px;
}
.pc-membership-form-actions .button {
margin-right: 10px;
}
.pc-membership-access-rules {
background: #fff;
border: 1px solid #c3c4c7;
border-radius: 4px;
padding: 20px;
}
.pc-membership-access-rules h2 {
margin-top: 0;
margin-bottom: 20px;
font-size: 18px;
}
.pc-membership-access-form {
background: #f0f0f1;
padding: 20px;
border-radius: 4px;
margin-bottom: 30px;
}
.pc-membership-access-form .pc-membership-form-row {
margin-bottom: 15px;
}
.pc-membership-access-form select[multiple] {
height: 120px;
}
@media screen and (max-width: 782px) {
.pc-membership-dashboard-cards {
grid-template-columns: 1fr;
}
}

View File

@@ -0,0 +1,251 @@
(function($) {
'use strict';
var PCMembershipAdmin = {
init: function() {
this.bindEvents();
this.loadStats();
this.initPlanModal();
this.initAccessForm();
},
bindEvents: function() {
$(document).on('click', '.pc-membership-add-plan-btn', $.proxy(this.openPlanModal, this));
$(document).on('click', '.pc-membership-edit-plan', $.proxy(this.editPlan, this));
$(document).on('click', '.pc-membership-delete-plan', $.proxy(this.deletePlan, this));
$(document).on('click', '.pc-membership-modal-close, .pc-membership-modal-cancel', $.proxy(this.closePlanModal, this));
$(document).on('submit', '#pc-membership-plan-form', $.proxy(this.savePlan, this));
$(document).on('change', '#plan_type', $.proxy(this.toggleBillingInterval, this));
$(document).on('click', '.pc-membership-delete-rule', $.proxy(this.deleteAccessRule, this));
$(document).on('submit', '#pc-membership-access-form', $.proxy(this.saveAccessRule, this));
$(document).on('change', '#access_redirect', $.proxy(this.toggleCustomRedirect, this));
$(document).on('change', '#access_content_type', $.proxy(this.loadContentOptions, this));
},
loadStats: function() {
$.ajax({
url: pcMembershipAdmin.ajax_url,
type: 'POST',
data: {
action: 'pc_membership_get_stats',
nonce: pcMembershipAdmin.nonce
},
success: function(response) {
if (response.success) {
var data = response.data;
$('#pc-members-count').text(data.active_members);
$('#pc-subscriptions-count').text(data.active_subscriptions);
$('#pc-revenue').text(data.revenue);
$('#pc-plans-count').text(data.total_plans);
if (data.recent_subscriptions && data.recent_subscriptions.length > 0) {
var tbody = $('#pc-recent-subscriptions tbody');
tbody.empty();
$.each(data.recent_subscriptions, function(i, sub) {
var display_name = sub.display_name || sub.user_login || 'User #' + sub.user_id;
tbody.append(
'<tr>' +
'<td>' + display_name + '</td>' +
'<td>' + sub.plan_name + '</td>' +
'<td>' + sub.status + '</td>' +
'<td>' + sub.started_at.substring(0, 10) + '</td>' +
'</tr>'
);
});
}
}
}
});
},
initPlanModal: function() {
$('#pc-membership-plan-modal').dialog({
autoOpen: false,
modal: true,
width: '90%',
maxWidth: 600,
closeText: ''
});
},
openPlanModal: function(e) {
if (e) e.preventDefault();
$('#pc-membership-plan-form')[0].reset();
$('#plan_id').val('');
$('#pc-membership-plan-modal').dialog('open');
this.toggleBillingInterval();
},
editPlan: function(e) {
e.preventDefault();
var planId = $(e.currentTarget).data('plan-id');
$.ajax({
url: pcMembershipAdmin.ajax_url,
type: 'POST',
data: {
action: 'pc_membership_get_plan',
plan_id: planId,
nonce: pcMembershipAdmin.nonce
},
success: $.proxy(function(response) {
if (response.success && response.data.plan) {
var plan = response.data.plan;
$('#plan_id').val(plan.id);
$('#plan_name').val(plan.name);
$('#plan_description').val(plan.description || '');
$('#plan_price').val(plan.price);
$('#plan_type').val(plan.is_subscription);
$('#plan_billing_interval').val(plan.billing_interval || 'month');
$('#plan_trial_days').val(plan.trial_days || 0);
$('#plan_benefits').val(plan.benefits || '');
$('#plan_role').val(plan.role || 'subscriber');
$('#pc-membership-plan-modal').dialog('open');
this.toggleBillingInterval();
}
}, this)
});
},
savePlan: function(e) {
e.preventDefault();
var $form = $('#pc-membership-plan-form');
var formData = $form.serialize();
$.ajax({
url: pcMembershipAdmin.ajax_url,
type: 'POST',
data: formData + '&action=pc_membership_save_plan&nonce=' + pcMembershipAdmin.nonce,
success: function(response) {
if (response.success) {
$('#pc-membership-plan-modal').dialog('close');
window.location.reload();
} else {
alert(pcMembershipAdmin.i18n.error + ': ' + (response.data || 'Unknown error'));
}
},
error: function() {
alert(pcMembershipAdmin.i18n.error);
}
});
},
deletePlan: function(e) {
e.preventDefault();
if (!confirm(pcMembershipAdmin.i18n.confirmDelete)) return;
var planId = $(e.currentTarget).data('plan-id');
$.ajax({
url: pcMembershipAdmin.ajax_url,
type: 'POST',
data: {
action: 'pc_membership_delete_plan',
plan_id: planId,
nonce: pcMembershipAdmin.nonce
},
success: function(response) {
if (response.success) {
window.location.reload();
} else {
alert(pcMembershipAdmin.i18n.error);
}
}
});
},
closePlanModal: function(e) {
if (e) e.preventDefault();
$('#pc-membership-plan-modal').dialog('close');
},
toggleBillingInterval: function() {
var isSubscription = $('#plan_type').val() === '1';
$('#billing_interval_row').toggle(isSubscription);
},
initAccessForm: function() {
this.loadContentOptions();
},
loadContentOptions: function() {
var contentType = $('#access_content_type').val();
var $select = $('#access_content_id');
$select.html('<option value="">Loading...</option>');
var data = {
action: 'pc_membership_get_content_for_rule',
content_type: contentType,
nonce: pcMembershipAdmin.nonce
};
$.ajax({
url: pcMembershipAdmin.ajax_url,
type: 'POST',
data: data,
success: function(response) {
$select.empty();
$select.append('<option value="">-- Select --</option>');
if (response.success && response.data.items) {
$.each(response.data.items, function(i, item) {
$select.append('<option value="' + item.id + '">' + item.title + '</option>');
});
}
}
});
},
saveAccessRule: function(e) {
e.preventDefault();
var $form = $('#pc-membership-access-form');
$.ajax({
url: pcMembershipAdmin.ajax_url,
type: 'POST',
data: $form.serialize() + '&action=pc_membership_save_access_rule&nonce=' + pcMembershipAdmin.nonce,
success: function(response) {
if (response.success) {
window.location.reload();
} else {
alert(pcMembershipAdmin.i18n.error + ': ' + (response.data || 'Unknown error'));
}
}
});
},
deleteAccessRule: function(e) {
e.preventDefault();
if (!confirm('Are you sure you want to delete this rule?')) return;
var ruleId = $(e.currentTarget).data('rule-id');
$.ajax({
url: pcMembershipAdmin.ajax_url,
type: 'POST',
data: {
action: 'pc_membership_delete_access_rule',
rule_id: ruleId,
nonce: pcMembershipAdmin.nonce
},
success: function(response) {
if (response.success) {
window.location.reload();
} else {
alert(pcMembershipAdmin.i18n.error);
}
}
});
},
toggleCustomRedirect: function() {
var redirect = $('#access_redirect').val();
$('#custom_redirect_row').toggle(redirect === 'custom');
}
};
$(document).ready(function() {
PCMembershipAdmin.init();
});
})(jQuery);

View File

@@ -0,0 +1,225 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class PC_Membership_Access_Control {
public static function init() {
add_action( 'wp', array( __CLASS__, 'check_access' ) );
add_action( 'template_redirect', array( __CLASS__, 'handle_restricted_access' ) );
}
public static function check_access() {
if ( is_admin() ) {
return;
}
if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
return;
}
$content_id = 0;
$content_type = '';
if ( is_singular( 'post' ) ) {
$content_id = get_the_ID();
$content_type = 'post';
} elseif ( is_singular( 'page' ) ) {
$content_id = get_the_ID();
$content_type = 'page';
} elseif ( is_category() || is_archive() ) {
$cat = get_queried_object();
if ( $cat && isset( $cat->term_id ) ) {
$content_id = $cat->term_id;
$content_type = 'category';
}
}
if ( empty( $content_id ) || empty( $content_type ) ) {
return;
}
$rule = self::get_access_rule( $content_type, $content_id );
if ( ! $rule ) {
return;
}
if ( self::user_has_access( $rule ) ) {
return;
}
self::redirect_user( $rule );
}
private static function get_access_rule( $content_type, $content_id ) {
global $wpdb;
return $wpdb->get_row( $wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}pc_membership_access_rules WHERE content_type = %s AND content_id = %d",
$content_type,
$content_id
) );
}
private static function user_has_access( $rule ) {
if ( ! is_user_logged_in() ) {
return false;
}
$plan_ids = maybe_unserialize( $rule->plan_ids );
if ( ! is_array( $plan_ids ) ) {
$plan_ids = array( $plan_ids );
}
if ( empty( $plan_ids ) ) {
return true;
}
$user_id = get_current_user_id();
global $wpdb;
$subscription = $wpdb->get_row( $wpdb->prepare(
"SELECT plan_id FROM {$wpdb->prefix}pc_membership_subscriptions WHERE user_id = %d AND status = 'active'",
$user_id
) );
if ( ! $subscription ) {
return false;
}
return in_array( $subscription->plan_id, $plan_ids );
}
private static function redirect_user( $rule ) {
$redirect_url = '';
switch ( $rule->redirect_type ) {
case 'custom':
$redirect_url = ! empty( $rule->custom_url ) ? $rule->custom_url : self::get_default_redirect();
break;
case 'login':
$redirect_url = self::get_page_url( 'login' );
break;
default:
$redirect_url = self::get_page_url( 'checkout' );
}
if ( ! $redirect_url ) {
$redirect_url = home_url();
}
wp_redirect( $redirect_url );
exit;
}
private static function get_default_redirect() {
return self::get_page_url( 'checkout' );
}
private static function get_page_url( $page_type ) {
$options = get_option( 'pc_membership_options' );
$page_id = isset( $options[ $page_type . '_page_id' ] ) ? absint( $options[ $page_type . '_page_id' ] ) : 0;
return $page_id ? get_permalink( $page_id ) : '';
}
public static function handle_restricted_access() {
if ( ! is_user_logged_in() ) {
return;
}
global $post;
if ( ! $post ) {
return;
}
$content_type = $post->post_type === 'page' ? 'page' : 'post';
$rule = self::get_access_rule( $content_type, $post->ID );
if ( ! $rule ) {
return;
}
if ( self::user_has_access( $rule ) ) {
return;
}
self::redirect_user( $rule );
}
public static function is_content_restricted( $content_id, $content_type = 'post' ) {
$rule = self::get_access_rule( $content_type, $content_id );
return ! empty( $rule );
}
public static function can_access_content( $user_id, $content_id, $content_type = 'post' ) {
$rule = self::get_access_rule( $content_type, $content_id );
if ( ! $rule ) {
return true;
}
if ( ! $user_id ) {
return false;
}
$plan_ids = maybe_unserialize( $rule->plan_ids );
if ( ! is_array( $plan_ids ) ) {
$plan_ids = array( $plan_ids );
}
global $wpdb;
$subscription = $wpdb->get_row( $wpdb->prepare(
"SELECT plan_id FROM {$wpdb->prefix}pc_membership_subscriptions WHERE user_id = %d AND status = 'active'",
$user_id
) );
if ( ! $subscription ) {
return false;
}
return in_array( $subscription->plan_id, $plan_ids );
}
public static function restrict_post( $post_id, $plan_ids, $redirect_type = 'checkout', $custom_url = '' ) {
global $wpdb;
$content_type = get_post_type( $post_id ) === 'page' ? 'page' : 'post';
$existing = $wpdb->get_var( $wpdb->prepare(
"SELECT id FROM {$wpdb->prefix}pc_membership_access_rules WHERE content_type = %s AND content_id = %d",
$content_type,
$post_id
) );
if ( $existing ) {
return $wpdb->update( $wpdb->prefix . 'pc_membership_access_rules', array(
'plan_ids' => maybe_serialize( $plan_ids ),
'redirect_type' => $redirect_type,
'custom_url' => $redirect_type === 'custom' ? $custom_url : '',
), array( 'id' => $existing ) );
}
return $wpdb->insert( $wpdb->prefix . 'pc_membership_access_rules', array(
'content_type' => $content_type,
'content_id' => $post_id,
'plan_ids' => maybe_serialize( $plan_ids ),
'redirect_type' => $redirect_type,
'custom_url' => $redirect_type === 'custom' ? $custom_url : '',
) );
}
public static function unrestrict_post( $post_id ) {
global $wpdb;
$content_type = get_post_type( $post_id ) === 'page' ? 'page' : 'post';
return $wpdb->delete( $wpdb->prefix . 'pc_membership_access_rules', array(
'content_type' => $content_type,
'content_id' => $post_id,
) );
}
}

View File

@@ -0,0 +1,128 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class PC_Membership_Activator {
public static function activate() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$table_plans = $wpdb->prefix . 'pc_membership_plans';
$table_subscriptions = $wpdb->prefix . 'pc_membership_subscriptions';
$table_payments = $wpdb->prefix . 'pc_membership_payments';
$table_access_rules = $wpdb->prefix . 'pc_membership_access_rules';
$sql = "CREATE TABLE $table_plans (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
description TEXT NULL,
price DECIMAL(10,2) NOT NULL DEFAULT 0.00,
is_subscription TINYINT(1) NOT NULL DEFAULT 0,
billing_interval VARCHAR(20) NULL,
trial_days INT(11) NOT NULL DEFAULT 0,
benefits TEXT NULL,
role VARCHAR(100) NULL,
stripe_price_id VARCHAR(255) NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY name (name)
) $charset_collate;\n";
$sql .= "CREATE TABLE $table_subscriptions (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT(20) UNSIGNED NOT NULL,
plan_id BIGINT(20) UNSIGNED NOT NULL,
stripe_customer_id VARCHAR(255) NULL,
stripe_subscription_id VARCHAR(255) NULL,
status VARCHAR(50) NOT NULL DEFAULT 'pending',
started_at DATETIME NOT NULL,
expires_at DATETIME NULL,
cancelled_at DATETIME NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY user_id (user_id),
KEY plan_id (plan_id),
KEY status (status),
KEY stripe_subscription_id (stripe_subscription_id)
) $charset_collate;\n";
$sql .= "CREATE TABLE $table_payments (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT(20) UNSIGNED NOT NULL,
subscription_id BIGINT(20) UNSIGNED NULL,
plan_id BIGINT(20) UNSIGNED NOT NULL,
stripe_payment_intent VARCHAR(255) NULL,
stripe_invoice_id VARCHAR(255) NULL,
amount DECIMAL(10,2) NOT NULL DEFAULT 0.00,
currency VARCHAR(10) NOT NULL DEFAULT 'usd',
status VARCHAR(50) NOT NULL DEFAULT 'pending',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY user_id (user_id),
KEY subscription_id (subscription_id),
KEY plan_id (plan_id)
) $charset_collate;\n";
$sql .= "CREATE TABLE $table_access_rules (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
content_type VARCHAR(20) NOT NULL,
content_id BIGINT(20) UNSIGNED NOT NULL,
plan_ids TEXT NOT NULL,
redirect_type VARCHAR(20) NOT NULL DEFAULT 'checkout',
custom_url VARCHAR(500) NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY content_type (content_type),
KEY content_id (content_id),
UNIQUE KEY content_rule (content_type, content_id)
) $charset_collate;";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( $sql );
self::create_default_plans();
self::set_plugin_version();
}
private static function create_default_plans() {
global $wpdb;
$plans_count = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}pc_membership_plans" );
if ( $plans_count > 0 ) {
return;
}
$wpdb->insert( $wpdb->prefix . 'pc_membership_plans', array(
'name' => __( 'Basic', 'pc-membership-abc123' ),
'description' => __( 'Get started with basic membership features.', 'pc-membership-abc123' ),
'price' => 9.99,
'is_subscription' => 1,
'billing_interval' => 'month',
'trial_days' => 0,
'benefits' => __( "Access to basic content\nEmail support\nMonthly newsletter", 'pc-membership-abc123' ),
'role' => 'subscriber',
) );
$wpdb->insert( $wpdb->prefix . 'pc_membership_plans', array(
'name' => __( 'Premium', 'pc-membership-abc123' ),
'description' => __( 'Unlock all premium features and content.', 'pc-membership-abc123' ),
'price' => 29.99,
'is_subscription' => 1,
'billing_interval' => 'month',
'trial_days' => 14,
'benefits' => __( "Access to all content\nPriority support\nExclusive webinars\nDownload resources\n24/7 chat support", 'pc-membership-abc123' ),
'role' => 'pc_member_1',
) );
}
private static function set_plugin_version() {
update_option( 'pc_membership_version', PC_MEMBERSHIP_VERSION );
}
}

View File

@@ -0,0 +1,13 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Handles plugin deactivation tasks.
*/
class PC_Membership_Deactivator {
public static function deactivate() {
// No specific actions needed on deactivation currently.
}
}
?>

View File

@@ -0,0 +1,191 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
function pc_membership_format_price( $price, $currency = null ) {
if ( is_null( $currency ) ) {
$options = get_option( 'pc_membership_options' );
$currency = isset( $options['currency'] ) ? $options['currency'] : 'usd';
}
$symbols = array(
'usd' => '$',
'eur' => '€',
'gbp' => '£',
);
$symbol = isset( $symbols[ $currency ] ) ? $symbols[ $currency ] : $currency;
return $symbol . number_format( (float) $price, 2 );
}
function pc_membership_get_plan( $plan_id ) {
global $wpdb;
return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}pc_membership_plans WHERE id = %d", $plan_id ) );
}
function pc_membership_get_all_plans() {
global $wpdb;
return $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}pc_membership_plans ORDER BY price ASC" );
}
function pc_membership_get_user_subscription( $user_id ) {
global $wpdb;
return $wpdb->get_row( $wpdb->prepare(
"SELECT s.*, p.name as plan_name, p.role, p.is_subscription, p.billing_interval
FROM {$wpdb->prefix}pc_membership_subscriptions s
LEFT JOIN {$wpdb->prefix}pc_membership_plans p ON s.plan_id = p.id
WHERE s.user_id = %d AND s.status = 'active'
ORDER BY s.id DESC LIMIT 1",
$user_id
) );
}
function pc_membership_is_user_on_plan( $user_id, $plan_id ) {
$subscription = pc_membership_get_user_subscription( $user_id );
return $subscription && $subscription->plan_id == $plan_id;
}
function pc_membership_has_active_subscription( $user_id ) {
return (bool) pc_membership_get_user_subscription( $user_id );
}
function pc_membership_get_page_url( $page_type ) {
$options = get_option( 'pc_membership_options' );
$page_id = isset( $options[ $page_type . '_page_id' ] ) ? absint( $options[ $page_type . '_page_id' ] ) : 0;
return $page_id ? get_permalink( $page_id ) : home_url();
}
function pc_membership_redirect_to_checkout() {
$checkout_url = pc_membership_get_page_url( 'checkout' );
if ( $checkout_url ) {
wp_redirect( $checkout_url );
exit;
}
}
function pc_membership_redirect_to_login() {
$login_url = pc_membership_get_page_url( 'login' );
if ( $login_url ) {
wp_redirect( $login_url );
exit;
}
wp_redirect( wp_login_url() );
exit;
}
function pc_membership_register_user( $user_login, $user_email, $user_password ) {
$errors = new WP_Error();
if ( username_exists( $user_login ) ) {
$errors->add( 'username_exists', __( 'Username already exists.', 'pc-membership-abc123' ) );
}
if ( email_exists( $user_email ) ) {
$errors->add( 'email_exists', __( 'Email already registered.', 'pc-membership-abc123' ) );
}
if ( ! is_email( $user_email ) ) {
$errors->add( 'invalid_email', __( 'Invalid email address.', 'pc-membership-abc123' ) );
}
if ( ! validate_username( $user_login ) ) {
$errors->add( 'invalid_username', __( 'Invalid username.', 'pc-membership-abc123' ) );
}
if ( strlen( $user_password ) < 8 ) {
$errors->add( 'weak_password', __( 'Password must be at least 8 characters.', 'pc-membership-abc123' ) );
}
if ( $errors->has_errors() ) {
return $errors;
}
$user_id = wp_create_user( $user_login, $user_password, $user_email );
if ( is_wp_error( $user_id ) ) {
return $user_id;
}
wp_update_user( array(
'ID' => $user_id,
'display_name' => $user_login,
) );
$user = get_userdata( $user_id );
$user->set_role( 'subscriber' );
do_action( 'pc_membership_user_registered', $user_id, $user_login, $user_email );
return $user_id;
}
function pc_membership_authenticate_user( $user_login, $user_password ) {
$user = wp_authenticate_username_password( null, $user_login, $user_password );
if ( is_wp_error( $user ) ) {
return $user;
}
wp_set_auth_cookie( $user->ID, true );
do_action( 'wp_login', $user_login, $user );
return $user;
}
function pc_membership_create_checkout_session( $plan_id, $user_id = null ) {
if ( ! class_exists( 'PC_Membership_Stripe' ) ) {
require_once PC_MEMBERSHIP_PLUGIN_DIR . 'includes/stripe-handler.php';
}
return PC_Membership_Stripe::create_checkout_session( $plan_id, $user_id );
}
function pc_membership_restrict_content( $content_id, $plan_ids, $redirect = 'checkout' ) {
if ( ! class_exists( 'PC_Membership_Access_Control' ) ) {
require_once PC_MEMBERSHIP_PLUGIN_DIR . 'includes/access-control.php';
}
return PC_Membership_Access_Control::restrict_post( $content_id, $plan_ids, $redirect );
}
function pc_membership_unrestrict_content( $content_id ) {
if ( ! class_exists( 'PC_Membership_Access_Control' ) ) {
require_once PC_MEMBERSHIP_PLUGIN_DIR . 'includes/access-control.php';
}
return PC_Membership_Access_Control::unrestrict_post( $content_id );
}
function pc_membership_get_stats() {
global $wpdb;
$stats = array(
'active_members' => 0,
'active_subscriptions' => 0,
'total_plans' => 0,
'revenue_this_month' => 0,
'revenue_last_month' => 0,
);
$stats['active_members'] = $wpdb->get_var( "SELECT COUNT(DISTINCT user_id) FROM {$wpdb->prefix}pc_membership_subscriptions WHERE status = 'active'" );
$stats['active_subscriptions'] = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}pc_membership_subscriptions WHERE status = 'active'" );
$stats['total_plans'] = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}pc_membership_plans" );
$current_month = date( 'Y-m-01' );
$last_month = date( 'Y-m-01', strtotime( '-1 month' ) );
$stats['revenue_this_month'] = $wpdb->get_var( $wpdb->prepare(
"SELECT COALESCE(SUM(amount), 0) FROM {$wpdb->prefix}pc_membership_payments WHERE status = 'succeeded' AND created_at >= %s",
$current_month
) );
$stats['revenue_last_month'] = $wpdb->get_var( $wpdb->prepare(
"SELECT COALESCE(SUM(amount), 0) FROM {$wpdb->prefix}pc_membership_payments WHERE status = 'succeeded' AND created_at >= %s AND created_at < %s",
$last_month,
$current_month
) );
return $stats;
}

View File

@@ -0,0 +1,520 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class PC_Membership_Stripe {
private static function get_options() {
return get_option( 'pc_membership_options', array() );
}
private static function get_secret_key() {
$options = self::get_options();
$mode = isset( $options['mode'] ) ? $options['mode'] : 'test';
return isset( $options[ $mode . '_secret_key' ] ) ? $options[ $mode . '_secret_key' ] : '';
}
private static function get_publishable_key() {
$options = self::get_options();
$mode = isset( $options['mode'] ) ? $options['mode'] : 'test';
return isset( $options[ $mode . '_publishable_key' ] ) ? $options[ $mode . '_publishable_key' ] : '';
}
private static function get_currency() {
$options = self::get_options();
return isset( $options['currency'] ) ? $options['currency'] : 'usd';
}
public static function init() {
$secret_key = self::get_secret_key();
if ( empty( $secret_key ) ) {
return false;
}
if ( ! class_exists( '\Stripe\Stripe' ) ) {
if ( file_exists( PC_MEMBERSHIP_PLUGIN_DIR . 'vendor/autoload.php' ) ) {
require_once PC_MEMBERSHIP_PLUGIN_DIR . 'vendor/autoload.php';
}
}
if ( class_exists( '\Stripe\Stripe' ) ) {
\Stripe\Stripe::setApiKey( $secret_key );
\Stripe\Stripe::setApiVersion( '2023-10-16' );
return true;
}
return false;
}
public static function create_checkout_session( $plan_id, $user_id = null, $customer_email = null ) {
if ( ! self::init() ) {
return new WP_Error( 'stripe_not_configured', __( 'Stripe is not configured properly.', 'pc-membership-abc123' ) );
}
global $wpdb;
$plan = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}pc_membership_plans WHERE id = %d", $plan_id ) );
if ( ! $plan ) {
return new WP_Error( 'plan_not_found', __( 'Plan not found.', 'pc-membership-abc123' ) );
}
$user = null;
$wp_user_id = null;
if ( $user_id ) {
$wp_user_id = $user_id;
} elseif ( is_user_logged_in() ) {
$wp_user_id = get_current_user_id();
}
$customer_id = null;
$session_data = array(
'payment_method_types' => array( 'card' ),
'line_items' => array(),
'success_url' => add_query_arg( array(
'session_id' => '{CHECKOUT_SESSION_ID}',
'plan_id' => $plan_id,
), self::get_success_url() ),
'cancel_url' => self::get_cancel_url(),
'metadata' => array(
'plan_id' => $plan_id,
'wp_user_id' => $wp_user_id ?: 0,
),
);
try {
if ( $wp_user_id ) {
$user = get_userdata( $wp_user_id );
if ( $user ) {
$customer_id = self::get_or_create_customer( $user, $wp_user_id );
if ( ! is_wp_error( $customer_id ) ) {
$session_data['customer'] = $customer_id;
} else {
$session_data['customer_email'] = $user->user_email;
}
}
} elseif ( $customer_email ) {
$session_data['customer_email'] = $customer_email;
}
if ( $plan->is_subscription ) {
$price_data = array(
'currency' => self::get_currency(),
'product_data' => array(
'name' => $plan->name,
'description' => $plan->description ? substr( $plan->description, 0, 500 ) : '',
),
'recurring' => array(
'interval' => $plan->billing_interval,
),
'unit_amount' => intval( $plan->price * 100 ),
);
$session_data['mode'] = 'subscription';
$session_data['line_items'] = array(
array(
'price_data' => $price_data,
'quantity' => 1,
),
);
if ( $plan->trial_days > 0 ) {
$session_data['subscription_data'] = array(
'trial_period_days' => $plan->trial_days,
);
}
} else {
$session_data['mode'] = 'payment';
$session_data['line_items'] = array(
array(
'price_data' => array(
'currency' => self::get_currency(),
'product_data' => array(
'name' => $plan->name,
'description' => $plan->description ? substr( $plan->description, 0, 500 ) : '',
),
'unit_amount' => intval( $plan->price * 100 ),
),
'quantity' => 1,
),
);
}
$session = \Stripe\Checkout\Session::create( $session_data );
return $session;
} catch ( \Exception $e ) {
return new WP_Error( 'stripe_error', $e->getMessage() );
}
}
public static function get_or_create_customer( $user, $wp_user_id ) {
global $wpdb;
$subscription = $wpdb->get_row( $wpdb->prepare(
"SELECT stripe_customer_id FROM {$wpdb->prefix}pc_membership_subscriptions WHERE user_id = %d ORDER BY id DESC LIMIT 1",
$wp_user_id
) );
if ( $subscription && ! empty( $subscription->stripe_customer_id ) ) {
return $subscription->stripe_customer_id;
}
try {
$customer = \Stripe\Customer::create( array(
'email' => $user->user_email,
'name' => $user->display_name,
'metadata' => array(
'wp_user_id' => $wp_user_id,
),
) );
return $customer->id;
} catch ( \Exception $e ) {
return new WP_Error( 'stripe_customer_error', $e->getMessage() );
}
}
public static function cancel_subscription( $stripe_subscription_id ) {
if ( ! self::init() ) {
return new WP_Error( 'stripe_not_configured', __( 'Stripe is not configured properly.', 'pc-membership-abc123' ) );
}
try {
$subscription = \Stripe\Subscription::retrieve( $stripe_subscription_id );
$subscription->cancel();
return true;
} catch ( \Exception $e ) {
return new WP_Error( 'stripe_error', $e->getMessage() );
}
}
public static function create_portal_session() {
if ( ! self::init() ) {
return new WP_Error( 'stripe_not_configured', __( 'Stripe is not configured properly.', 'pc-membership-abc123' ) );
}
if ( ! is_user_logged_in() ) {
return new WP_Error( 'not_logged_in', __( 'User must be logged in.', 'pc-membership-abc123' ) );
}
global $wpdb;
$subscription = $wpdb->get_row( $wpdb->prepare(
"SELECT stripe_customer_id FROM {$wpdb->prefix}pc_membership_subscriptions WHERE user_id = %d AND status = 'active' ORDER BY id DESC LIMIT 1",
get_current_user_id()
) );
if ( ! $subscription || empty( $subscription->stripe_customer_id ) ) {
return new WP_Error( 'no_customer', __( 'No Stripe customer found.', 'pc-membership-abc123' ) );
}
try {
$session = \Stripe\BillingPortal\Session::create( array(
'customer' => $subscription->stripe_customer_id,
'return_url' => self::get_page_url( 'account' ),
) );
return $session;
} catch ( \Exception $e ) {
return new WP_Error( 'stripe_error', $e->getMessage() );
}
}
public static function retrieve_session( $session_id ) {
if ( ! self::init() ) {
return new WP_Error( 'stripe_not_configured', __( 'Stripe is not configured properly.', 'pc-membership-abc123' ) );
}
try {
$session = \Stripe\Checkout\Session::retrieve( $session_id );
return $session;
} catch ( \Exception $e ) {
return new WP_Error( 'stripe_error', $e->getMessage() );
}
}
public static function handle_webhook() {
if ( ! self::init() ) {
return;
}
$options = get_option( 'pc_membership_options' );
$webhook_secret = isset( $options['webhook_secret'] ) ? $options['webhook_secret'] : '';
$payload = file_get_contents( 'php://input' );
$sig_header = isset( $_SERVER['HTTP_STRIPE_SIGNATURE'] ) ? $_SERVER['HTTP_STRIPE_SIGNATURE'] : '';
if ( empty( $webhook_secret ) || empty( $sig_header ) ) {
return;
}
try {
$event = \Stripe\Webhook::constructEvent( $payload, $sig_header, $webhook_secret );
} catch ( \Exception $e ) {
return;
}
self::process_webhook_event( $event );
}
private static function process_webhook_event( $event ) {
global $wpdb;
switch ( $event->type ) {
case 'checkout.session.completed':
self::handle_checkout_completed( $event->data->object );
break;
case 'customer.subscription.created':
case 'customer.subscription.updated':
self::handle_subscription_updated( $event->data->object );
break;
case 'customer.subscription.deleted':
self::handle_subscription_deleted( $event->data->object );
break;
case 'invoice.payment_succeeded':
self::handle_payment_succeeded( $event->data->object );
break;
case 'invoice.payment_failed':
self::handle_payment_failed( $event->data->object );
break;
}
}
private static function handle_checkout_completed( $session ) {
global $wpdb;
$plan_id = isset( $session->metadata->plan_id ) ? absint( $session->metadata->plan_id ) : 0;
$wp_user_id = isset( $session->metadata->wp_user_id ) ? absint( $session->metadata->wp_user_id ) : 0;
$customer_id = $session->customer;
$subscription_id = null;
if ( empty( $plan_id ) ) {
return;
}
$plan = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}pc_membership_plans WHERE id = %d", $plan_id ) );
if ( ! $plan ) {
return;
}
if ( $plan->is_subscription && ! empty( $session->subscription ) ) {
$subscription_id = $session->subscription;
try {
$stripe_subscription = \Stripe\Subscription::retrieve( $subscription_id );
$period_end = date( 'Y-m-d H:i:s', $stripe_subscription->current_period_end );
} catch ( \Exception $e ) {
$period_end = null;
}
} else {
$period_end = null;
}
if ( $wp_user_id && $customer_id ) {
$existing = $wpdb->get_var( $wpdb->prepare(
"SELECT id FROM {$wpdb->prefix}pc_membership_subscriptions WHERE user_id = %d AND plan_id = %d",
$wp_user_id,
$plan_id
) );
if ( $existing ) {
$wpdb->update( $wpdb->prefix . 'pc_membership_subscriptions', array(
'stripe_customer_id' => $customer_id,
'stripe_subscription_id' => $subscription_id ?: '',
'status' => 'active',
'expires_at' => $period_end,
), array( 'id' => $existing ) );
} else {
$wpdb->insert( $wpdb->prefix . 'pc_membership_subscriptions', array(
'user_id' => $wp_user_id,
'plan_id' => $plan_id,
'stripe_customer_id' => $customer_id,
'stripe_subscription_id' => $subscription_id ?: '',
'status' => 'active',
'started_at' => current_time( 'mysql' ),
'expires_at' => $period_end,
) );
if ( $plan->role ) {
$user = get_userdata( $wp_user_id );
if ( $user ) {
$user->set_role( $plan->role );
}
}
}
if ( $session->mode === 'payment' && isset( $session->amount_total ) ) {
$wpdb->insert( $wpdb->prefix . 'pc_membership_payments', array(
'user_id' => $wp_user_id,
'plan_id' => $plan_id,
'stripe_payment_intent' => $session->payment_intent ?: '',
'amount' => $session->amount_total / 100,
'currency' => self::get_currency(),
'status' => 'succeeded',
) );
}
}
}
private static function handle_subscription_updated( $subscription ) {
global $wpdb;
$stripe_sub_id = $subscription->id;
$customer_id = $subscription->customer;
$status = self::map_stripe_status( $subscription->status );
$period_end = date( 'Y-m-d H:i:s', $subscription->current_period_end );
$existing = $wpdb->get_row( $wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}pc_membership_subscriptions WHERE stripe_subscription_id = %s",
$stripe_sub_id
) );
if ( $existing ) {
$wpdb->update( $wpdb->prefix . 'pc_membership_subscriptions', array(
'status' => $status,
'expires_at' => $period_end,
), array( 'id' => $existing->id ) );
if ( $status === 'active' ) {
$plan = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}pc_membership_plans WHERE id = %d", $existing->plan_id ) );
if ( $plan && $plan->role ) {
$user = get_userdata( $existing->user_id );
if ( $user ) {
$user->set_role( $plan->role );
}
}
}
}
}
private static function handle_subscription_deleted( $subscription ) {
global $wpdb;
$stripe_sub_id = $subscription->id;
$existing = $wpdb->get_row( $wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}pc_membership_subscriptions WHERE stripe_subscription_id = %s",
$stripe_sub_id
) );
if ( $existing ) {
$wpdb->update( $wpdb->prefix . 'pc_membership_subscriptions', array(
'status' => 'cancelled',
), array( 'id' => $existing->id ) );
$user = get_userdata( $existing->user_id );
if ( $user ) {
$user->set_role( 'subscriber' );
}
}
}
private static function handle_payment_succeeded( $invoice ) {
global $wpdb;
if ( empty( $invoice->subscription ) ) {
return;
}
$stripe_sub_id = $invoice->subscription;
$subscription = $wpdb->get_row( $wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}pc_membership_subscriptions WHERE stripe_subscription_id = %s",
$stripe_sub_id
) );
if ( ! $subscription ) {
return;
}
$wpdb->insert( $wpdb->prefix . 'pc_membership_payments', array(
'user_id' => $subscription->user_id,
'plan_id' => $subscription->plan_id,
'stripe_payment_intent' => $invoice->payment_intent ?: '',
'amount' => $invoice->amount_paid / 100,
'currency' => strtolower( $invoice->currency ),
'status' => 'succeeded',
) );
if ( isset( $invoice->billing_reason ) && $invoice->billing_reason === 'subscription_cycle' ) {
$stripe_sub = \Stripe\Subscription::retrieve( $stripe_sub_id );
$period_end = date( 'Y-m-d H:i:s', $stripe_sub->current_period_end );
$wpdb->update( $wpdb->prefix . 'pc_membership_subscriptions', array(
'expires_at' => $period_end,
), array( 'id' => $subscription->id ) );
}
}
private static function handle_payment_failed( $invoice ) {
global $wpdb;
if ( empty( $invoice->subscription ) ) {
return;
}
$stripe_sub_id = $invoice->subscription;
$subscription = $wpdb->get_row( $wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}pc_membership_subscriptions WHERE stripe_subscription_id = %s",
$stripe_sub_id
) );
if ( $subscription ) {
$user = get_user_by( 'id', $subscription->user_id );
if ( $user ) {
wp_mail(
$user->user_email,
__( 'Payment Failed', 'pc-membership-abc123' ),
__( 'Your membership payment has failed. Please update your payment method to continue your subscription.', 'pc-membership-abc123' )
);
}
}
}
private static function map_stripe_status( $stripe_status ) {
$map = array(
'active' => 'active',
'past_due' => 'past_due',
'unpaid' => 'unpaid',
'canceled' => 'cancelled',
'incomplete' => 'pending',
'incomplete_expired' => 'expired',
'trialing' => 'active',
'paused' => 'paused',
);
return isset( $map[ $stripe_status ] ) ? $map[ $stripe_status ] : 'pending';
}
private static function get_success_url() {
$options = get_option( 'pc_membership_options' );
$page_id = isset( $options['success_page_id'] ) ? absint( $options['success_page_id'] ) : 0;
return $page_id ? get_permalink( $page_id ) : home_url();
}
private static function get_cancel_url() {
$options = get_option( 'pc_membership_options' );
$page_id = isset( $options['cancel_page_id'] ) ? absint( $options['cancel_page_id'] ) : 0;
return $page_id ? get_permalink( $page_id ) : home_url();
}
private static function get_page_url( $page_type ) {
$options = get_option( 'pc_membership_options' );
$page_id = isset( $options[ $page_type . '_page_id' ] ) ? absint( $options[ $page_type . '_page_id' ] ) : 0;
return $page_id ? get_permalink( $page_id ) : home_url();
}
}

View File

@@ -0,0 +1,131 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class PC_Membership_User_Roles {
const MEMBER_ROLE_PREFIX = 'pc_member_';
public static function init() {
add_action( 'init', array( __CLASS__, 'register_member_roles' ) );
add_filter( 'editable_roles', array( __CLASS__, 'filter_editable_roles' ) );
add_action( 'pc_membership_subscription_activated', array( __CLASS__, 'on_subscription_activated' ), 10, 2 );
add_action( 'pc_membership_subscription_cancelled', array( __CLASS__, 'on_subscription_cancelled' ), 10, 2 );
}
public static function register_member_roles() {
$base_role = get_role( 'subscriber' );
if ( ! $base_role ) {
return;
}
$capabilities = $base_role->capabilities;
foreach ( range( 1, 10 ) as $level ) {
$role_name = self::MEMBER_ROLE_PREFIX . $level;
if ( ! get_role( $role_name ) ) {
add_role( $role_name, sprintf( __( 'Member Level %d', 'pc-membership-abc123' ), $level ), $capabilities );
}
}
}
public static function get_member_role_for_plan( $plan_id ) {
global $wpdb;
$plan = $wpdb->get_row( $wpdb->prepare( "SELECT role FROM {$wpdb->prefix}pc_membership_plans WHERE id = %d", $plan_id ) );
if ( $plan && ! empty( $plan->role ) ) {
return $plan->role;
}
return self::MEMBER_ROLE_PREFIX . '1';
}
public static function set_user_member_role( $user_id, $plan_id ) {
$role = self::get_member_role_for_plan( $plan_id );
$user = get_userdata( $user_id );
if ( $user ) {
$user->set_role( $role );
}
}
public static function remove_member_role( $user_id ) {
$user = get_userdata( $user_id );
if ( $user ) {
$user->set_role( 'subscriber' );
}
}
public static function on_subscription_activated( $user_id, $plan_id ) {
self::set_user_member_role( $user_id, $plan_id );
}
public static function on_subscription_cancelled( $user_id, $plan_id ) {
self::remove_member_role( $user_id );
}
public static function filter_editable_roles( $roles ) {
foreach ( $roles as $role_key => $role_data ) {
if ( strpos( $role_key, self::MEMBER_ROLE_PREFIX ) === 0 ) {
unset( $roles[ $role_key ] );
}
}
return $roles;
}
public static function get_plan_for_user( $user_id ) {
global $wpdb;
$subscription = $wpdb->get_row( $wpdb->prepare(
"SELECT plan_id FROM {$wpdb->prefix}pc_membership_subscriptions WHERE user_id = %d AND status = 'active' ORDER BY id DESC LIMIT 1",
$user_id
) );
if ( ! $subscription ) {
return null;
}
return $subscription->plan_id;
}
public static function is_user_on_plan( $user_id, $plan_id ) {
$user_plan = self::get_plan_for_user( $user_id );
return $user_plan == $plan_id;
}
public static function user_has_active_subscription( $user_id ) {
global $wpdb;
$count = $wpdb->get_var( $wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}pc_membership_subscriptions WHERE user_id = %d AND status = 'active'",
$user_id
) );
return $count > 0;
}
public static function add_member_capabilities() {
$role = get_role( 'administrator' );
if ( $role ) {
$role->add_cap( 'pc_membership_manage' );
$role->add_cap( 'pc_membership_view_stats' );
}
}
public static function remove_member_capabilities() {
$role = get_role( 'administrator' );
if ( $role ) {
$role->remove_cap( 'pc_membership_manage' );
$role->remove_cap( 'pc_membership_view_stats' );
}
}
}

View File

@@ -0,0 +1,96 @@
<?php
/**
* Plugin Name: PC Membership Plugin
* Plugin URI: https://plugincompass.com/plugins/pc-membership-abc123
* Description: Complete membership system with custom pages, Stripe payments (one-time & subscriptions), plan management, and access control.
* Version: 1.0.0
* Author: Plugin Compass
* Author URI: https://plugincompass.com
* Text Domain: pc-membership-abc123
* Domain Path: /languages
* Update URI: false
* License: GPL v2 or later
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
define( 'PC_MEMBERSHIP_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'PC_MEMBERSHIP_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
define( 'PC_MEMBERSHIP_SLUG', 'pc-membership-abc123' );
define( 'PC_MEMBERSHIP_VERSION', '1.0.0' );
spl_autoload_register( function( $class ) {
if ( strpos( $class, 'PC_Membership_' ) !== 0 ) {
return;
}
$relative_class = substr( $class, strlen( 'PC_Membership_' ) );
$file = PC_MEMBERSHIP_PLUGIN_DIR . 'includes/' . strtolower( str_replace( '_', '-', $relative_class ) ) . '.php';
if ( file_exists( $file ) ) {
require_once $file;
}
} );
require_once PC_MEMBERSHIP_PLUGIN_DIR . 'includes/activator.php';
require_once PC_MEMBERSHIP_PLUGIN_DIR . 'includes/deactivator.php';
register_activation_hook( __FILE__, array( 'PC_Membership_Activator', 'activate' ) );
register_deactivation_hook( __FILE__, array( 'PC_Membership_Deactivator', 'deactivate' ) );
require_once PC_MEMBERSHIP_PLUGIN_DIR . 'admin/admin.php';
require_once PC_MEMBERSHIP_PLUGIN_DIR . 'public/public.php';
require_once PC_MEMBERSHIP_PLUGIN_DIR . 'includes/access-control.php';
require_once PC_MEMBERSHIP_PLUGIN_DIR . 'includes/user-roles.php';
require_once PC_MEMBERSHIP_PLUGIN_DIR . 'includes/helpers.php';
function pc_membership_init() {
load_plugin_textdomain( 'pc-membership-abc123', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' );
PC_Membership_Admin::init();
PC_Membership_Public::init();
PC_Membership_Access_Control::init();
PC_Membership_User_Roles::init();
}
add_action( 'plugins_loaded', 'pc_membership_init' );
add_action( 'init', 'pc_membership_register_webhook_endpoint' );
function pc_membership_register_webhook_endpoint() {
add_rewrite_rule( '^pc-membership-webhook/?$', 'index.php?pc_membership_webhook=1', 'top' );
}
add_filter( 'query_vars', 'pc_membership_webhook_query_vars' );
function pc_membership_webhook_query_vars( $vars ) {
$vars[] = 'pc_membership_webhook';
return $vars;
}
add_action( 'template_redirect', 'pc_membership_handle_webhook' );
function pc_membership_handle_webhook() {
if ( intval( get_query_var( 'pc_membership_webhook' ) ) === 1 ) {
if ( ! class_exists( 'PC_Membership_Stripe' ) ) {
require_once PC_MEMBERSHIP_PLUGIN_DIR . 'includes/stripe-handler.php';
}
PC_Membership_Stripe::handle_webhook();
exit;
}
}
register_activation_hook( __FILE__, 'pc_membership_flush_rewrites' );
function pc_membership_flush_rewrites() {
pc_membership_register_webhook_endpoint();
flush_rewrite_rules();
}
register_deactivation_hook( __FILE__, 'pc_membership_flush_rewrites' );
function pc_membership_deactivation_flush() {
flush_rewrite_rules();
}
add_filter( 'site_transient_update_plugins', function( $value ) {
$plugin_file = plugin_basename( __FILE__ );
if ( isset( $value->response[ $plugin_file ] ) ) {
unset( $value->response[ $plugin_file ] );
}
return $value;
} );

View File

@@ -0,0 +1,449 @@
.pc-membership-checkout-wrapper {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.pc-membership-pricing-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 24px;
padding: 20px 0;
}
.pc-membership-pricing-card {
background: #fff;
border: 2px solid #e5e5e5;
border-radius: 8px;
padding: 24px;
text-align: center;
transition: all 0.3s ease;
position: relative;
}
.pc-membership-pricing-card:hover {
border-color: #2271b1;
transform: translateY(-4px);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.pc-membership-pricing-card.featured {
border-color: #2271b1;
transform: scale(1.02);
}
.pc-membership-pricing-card.featured::before {
content: 'Popular';
position: absolute;
top: -12px;
left: 50%;
transform: translateX(-50%);
background: #2271b1;
color: #fff;
padding: 4px 16px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.pc-membership-pricing-header {
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid #e5e5e5;
}
.pc-membership-plan-name {
font-size: 24px;
margin: 0 0 15px;
color: #23282d;
font-weight: 700;
}
.pc-membership-plan-price {
color: #2271b1;
}
.pc-membership-price-amount {
font-size: 42px;
font-weight: 700;
line-height: 1;
}
.pc-membership-price-interval {
font-size: 16px;
color: #646970;
font-weight: 400;
}
.pc-membership-trial-badge {
display: inline-block;
background: #46b450;
color: #fff;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
margin-top: 10px;
}
.pc-membership-plan-description {
color: #646970;
margin-bottom: 20px;
font-size: 14px;
line-height: 1.5;
}
.pc-membership-plan-benefits {
list-style: none;
padding: 0;
margin: 0 0 24px;
text-align: left;
}
.pc-membership-plan-benefits li {
padding: 10px 0;
display: flex;
align-items: center;
gap: 10px;
color: #3c434a;
border-bottom: 1px solid #f0f0f1;
}
.pc-membership-plan-benefits li:last-child {
border-bottom: none;
}
.pc-membership-plan-benefits .dashicons {
color: #46b450;
font-size: 20px;
width: 20px;
height: 20px;
flex-shrink: 0;
}
.pc-membership-select-plan-btn,
.pc-membership-checkout-btn {
width: 100%;
padding: 14px 24px;
font-size: 16px;
font-weight: 600;
border-radius: 4px;
transition: all 0.2s ease;
}
.pc-membership-checkout-form-wrapper {
background: #fff;
border-radius: 8px;
padding: 30px;
margin-top: 30px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.pc-membership-login-wrapper,
.pc-membership-register-wrapper,
.pc-membership-account-wrapper {
max-width: 500px;
margin: 0 auto;
padding: 40px 20px;
}
.pc-membership-login-form-container,
.pc-membership-register-form-container,
.pc-membership-account-wrapper {
background: #fff;
border-radius: 8px;
padding: 40px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.pc-membership-login-form-container h2,
.pc-membership-register-form-container h2,
.pc-membership-account-wrapper h2,
.pc-membership-account-wrapper h3 {
margin-top: 0;
margin-bottom: 24px;
color: #23282d;
}
.pc-membership-form-group {
margin-bottom: 20px;
}
.pc-membership-form-group label {
display: block;
font-weight: 600;
margin-bottom: 6px;
color: #3c434a;
}
.pc-membership-form-group input[type="text"],
.pc-membership-form-group input[type="email"],
.pc-membership-form-group input[type="password"] {
width: 100%;
padding: 12px;
border: 1px solid #c3c4c7;
border-radius: 4px;
font-size: 16px;
transition: border-color 0.2s ease;
}
.pc-membership-form-group input:focus {
outline: none;
border-color: #2271b1;
box-shadow: 0 0 0 3px rgba(34, 113, 177, 0.1);
}
.pc-membership-remember-me {
font-weight: normal;
}
.pc-membership-remember-me input {
margin-right: 8px;
}
.pc-membership-login-btn,
.pc-membership-register-btn {
width: 100%;
padding: 14px 24px;
font-size: 16px;
font-weight: 600;
margin-top: 10px;
}
.pc-membership-login-links {
margin-top: 20px;
text-align: center;
padding-top: 20px;
border-top: 1px solid #e5e5e5;
}
.pc-membership-login-links a {
display: inline-block;
margin: 0 10px;
color: #2271b1;
text-decoration: none;
}
.pc-membership-login-links a:hover {
text-decoration: underline;
}
.pc-membership-login-message,
.pc-membership-register-message {
margin-top: 15px;
padding: 12px;
border-radius: 4px;
display: none;
}
.pc-membership-login-message.success,
.pc-membership-register-message.success {
background: #dff0d8;
border: 1px solid #d6e9c6;
color: #3c763d;
display: block;
}
.pc-membership-login-message.error,
.pc-membership-register-message.error {
background: #f2dede;
border: 1px solid #ebccd1;
color: #a94442;
display: block;
}
.pc-membership-account-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid #e5e5e5;
}
.pc-membership-account-header h2 {
margin: 0;
}
.pc-membership-subscription-details {
background: #f0f0f1;
border-radius: 8px;
padding: 24px;
margin-bottom: 30px;
}
.pc-membership-subscription-details h3 {
margin-top: 0;
margin-bottom: 20px;
}
.pc-membership-subscription-info {
margin-bottom: 20px;
}
.pc-membership-subscription-row {
display: flex;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid #e5e5e5;
}
.pc-membership-subscription-row:last-child {
border-bottom: none;
}
.pc-membership-label {
color: #646970;
font-weight: 500;
}
.pc-membership-value {
font-weight: 600;
color: #23282d;
}
.pc-membership-status-active {
color: #46b450;
}
.pc-membership-status-cancelled,
.pc-membership-status-expired,
.pc-membership-status-past_due {
color: #dc3232;
}
.pc-membership-status-pending {
color: #f56e28;
}
.pc-membership-subscription-actions {
padding-top: 15px;
border-top: 1px solid #e5e5e5;
}
.pc-membership-no-subscription {
text-align: center;
padding: 40px 20px;
background: #f0f0f1;
border-radius: 8px;
}
.pc-membership-no-subscription p {
margin-bottom: 20px;
color: #646970;
}
.pc-membership-account-section {
margin-top: 30px;
padding-top: 30px;
border-top: 1px solid #e5e5e5;
}
.pc-membership-account-section h3 {
margin-bottom: 20px;
}
.pc-membership-success-wrapper,
.pc-membership-cancel-wrapper {
max-width: 600px;
margin: 0 auto;
padding: 60px 20px;
text-align: center;
}
.pc-membership-success-message,
.pc-membership-cancel-message {
background: #fff;
border-radius: 8px;
padding: 50px 40px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.pc-membership-success-icon,
.pc-membership-cancel-icon {
margin-bottom: 20px;
}
.pc-membership-success-message h2,
.pc-membership-cancel-message h2 {
margin: 0 0 15px;
color: #23282d;
}
.pc-membership-success-message p,
.pc-membership-cancel-message p {
color: #646970;
margin-bottom: 30px;
font-size: 16px;
}
.pc-membership-message {
padding: 15px 20px;
border-radius: 4px;
margin: 20px 0;
}
.pc-membership-message-info {
background: #d9edf7;
border: 1px solid #bce8f1;
color: #31708f;
}
.pc-membership-message-success {
background: #dff0d8;
border: 1px solid #d6e9c6;
color: #3c763d;
}
.pc-membership-message-error {
background: #f2dede;
border: 1px solid #ebccd1;
color: #a94442;
}
@media screen and (max-width: 768px) {
.pc-membership-pricing-cards {
grid-template-columns: 1fr;
}
.pc-membership-pricing-card.featured {
transform: none;
}
.pc-membership-login-form-container,
.pc-membership-register-form-container,
.pc-membership-account-wrapper {
padding: 24px;
}
.pc-membership-account-header {
flex-direction: column;
gap: 15px;
text-align: center;
}
.pc-membership-subscription-row {
flex-direction: column;
gap: 5px;
}
}
@media (prefers-reduced-motion: reduce) {
.pc-membership-pricing-card,
.pc-membership-select-plan-btn,
.pc-membership-pricing-card:hover {
transition: none;
transform: none;
}
}
@media (prefers-contrast: high) {
.pc-membership-pricing-card {
border-width: 3px;
}
.pc-membership-plan-benefits .dashicons {
color: #006600;
}
}

View File

@@ -0,0 +1,207 @@
(function($) {
'use strict';
var PCMembership = {
stripe: null,
init: function() {
this.bindEvents();
this.initStripe();
},
bindEvents: function() {
$(document).on('click', '.pc-membership-select-plan-btn', $.proxy(this.selectPlan, this));
$(document).on('submit', '#pc-membership-login-form', $.proxy(this.handleLogin, this));
$(document).on('submit', '#pc-membership-register-form', $.proxy(this.handleRegister, this));
$(document).on('submit', '#pc-membership-profile-form', $.proxy(this.handleProfileUpdate, this));
$(document).on('click', '#pc-membership-cancel-subscription', $.proxy(this.cancelSubscription, this));
$(document).on('click', '#pc-membership-update-payment-method', $.proxy(this.updatePaymentMethod, this));
},
initStripe: function() {
if (typeof Stripe !== 'undefined' && pcMembership.stripe_key) {
this.stripe = Stripe(pcMembership.stripe_key);
}
},
selectPlan: function(e) {
e.preventDefault();
var planId = $(e.currentTarget).data('plan-id');
if (!planId) {
alert(pcMembership.i18n.selectPlan);
return;
}
var $button = $(e.currentTarget);
var originalText = $button.text();
$button.text(pcMembership.i18n.processing).prop('disabled', true);
$.ajax({
url: pcMembership.ajax_url,
type: 'POST',
data: {
action: 'pc_membership_create_checkout',
plan_id: planId,
nonce: pcMembership.nonce
},
success: $.proxy(function(response) {
if (response.success && response.data.url) {
window.location.href = response.data.url;
} else {
alert(pcMembership.i18n.error + ': ' + (response.data || 'Unknown error'));
$button.text(originalText).prop('disabled', false);
}
}, this),
error: $.proxy(function() {
alert(pcMembership.i18n.error);
$button.text(originalText).prop('disabled', false);
}, this)
});
},
handleLogin: function(e) {
e.preventDefault();
var $form = $('#pc-membership-login-form');
var $message = $('#pc-membership-login-message');
var formData = $form.serialize();
$message.removeClass('success error').hide();
$.ajax({
url: pcMembership.ajax_url,
type: 'POST',
data: formData + '&action=pc_membership_login&nonce=' + pcMembership.nonce,
success: $.proxy(function(response) {
if (response.success) {
$message.addClass('success').text(pcMembership.i18n.success).show();
setTimeout(function() {
window.location.href = response.data.redirect || pcMembership.account_url;
}, 1000);
} else {
$message.addClass('error').text(response.data || pcMembership.i18n.error).show();
}
}, this),
error: function() {
$message.addClass('error').text(pcMembership.i18n.error).show();
}
});
},
handleRegister: function(e) {
e.preventDefault();
var $form = $('#pc-membership-register-form');
var $message = $('#pc-membership-register-message');
var formData = $form.serialize();
var password = $('#user_password').val();
var passwordConfirm = $('#user_password_confirm').val();
if (password !== passwordConfirm) {
$message.addClass('error').text('Passwords do not match').show();
return;
}
if (password.length < 8) {
$message.addClass('error').text('Password must be at least 8 characters').show();
return;
}
$message.removeClass('success error').hide();
$.ajax({
url: pcMembership.ajax_url,
type: 'POST',
data: formData + '&action=pc_membership_register&nonce=' + pcMembership.nonce,
success: $.proxy(function(response) {
if (response.success) {
$message.addClass('success').text(pcMembership.i18n.success).show();
setTimeout(function() {
window.location.href = response.data.redirect || pcMembership.account_url;
}, 1000);
} else {
$message.addClass('error').text(response.data || pcMembership.i18n.error).show();
}
}, this),
error: function() {
$message.addClass('error').text(pcMembership.i18n.error).show();
}
});
},
handleProfileUpdate: function(e) {
e.preventDefault();
var $form = $('#pc-membership-profile-form');
$.ajax({
url: pcMembership.ajax_url,
type: 'POST',
data: $form.serialize() + '&action=pc_membership_update_profile&nonce=' + pcMembership.nonce,
success: function(response) {
if (response.success) {
alert('Profile updated successfully');
} else {
alert(response.data || 'Error updating profile');
}
}
});
},
cancelSubscription: function(e) {
e.preventDefault();
if (!confirm(pcMembership.i18n.confirmCancel)) return;
var subscriptionId = $(e.currentTarget).data('subscription-id');
var $button = $(e.currentTarget);
$button.text(pcMembership.i18n.processing).prop('disabled', true);
$.ajax({
url: pcMembership.ajax_url,
type: 'POST',
data: {
action: 'pc_membership_cancel_subscription',
subscription_id: subscriptionId,
nonce: pcMembership.nonce
},
success: function(response) {
if (response.success) {
window.location.reload();
} else {
alert(pcMembership.i18n.error);
$button.text(pcMembership.i18n.cancel).prop('disabled', false);
}
},
error: function() {
alert(pcMembership.i18n.error);
$button.text(pcMembership.i18n.cancel).prop('disabled', false);
}
});
},
updatePaymentMethod: function(e) {
e.preventDefault();
$.ajax({
url: pcMembership.ajax_url,
type: 'POST',
data: {
action: 'pc_membership_update_payment_method',
nonce: pcMembership.nonce
},
success: function(response) {
if (response.success && response.data.url) {
window.location.href = response.data.url;
} else {
alert(pcMembership.i18n.error);
}
}
});
}
};
$(document).ready(function() {
PCMembership.init();
});
})(jQuery);

View File

@@ -0,0 +1,656 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class PC_Membership_Public {
public static function init() {
add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ) );
add_shortcode( 'pc_membership_checkout', array( __CLASS__, 'checkout_shortcode' ) );
add_shortcode( 'pc_membership_login', array( __CLASS__, 'login_shortcode' ) );
add_shortcode( 'pc_membership_register', array( __CLASS__, 'register_shortcode' ) );
add_shortcode( 'pc_membership_account', array( __CLASS__, 'account_shortcode' ) );
add_shortcode( 'pc_membership_success', array( __CLASS__, 'success_shortcode' ) );
add_shortcode( 'pc_membership_cancel', array( __CLASS__, 'cancel_shortcode' ) );
add_shortcode( 'pc_membership_pricing', array( __CLASS__, 'pricing_shortcode' ) );
add_action( 'wp_ajax_pc_membership_create_checkout', array( __CLASS__, 'ajax_create_checkout' ) );
add_action( 'wp_ajax_nopriv_pc_membership_create_checkout', array( __CLASS__, 'ajax_create_checkout' ) );
add_action( 'wp_ajax_pc_membership_cancel_subscription', array( __CLASS__, 'ajax_cancel_subscription' ) );
add_action( 'wp_ajax_nopriv_pc_membership_cancel_subscription', array( __CLASS__, 'ajax_cancel_subscription' ) );
add_action( 'wp_ajax_pc_membership_update_payment_method', array( __CLASS__, 'ajax_update_payment_method' ) );
add_action( 'wp_ajax_nopriv_pc_membership_update_payment_method', array( __CLASS__, 'ajax_update_payment_method' ) );
add_action( 'wp_ajax_pc_membership_login', array( __CLASS__, 'ajax_login' ) );
add_action( 'wp_ajax_nopriv_pc_membership_login', array( __CLASS__, 'ajax_login' ) );
add_action( 'wp_ajax_pc_membership_register', array( __CLASS__, 'ajax_register' ) );
add_action( 'wp_ajax_nopriv_pc_membership_register', array( __CLASS__, 'ajax_register' ) );
add_action( 'wp_ajax_pc_membership_update_profile', array( __CLASS__, 'ajax_update_profile' ) );
add_action( 'template_redirect', array( __CLASS__, 'handle_stripe_return' ) );
}
public static function enqueue_assets() {
if ( ! self::is_membership_page() ) {
return;
}
wp_enqueue_style( 'pc-membership-public-style', PC_MEMBERSHIP_PLUGIN_URL . 'public/css/public-style.css', array(), PC_MEMBERSHIP_VERSION );
wp_enqueue_script( 'pc-membership-stripe-js', 'https://js.stripe.com/v3/', array(), null, true );
wp_enqueue_script( 'pc-membership-public-script', PC_MEMBERSHIP_PLUGIN_URL . 'public/js/public-script.js', array( 'jquery', 'pc-membership-stripe-js' ), PC_MEMBERSHIP_VERSION, true );
$options = get_option( 'pc_membership_options' );
$publishable_key = '';
if ( ! empty( $options ) ) {
$mode = isset( $options['mode'] ) ? $options['mode'] : 'test';
$publishable_key = isset( $options[ $mode . '_publishable_key' ] ) ? $options[ $mode . '_publishable_key' ] : '';
}
wp_localize_script( 'pc-membership-public-script', 'pcMembership', array(
'ajax_url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'pc_membership_nonce' ),
'stripe_key' => $publishable_key,
'i18n' => array(
'processing' => __( 'Processing...', 'pc-membership-abc123' ),
'selectPlan' => __( 'Please select a plan', 'pc-membership-abc123' ),
'error' => __( 'An error occurred. Please try again.', 'pc-membership-abc123' ),
'success' => __( 'Success!', 'pc-membership-abc123' ),
'cancel' => __( 'Cancel Subscription', 'pc-membership-abc123' ),
'confirmCancel'=> __( 'Are you sure you want to cancel your subscription?', 'pc-membership-abc123' ),
),
) );
}
private static function is_membership_page() {
if ( ! is_singular() && ! is_page() ) {
return false;
}
$options = get_option( 'pc_membership_options', array() );
$page_ids = array(
isset( $options['checkout_page_id'] ) ? $options['checkout_page_id'] : 0,
isset( $options['login_page_id'] ) ? $options['login_page_id'] : 0,
isset( $options['register_page_id'] ) ? $options['register_page_id'] : 0,
isset( $options['account_page_id'] ) ? $options['account_page_id'] : 0,
isset( $options['success_page_id'] ) ? $options['success_page_id'] : 0,
isset( $options['cancel_page_id'] ) ? $options['cancel_page_id'] : 0,
);
return in_array( get_the_ID(), array_filter( $page_ids ) );
}
public static function checkout_shortcode( $atts ) {
ob_start();
self::render_checkout_page();
return ob_get_clean();
}
public static function render_checkout_page() {
global $wpdb;
$plans = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}pc_membership_plans ORDER BY price ASC" );
if ( empty( $plans ) ) {
echo '<div class="pc-membership-message pc-membership-message-info">';
esc_html_e( 'No membership plans available at this time.', 'pc-membership-abc123' );
echo '</div>';
return;
}
$options = get_option( 'pc_membership_options' );
$mode = isset( $options['mode'] ) ? $options['mode'] : 'test';
echo '<div class="pc-membership-checkout-wrapper">';
echo '<div class="pc-membership-pricing-cards">';
foreach ( $plans as $plan ) {
$benefits = array_filter( array_map( 'trim', explode( "\n", $plan->benefits ) ) );
$billing_label = $plan->is_subscription
? sprintf( '%s / %s', pc_membership_format_price( $plan->price ), $plan->billing_interval )
: pc_membership_format_price( $plan->price );
echo '<div class="pc-membership-pricing-card" data-plan-id="' . esc_attr( $plan->id ) . '">';
echo '<div class="pc-membership-pricing-header">';
echo '<h3 class="pc-membership-plan-name">' . esc_html( $plan->name ) . '</h3>';
echo '<div class="pc-membership-plan-price">';
echo '<span class="pc-membership-price-amount">' . esc_html( pc_membership_format_price( $plan->price ) ) . '</span>';
if ( $plan->is_subscription ) {
echo '<span class="pc-membership-price-interval"> / ' . esc_html( $plan->billing_interval ) . '</span>';
}
echo '</div>';
if ( $plan->trial_days > 0 ) {
echo '<div class="pc-membership-trial-badge">';
printf( esc_html__( '%d Day Free Trial', 'pc-membership-abc123' ), $plan->trial_days );
echo '</div>';
}
echo '</div>';
if ( $plan->description ) {
echo '<div class="pc-membership-plan-description">' . wp_kses_post( $plan->description ) . '</div>';
}
if ( ! empty( $benefits ) ) {
echo '<ul class="pc-membership-plan-benefits">';
foreach ( $benefits as $benefit ) {
echo '<li><span class="dashicons dashicons-yes-alt"></span>' . esc_html( $benefit ) . '</li>';
}
echo '</ul>';
}
echo '<button class="pc-membership-select-plan-btn button button-primary" data-plan-id="' . esc_attr( $plan->id ) . '">';
esc_html_e( 'Select Plan', 'pc-membership-abc123' );
echo '</button>';
echo '</div>';
}
echo '</div>';
echo '<div class="pc-membership-checkout-form-wrapper" style="display: none;">';
echo '<div id="pc-membership-checkout-form-container"></div>';
echo '</div>';
echo '</div>';
}
public static function login_shortcode( $atts ) {
if ( is_user_logged_in() ) {
$account_page = self::get_page_url( 'account' );
if ( $account_page ) {
wp_redirect( $account_page );
exit;
}
}
ob_start();
self::render_login_form();
return ob_get_clean();
}
public static function render_login_form() {
$options = get_option( 'pc_membership_options' );
$register_page = isset( $options['register_page_id'] ) ? get_permalink( $options['register_page_id'] ) : false;
$account_page = self::get_page_url( 'account' );
?>
<div class="pc-membership-login-wrapper">
<div class="pc-membership-login-form-container">
<h2><?php esc_html_e( 'Member Login', 'pc-membership-abc123' ); ?></h2>
<form id="pc-membership-login-form" method="post">
<?php wp_nonce_field( 'pc_membership_login', 'pc_membership_login_nonce' ); ?>
<div class="pc-membership-form-group">
<label for="user_login"><?php esc_html_e( 'Username or Email', 'pc-membership-abc123' ); ?></label>
<input type="text" name="user_login" id="user_login" required class="regular-text">
</div>
<div class="pc-membership-form-group">
<label for="user_password"><?php esc_html_e( 'Password', 'pc-membership-abc123' ); ?></label>
<input type="password" name="user_password" id="user_password" required class="regular-text">
</div>
<div class="pc-membership-form-group">
<label class="pc-membership-remember-me">
<input type="checkbox" name="rememberme" value="forever">
<?php esc_html_e( 'Remember me', 'pc-membership-abc123' ); ?>
</label>
</div>
<button type="submit" class="button button-primary pc-membership-login-btn">
<?php esc_html_e( 'Log In', 'pc-membership-abc123' ); ?>
</button>
<div class="pc-membership-login-links">
<?php if ( $register_page ) : ?>
<a href="<?php echo esc_url( $register_page ); ?>">
<?php esc_html_e( 'Create an account', 'pc-membership-abc123' ); ?>
</a>
<?php endif; ?>
<a href="<?php echo esc_url( wp_lostpassword_url() ); ?>">
<?php esc_html_e( 'Forgot password?', 'pc-membership-abc123' ); ?>
</a>
</div>
</form>
<div id="pc-membership-login-message"></div>
</div>
</div>
<?php
}
public static function register_shortcode( $atts ) {
if ( is_user_logged_in() ) {
$account_page = self::get_page_url( 'account' );
if ( $account_page ) {
wp_redirect( $account_page );
exit;
}
}
ob_start();
self::render_registration_form();
return ob_get_clean();
}
public static function render_registration_form() {
$options = get_option( 'pc_membership_options' );
$login_page = isset( $options['login_page_id'] ) ? get_permalink( $options['login_page_id'] ) : false;
?>
<div class="pc-membership-register-wrapper">
<div class="pc-membership-register-form-container">
<h2><?php esc_html_e( 'Create Account', 'pc-membership-abc123' ); ?></h2>
<form id="pc-membership-register-form" method="post">
<?php wp_nonce_field( 'pc_membership_register', 'pc_membership_register_nonce' ); ?>
<div class="pc-membership-form-group">
<label for="user_login"><?php esc_html_e( 'Username', 'pc-membership-abc123' ); ?> *</label>
<input type="text" name="user_login" id="user_login" required class="regular-text" minlength="4">
</div>
<div class="pc-membership-form-group">
<label for="user_email"><?php esc_html_e( 'Email', 'pc-membership-abc123' ); ?> *</label>
<input type="email" name="user_email" id="user_email" required class="regular-text">
</div>
<div class="pc-membership-form-group">
<label for="user_password"><?php esc_html_e( 'Password', 'pc-membership-abc123' ); ?> *</label>
<input type="password" name="user_password" id="user_password" required class="regular-text" minlength="8">
</div>
<div class="pc-membership-form-group">
<label for="user_password_confirm"><?php esc_html_e( 'Confirm Password', 'pc-membership-abc123' ); ?> *</label>
<input type="password" name="user_password_confirm" id="user_password_confirm" required class="regular-text">
</div>
<button type="submit" class="button button-primary pc-membership-register-btn">
<?php esc_html_e( 'Create Account', 'pc-membership-abc123' ); ?>
</button>
<div class="pc-membership-login-links">
<?php if ( $login_page ) : ?>
<a href="<?php echo esc_url( $login_page ); ?>">
<?php esc_html_e( 'Already have an account? Log in', 'pc-membership-abc123' ); ?>
</a>
<?php endif; ?>
</div>
</form>
<div id="pc-membership-register-message"></div>
</div>
</div>
<?php
}
public static function account_shortcode( $atts ) {
if ( ! is_user_logged_in() ) {
$login_page = self::get_page_url( 'login' );
if ( $login_page ) {
wp_redirect( $login_page );
exit;
}
}
ob_start();
self::render_account_page();
return ob_get_clean();
}
public static function render_account_page() {
$user = wp_get_current_user();
$subscription = self::get_user_subscription( $user->ID );
?>
<div class="pc-membership-account-wrapper">
<div class="pc-membership-account-header">
<h2><?php printf( esc_html__( 'Welcome, %s', 'pc-membership-abc123' ), esc_html( $user->display_name ) ); ?></h2>
<a href="<?php echo esc_url( wp_logout_url( self::get_page_url( 'login' ) ) ); ?>" class="button">
<?php esc_html_e( 'Log Out', 'pc-membership-abc123' ); ?>
</a>
</div>
<?php if ( $subscription ) : ?>
<?php
global $wpdb;
$plan = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}pc_membership_plans WHERE id = %d", $subscription->plan_id ) );
?>
<div class="pc-membership-subscription-details">
<h3><?php esc_html_e( 'Your Subscription', 'pc-membership-abc123' ); ?></h3>
<div class="pc-membership-subscription-info">
<div class="pc-membership-subscription-row">
<span class="pc-membership-label"><?php esc_html_e( 'Plan', 'pc-membership-abc123' ); ?>:</span>
<span class="pc-membership-value"><?php echo $plan ? esc_html( $plan->name ) : esc_html__( 'Unknown', 'pc-membership-abc123' ); ?></span>
</div>
<div class="pc-membership-subscription-row">
<span class="pc-membership-label"><?php esc_html_e( 'Status', 'pc-membership-abc123' ); ?>:</span>
<span class="pc-membership-value pc-membership-status-<?php echo esc_attr( $subscription->status ); ?>">
<?php echo esc_html( ucfirst( $subscription->status ) ); ?>
</span>
</div>
<div class="pc-membership-subscription-row">
<span class="pc-membership-label"><?php esc_html_e( 'Started', 'pc-membership-abc123' ); ?>:</span>
<span class="pc-membership-value"><?php echo esc_html( date_i18n( get_option( 'date_format' ), strtotime( $subscription->started_at ) ) ); ?></span>
</div>
<?php if ( $subscription->expires_at ) : ?>
<div class="pc-membership-subscription-row">
<span class="pc-membership-label"><?php esc_html_e( 'Expires', 'pc-membership-abc123' ); ?>:</span>
<span class="pc-membership-value"><?php echo esc_html( date_i18n( get_option( 'date_format' ), strtotime( $subscription->expires_at ) ) ); ?></span>
</div>
<?php endif; ?>
</div>
<?php if ( $subscription->status === 'active' && $plan && $plan->is_subscription ) : ?>
<div class="pc-membership-subscription-actions">
<button type="button" id="pc-membership-cancel-subscription" class="button button-secondary" data-subscription-id="<?php echo esc_attr( $subscription->id ); ?>">
<?php esc_html_e( 'Cancel Subscription', 'pc-membership-abc123' ); ?>
</button>
</div>
<?php endif; ?>
</div>
<?php else : ?>
<div class="pc-membership-no-subscription">
<p><?php esc_html_e( 'You don\'t have an active membership subscription.', 'pc-membership-abc123' ); ?></p>
<a href="<?php echo esc_url( self::get_page_url( 'checkout' ) ); ?>" class="button button-primary">
<?php esc_html_e( 'Choose a Plan', 'pc-membership-abc123' ); ?>
</a>
</div>
<?php endif; ?>
<div class="pc-membership-account-section">
<h3><?php esc_html_e( 'Profile Information', 'pc-membership-abc123' ); ?></h3>
<form id="pc-membership-profile-form" method="post">
<?php wp_nonce_field( 'pc_membership_update_profile', 'pc_membership_profile_nonce' ); ?>
<div class="pc-membership-form-group">
<label for="display_name"><?php esc_html_e( 'Display Name', 'pc-membership-abc123' ); ?></label>
<input type="text" name="display_name" id="display_name" value="<?php echo esc_attr( $user->display_name ); ?>" class="regular-text">
</div>
<div class="pc-membership-form-group">
<label for="user_email"><?php esc_html_e( 'Email', 'pc-membership-abc123' ); ?></label>
<input type="email" name="user_email" id="user_email" value="<?php echo esc_attr( $user->user_email ); ?>" class="regular-text" readonly>
<p class="description"><?php esc_html_e( 'Email cannot be changed.', 'pc-membership-abc123' ); ?></p>
</div>
<button type="submit" class="button button-primary">
<?php esc_html_e( 'Update Profile', 'pc-membership-abc123' ); ?>
</button>
</form>
</div>
</div>
<?php
}
public static function success_shortcode( $atts ) {
ob_start();
?>
<div class="pc-membership-success-wrapper">
<div class="pc-membership-success-message">
<div class="pc-membership-success-icon">
<span class="dashicons dashicons-yes-alt" style="font-size: 64px; height: 64px; width: 64px; color: #46b450;"></span>
</div>
<h2><?php esc_html_e( 'Payment Successful!', 'pc-membership-abc123' ); ?></h2>
<p><?php esc_html_e( 'Thank you for your purchase. Your membership is now active.', 'pc-membership-abc123' ); ?></p>
<a href="<?php echo esc_url( self::get_page_url( 'account' ) ); ?>" class="button button-primary">
<?php esc_html_e( 'Go to My Account', 'pc-membership-abc123' ); ?>
</a>
</div>
</div>
<?php
return ob_get_clean();
}
public static function cancel_shortcode( $atts ) {
ob_start();
?>
<div class="pc-membership-cancel-wrapper">
<div class="pc-membership-cancel-message">
<div class="pc-membership-cancel-icon">
<span class="dashicons dashicons-dismiss" style="font-size: 64px; height: 64px; width: 64px; color: #dc3232;"></span>
</div>
<h2><?php esc_html_e( 'Payment Cancelled', 'pc-membership-abc123' ); ?></h2>
<p><?php esc_html_e( 'Your payment was cancelled. No charges were made.', 'pc-membership-abc123' ); ?></p>
<a href="<?php echo esc_url( self::get_page_url( 'checkout' ) ); ?>" class="button button-primary">
<?php esc_html_e( 'Try Again', 'pc-membership-abc123' ); ?>
</a>
</div>
</div>
<?php
return ob_get_clean();
}
public static function pricing_shortcode( $atts ) {
ob_start();
self::render_checkout_page();
return ob_get_clean();
}
private static function get_page_url( $page_type ) {
$options = get_option( 'pc_membership_options' );
$page_id = isset( $options[ $page_type . '_page_id' ] ) ? absint( $options[ $page_type . '_page_id' ] ) : 0;
return $page_id ? get_permalink( $page_id ) : home_url();
}
private static function get_user_subscription( $user_id ) {
global $wpdb;
return $wpdb->get_row( $wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}pc_membership_subscriptions WHERE user_id = %d AND status = 'active' ORDER BY id DESC LIMIT 1",
$user_id
) );
}
public static function ajax_create_checkout() {
check_ajax_referer( 'pc_membership_nonce', 'nonce' );
$plan_id = isset( $_POST['plan_id'] ) ? absint( $_POST['plan_id'] ) : 0;
if ( ! $plan_id ) {
wp_send_json_error( __( 'Invalid plan', 'pc-membership-abc123' ) );
}
if ( ! class_exists( 'PC_Membership_Stripe' ) ) {
require_once PC_MEMBERSHIP_PLUGIN_DIR . 'includes/stripe-handler.php';
}
$result = PC_Membership_Stripe::create_checkout_session( $plan_id );
if ( is_wp_error( $result ) ) {
wp_send_json_error( $result->get_error_message() );
}
wp_send_json_success( array( 'url' => $result->url ) );
}
public static function ajax_cancel_subscription() {
check_ajax_referer( 'pc_membership_nonce', 'nonce' );
if ( ! is_user_logged_in() ) {
wp_send_json_error( __( 'Must be logged in', 'pc-membership-abc123' ) );
}
$subscription_id = isset( $_POST['subscription_id'] ) ? absint( $_POST['subscription_id'] ) : 0;
if ( ! $subscription_id ) {
wp_send_json_error( __( 'Invalid subscription', 'pc-membership-abc123' ) );
}
global $wpdb;
$subscription = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}pc_membership_subscriptions WHERE id = %d AND user_id = %d", $subscription_id, get_current_user_id() ) );
if ( ! $subscription ) {
wp_send_json_error( __( 'Subscription not found', 'pc-membership-abc123' ) );
}
if ( ! class_exists( 'PC_Membership_Stripe' ) ) {
require_once PC_MEMBERSHIP_PLUGIN_DIR . 'includes/stripe-handler.php';
}
$result = PC_Membership_Stripe::cancel_subscription( $subscription->stripe_subscription_id );
if ( is_wp_error( $result ) ) {
wp_send_json_error( $result->get_error_message() );
}
$wpdb->update( $wpdb->prefix . 'pc_membership_subscriptions', array( 'status' => 'cancelled' ), array( 'id' => $subscription_id ) );
wp_send_json_success();
}
public static function ajax_update_payment_method() {
check_ajax_referer( 'pc_membership_nonce', 'nonce' );
if ( ! is_user_logged_in() ) {
wp_send_json_error( __( 'Must be logged in', 'pc-membership-abc123' ) );
}
if ( ! class_exists( 'PC_Membership_Stripe' ) ) {
require_once PC_MEMBERSHIP_PLUGIN_DIR . 'includes/stripe-handler.php';
}
$result = PC_Membership_Stripe::create_portal_session();
if ( is_wp_error( $result ) ) {
wp_send_json_error( $result->get_error_message() );
}
wp_send_json_success( array( 'url' => $result->url ) );
}
public static function ajax_login() {
check_ajax_referer( 'pc_membership_login', 'nonce' );
$user_login = isset( $_POST['user_login'] ) ? sanitize_text_field( wp_unslash( $_POST['user_login'] ) ) : '';
$user_password = isset( $_POST['user_password'] ) ? $_POST['user_password'] : '';
if ( empty( $user_login ) || empty( $user_password ) ) {
wp_send_json_error( __( 'Please enter username and password', 'pc-membership-abc123' ) );
}
$user = wp_authenticate_username_password( null, $user_login, $user_password );
if ( is_wp_error( $user ) ) {
wp_send_json_error( $user->get_error_message() );
}
wp_set_auth_cookie( $user->ID, true );
$redirect_url = self::get_page_url( 'account' );
wp_send_json_success( array( 'redirect' => $redirect_url ) );
}
public static function ajax_register() {
check_ajax_referer( 'pc_membership_register', 'nonce' );
$user_login = isset( $_POST['user_login'] ) ? sanitize_text_field( wp_unslash( $_POST['user_login'] ) ) : '';
$user_email = isset( $_POST['user_email'] ) ? sanitize_email( wp_unslash( $_POST['user_email'] ) ) : '';
$user_password = isset( $_POST['user_password'] ) ? $_POST['user_password'] : '';
$user_password_confirm = isset( $_POST['user_password_confirm'] ) ? $_POST['user_password_confirm'] : '';
if ( empty( $user_login ) || empty( $user_email ) || empty( $user_password ) ) {
wp_send_json_error( __( 'All fields are required', 'pc-membership-abc123' ) );
}
if ( ! is_email( $user_email ) ) {
wp_send_json_error( __( 'Invalid email address', 'pc-membership-abc123' ) );
}
if ( $user_password !== $user_password_confirm ) {
wp_send_json_error( __( 'Passwords do not match', 'pc-membership-abc123' ) );
}
if ( strlen( $user_password ) < 8 ) {
wp_send_json_error( __( 'Password must be at least 8 characters', 'pc-membership-abc123' ) );
}
if ( username_exists( $user_login ) ) {
wp_send_json_error( __( 'Username already exists', 'pc-membership-abc123' ) );
}
if ( email_exists( $user_email ) ) {
wp_send_json_error( __( 'Email already registered', 'pc-membership-abc123' ) );
}
$user_id = wp_create_user( $user_login, $user_password, $user_email );
if ( is_wp_error( $user_id ) ) {
wp_send_json_error( $user_id->get_error_message() );
}
wp_update_user( array(
'ID' => $user_id,
'display_name' => $user_login,
) );
$user = get_userdata( $user_id );
$user->set_role( 'subscriber' );
wp_set_auth_cookie( $user_id, true );
do_action( 'pc_membership_user_registered', $user_id, $user_login, $user_email );
$redirect_url = self::get_page_url( 'account' );
wp_send_json_success( array( 'redirect' => $redirect_url ) );
}
public static function ajax_update_profile() {
check_ajax_referer( 'pc_membership_update_profile', 'nonce' );
if ( ! is_user_logged_in() ) {
wp_send_json_error( __( 'Must be logged in', 'pc-membership-abc123' ) );
}
$display_name = isset( $_POST['display_name'] ) ? sanitize_text_field( wp_unslash( $_POST['display_name'] ) ) : '';
if ( empty( $display_name ) ) {
wp_send_json_error( __( 'Display name is required', 'pc-membership-abc123' ) );
}
$result = wp_update_user( array(
'ID' => get_current_user_id(),
'display_name' => $display_name,
) );
if ( is_wp_error( $result ) ) {
wp_send_json_error( $result->get_error_message() );
}
wp_send_json_success();
}
public static function handle_stripe_return() {
if ( ! isset( $_GET['session_id'] ) ) {
return;
}
$session_id = sanitize_text_field( wp_unslash( $_GET['session_id'] ) );
if ( ! class_exists( 'PC_Membership_Stripe' ) ) {
require_once PC_MEMBERSHIP_PLUGIN_DIR . 'includes/stripe-handler.php';
}
$session = PC_Membership_Stripe::retrieve_session( $session_id );
if ( is_wp_error( $session ) ) {
return;
}
$success_page = self::get_page_url( 'success' );
if ( $success_page ) {
wp_redirect( $success_page );
exit;
}
}
}

View File

@@ -0,0 +1,19 @@
<?php
/**
* Uninstall script for PC Membership Plugin.
* Removes custom tables created by the plugin.
*/
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit; // Prevent direct access.
}
global $wpdb;
$tables = array(
$wpdb->prefix . 'pc_membership_plans',
$wpdb->prefix . 'pc_membership_subscriptions',
);
foreach ( $tables as $table ) {
$wpdb->query( "DROP TABLE IF EXISTS `$table`" );
}
// Option cleanup.
delete_option( 'pc_membership_options' );
?>

View File

@@ -0,0 +1,23 @@
[
{
"id": "woocommerce-seo-booster",
"name": "WooCommerce SEO Booster",
"description": "A comprehensive starting point for WooCommerce SEO plugins. Includes meta tags, schema markup, and sitemap generation scaffolding.",
"image": "/assets/templates/seo-booster.jpg",
"category": "E-commerce"
},
{
"id": "elementor-addon-starter",
"name": "Elementor Addon Starter",
"description": "Boilerplate for creating custom Elementor widgets. Comes with a basic hello-world widget and controls setup.",
"image": "/assets/templates/elementor-starter.jpg",
"category": "Page Builder"
},
{
"id": "admin-dashboard-widget",
"name": "Admin Dashboard Widget",
"description": "Add custom widgets to the WordPress admin dashboard. Useful for client reports or quick execute actions.",
"image": "/assets/templates/admin-widget.jpg",
"category": "Admin"
}
]