chore: complete M1.1 runtime QA and release packaging
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace XShop\Core;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
final class Plugin {
|
||||
|
||||
private static ?self $instance = null;
|
||||
|
||||
public static function instance(): self {
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
// Core modules are loaded lazily; v1 wiring is scaffolded.
|
||||
// Each module file guards with `class_exists` checks.
|
||||
$this->loadModules();
|
||||
add_action('init', [$this, 'registerCpts']);
|
||||
add_action('admin_menu', [$this, 'registerAdminMenu']);
|
||||
}
|
||||
|
||||
private function loadModules(): void {
|
||||
// Autoload-free includes; keep explicit for reviewability.
|
||||
$mods = [
|
||||
'Modules/Banners/CPT.php',
|
||||
'Modules/QA/CPT.php',
|
||||
];
|
||||
foreach ($mods as $rel) {
|
||||
$path = XSHOP_CORE_DIR . '/includes/' . $rel;
|
||||
if (file_exists($path)) {
|
||||
require_once $path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function registerCpts(): void {
|
||||
do_action('xshop_core_register_cpts');
|
||||
}
|
||||
|
||||
public function registerAdminMenu(): void {
|
||||
if (!current_user_can('manage_options')) { return; }
|
||||
add_menu_page(
|
||||
esc_html__('XShop', 'xshop'),
|
||||
esc_html__('XShop', 'xshop'),
|
||||
'manage_options',
|
||||
'xshop',
|
||||
[$this, 'renderDashboard'],
|
||||
'dashicons-store',
|
||||
58
|
||||
);
|
||||
// Submenus added by modules via `xshop_core_admin_submenus` hook in M4.
|
||||
}
|
||||
|
||||
public function renderDashboard(): void {
|
||||
if (!current_user_can('manage_options')) { return; }
|
||||
echo '<div class="wrap"><h1>' . esc_html__('XShop — Dashboard', 'xshop') . '</h1>';
|
||||
echo '<p>' . esc_html__('Welcome to XShop. Use the sidebar to manage theme features.', 'xshop') . '</p></div>';
|
||||
}
|
||||
|
||||
public static function activate(): void {
|
||||
// Flush rewrites for CPTs.
|
||||
do_action('xshop_core_register_cpts');
|
||||
flush_rewrite_rules();
|
||||
}
|
||||
|
||||
public static function deactivate(): void {
|
||||
flush_rewrite_rules();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace XShop\Core\Modules\Banners;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
add_action('xshop_core_register_cpts', __NAMESPACE__ . '\\register_cpt');
|
||||
add_action('add_meta_boxes', __NAMESPACE__ . '\\add_metabox');
|
||||
add_action('save_post_xshop_banner', __NAMESPACE__ . '\\save_post', 10, 2);
|
||||
|
||||
function register_cpt(): void {
|
||||
register_post_type('xshop_banner', [
|
||||
'label' => esc_html__('Banners', 'xshop'),
|
||||
'labels' => [
|
||||
'name' => esc_html__('Banners', 'xshop'),
|
||||
'singular_name' => esc_html__('Banner', 'xshop'),
|
||||
'add_new_item' => esc_html__('Add New Banner', 'xshop'),
|
||||
],
|
||||
'public' => false,
|
||||
'show_ui' => true,
|
||||
'show_in_menu' => 'xshop',
|
||||
'show_in_rest' => false,
|
||||
'supports' => ['title'],
|
||||
'capability_type' => 'post',
|
||||
'map_meta_cap' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
function add_metabox(): void {
|
||||
add_meta_box('xshop_banner_meta', esc_html__('Banner Details', 'xshop'), __NAMESPACE__ . '\\render_metabox', 'xshop_banner', 'normal', 'high');
|
||||
}
|
||||
|
||||
function render_metabox(\WP_Post $post): void {
|
||||
if (!current_user_can('edit_post', $post->ID)) { return; }
|
||||
wp_nonce_field('xshop_banner_save', 'xshop_banner_nonce');
|
||||
$image = (int) get_post_meta($post->ID, '_xshop_banner_image', true);
|
||||
$mobileImage = (int) get_post_meta($post->ID, '_xshop_banner_mobile_image', true);
|
||||
$link = (string) get_post_meta($post->ID, '_xshop_banner_link', true);
|
||||
$cta = (string) get_post_meta($post->ID, '_xshop_banner_cta', true);
|
||||
$position = (string) get_post_meta($post->ID, '_xshop_banner_position', true);
|
||||
$active = get_post_meta($post->ID, '_xshop_banner_active', true);
|
||||
$active = $active === '' ? '1' : $active;
|
||||
?>
|
||||
<p>
|
||||
<label for="xshop_banner_position"><?php esc_html_e('Position', 'xshop'); ?></label><br>
|
||||
<select id="xshop_banner_position" name="xshop_banner_position">
|
||||
<?php foreach (['home_hero','home_strip','shop_top','product_sidebar','footer_top'] as $opt): ?>
|
||||
<option value="<?php echo esc_attr($opt); ?>" <?php selected($position ?: 'home_hero', $opt); ?>><?php echo esc_html($opt); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</p>
|
||||
<p>
|
||||
<label for="xshop_banner_link"><?php esc_html_e('Link URL', 'xshop'); ?></label><br>
|
||||
<input type="url" id="xshop_banner_link" name="xshop_banner_link" value="<?php echo esc_attr($link); ?>" class="large-text" />
|
||||
</p>
|
||||
<p>
|
||||
<label for="xshop_banner_cta"><?php esc_html_e('CTA Text', 'xshop'); ?></label><br>
|
||||
<input type="text" id="xshop_banner_cta" name="xshop_banner_cta" value="<?php echo esc_attr($cta); ?>" class="large-text" />
|
||||
</p>
|
||||
<p>
|
||||
<label><?php esc_html_e('Image ID', 'xshop'); ?></label><br>
|
||||
<input type="number" name="xshop_banner_image" value="<?php echo esc_attr((string) $image); ?>" min="0" />
|
||||
<span class="description"><?php esc_html_e('Use Media Library attachment ID.', 'xshop'); ?></span>
|
||||
</p>
|
||||
<p>
|
||||
<label><?php esc_html_e('Mobile Image ID (optional)', 'xshop'); ?></label><br>
|
||||
<input type="number" name="xshop_banner_mobile_image" value="<?php echo esc_attr((string) $mobileImage); ?>" min="0" />
|
||||
</p>
|
||||
<p>
|
||||
<label><input type="checkbox" name="xshop_banner_active" value="1" <?php checked($active, '1'); ?> /> <?php esc_html_e('Active', 'xshop'); ?></label>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
function save_post(int $postId, \WP_Post $post): void {
|
||||
if (!isset($_POST['xshop_banner_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['xshop_banner_nonce'])), 'xshop_banner_save')) { return; }
|
||||
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { return; }
|
||||
if (!current_user_can('edit_post', $postId)) { return; }
|
||||
|
||||
$position = isset($_POST['xshop_banner_position']) ? sanitize_key(wp_unslash($_POST['xshop_banner_position'])) : 'home_hero';
|
||||
$allowed = ['home_hero','home_strip','shop_top','product_sidebar','footer_top'];
|
||||
if (!in_array($position, $allowed, true)) { $position = 'home_hero'; }
|
||||
update_post_meta($postId, '_xshop_banner_position', $position);
|
||||
update_post_meta($postId, '_xshop_banner_link', isset($_POST['xshop_banner_link']) ? esc_url_raw(wp_unslash($_POST['xshop_banner_link'])) : '');
|
||||
update_post_meta($postId, '_xshop_banner_cta', isset($_POST['xshop_banner_cta']) ? sanitize_text_field(wp_unslash($_POST['xshop_banner_cta'])) : '');
|
||||
update_post_meta($postId, '_xshop_banner_image', isset($_POST['xshop_banner_image']) ? absint($_POST['xshop_banner_image']) : 0);
|
||||
update_post_meta($postId, '_xshop_banner_mobile_image', isset($_POST['xshop_banner_mobile_image']) ? absint($_POST['xshop_banner_mobile_image']) : 0);
|
||||
update_post_meta($postId, '_xshop_banner_active', isset($_POST['xshop_banner_active']) ? '1' : '0');
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace XShop\Core\Modules\QA;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
add_action('xshop_core_register_cpts', __NAMESPACE__ . '\\register_cpt');
|
||||
|
||||
function register_cpt(): void {
|
||||
register_post_type('xshop_qa', [
|
||||
'label' => esc_html__('Questions & Answers', 'xshop'),
|
||||
'labels' => [
|
||||
'name' => esc_html__('Questions & Answers', 'xshop'),
|
||||
'singular_name' => esc_html__('Question', 'xshop'),
|
||||
],
|
||||
'public' => false,
|
||||
'show_ui' => true,
|
||||
'show_in_menu' => 'xshop',
|
||||
'supports' => ['title', 'editor', 'author'],
|
||||
'capability_type' => 'post',
|
||||
'map_meta_cap' => true,
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: XShop Core
|
||||
* Plugin URI: https://example.com/xshop-core
|
||||
* Description: Companion plugin for XShop theme – wishlist, compare, Q&A, AJAX search/filter, banners, demo import, and more. Requires XShop theme.
|
||||
* Version: 1.0.0
|
||||
* Author: XShop Team
|
||||
* Author URI: https://example.com
|
||||
* Text Domain: xshop
|
||||
* Domain Path: /languages
|
||||
* Requires at least: 6.4
|
||||
* Requires PHP: 8.2
|
||||
* License: Commercial
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
define('XSHOP_CORE_VERSION', '1.0.0');
|
||||
define('XSHOP_CORE_FILE', __FILE__);
|
||||
define('XSHOP_CORE_DIR', __DIR__);
|
||||
define('XSHOP_CORE_URI', plugin_dir_url(__FILE__));
|
||||
|
||||
require_once XSHOP_CORE_DIR . '/includes/Core/Plugin.php';
|
||||
|
||||
register_activation_hook(__FILE__, ['XShop\Core\Plugin', 'activate']);
|
||||
register_deactivation_hook(__FILE__, ['XShop\Core\Plugin', 'deactivate']);
|
||||
|
||||
add_action('plugins_loaded', function (): void {
|
||||
load_plugin_textdomain('xshop', false, dirname(plugin_basename(__FILE__)) . '/languages');
|
||||
XShop\Core\Plugin::instance()->init();
|
||||
});
|
||||
Reference in New Issue
Block a user