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,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();
}