chore: complete M1.1 runtime QA and release packaging

This commit is contained in:
XShop
2026-09-13 19:04:41 +03:30
commit f11e1d650d
66 changed files with 3094 additions and 0 deletions
+74
View File
@@ -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,
]);
}