feat: implement WooCommerce storefront foundation
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
/**
|
||||
* Mini Cart — foundation (item count, items, subtotal, links, empty).
|
||||
*
|
||||
* @package XShop
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
if (!function_exists('WC') || !WC()->cart) {
|
||||
return;
|
||||
}
|
||||
$cart = WC()->cart;
|
||||
$count = $cart->get_cart_contents_count();
|
||||
$items = $cart->get_cart();
|
||||
?>
|
||||
<div class="xshop-mini-cart" data-xshop-mini-cart>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-block-end:var(--xshop-space-3)">
|
||||
<strong><?php esc_html_e('Cart', 'xshop'); ?></strong>
|
||||
<span class="xshop-mini-cart__count" aria-label="<?php echo esc_attr(sprintf(esc_html__('%d items', 'xshop'), $count)); ?>"><?php echo esc_html((string) $count); ?></span>
|
||||
</div>
|
||||
|
||||
<?php if (empty($items)): ?>
|
||||
<p class="xshop-mini-cart__empty"><?php esc_html_e('Your cart is empty.', 'xshop'); ?></p>
|
||||
<a href="<?php echo esc_url(wc_get_page_permalink('shop')); ?>" class="xshop-btn xshop-btn--primary" style="inline-size:100%"><?php esc_html_e('Browse shop', 'xshop'); ?></a>
|
||||
<?php else: ?>
|
||||
<ul class="xshop-mini-cart__list">
|
||||
<?php foreach ($items as $key => $item):
|
||||
$product = $item['data'] ?? null;
|
||||
if (!$product instanceof WC_Product) { continue; }
|
||||
$name = $product->get_name();
|
||||
$qty = (int) $item['quantity'];
|
||||
?>
|
||||
<li class="xshop-mini-cart__item">
|
||||
<a href="<?php echo esc_url(get_permalink($product->get_id())); ?>">
|
||||
<?php echo wp_get_attachment_image($product->get_image_id() ?: 0, 'thumbnail', false, ['alt' => esc_attr($name), 'loading' => 'lazy']); ?>
|
||||
</a>
|
||||
<span>
|
||||
<a href="<?php echo esc_url(get_permalink($product->get_id())); ?>" style="color:var(--xshop-text);font-size:var(--xshop-text-sm)"><?php echo esc_html($name); ?></a>
|
||||
<span style="display:block;color:var(--xshop-muted);font-size:var(--xshop-text-xs)"><?php echo esc_html(sprintf(esc_html__('%d × %s', 'xshop'), $qty, wp_strip_all_tags($product->get_price_html()))); ?></span>
|
||||
</span>
|
||||
<a href="<?php echo esc_url(wc_get_cart_remove_url($key)); ?>" aria-label="<?php esc_attr_e('Remove item', 'xshop'); ?>" style="color:var(--xshop-danger)">×</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<div style="border-block-start:1px solid var(--xshop-border);padding-block-start:var(--xshop-space-3);margin-block-start:var(--xshop-space-3);display:flex;justify-content:space-between;font-weight:var(--xshop-weight-bold)">
|
||||
<span><?php esc_html_e('Subtotal', 'xshop'); ?></span>
|
||||
<span><?php echo wp_kses_post($cart->get_cart_subtotal()); ?></span>
|
||||
</div>
|
||||
<div style="display:flex;gap:var(--xshop-space-2);margin-block-start:var(--xshop-space-3)">
|
||||
<a href="<?php echo esc_url(wc_get_cart_url()); ?>" class="xshop-btn xshop-btn--secondary" style="flex:1"><?php esc_html_e('View cart', 'xshop'); ?></a>
|
||||
<a href="<?php echo esc_url(wc_get_checkout_url()); ?>" class="xshop-btn xshop-btn--primary" style="flex:1"><?php esc_html_e('Checkout', 'xshop'); ?></a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
/**
|
||||
* XShop Product Card — reusable for shop/category/home/related/upsells/search.
|
||||
*
|
||||
* Expects: $args['product'] = WC_Product|int (ID)
|
||||
* Optional: $args['show_hooks'] bool (wishlist/compare placeholders)
|
||||
*
|
||||
* @package XShop
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
$product = $args['product'] ?? null;
|
||||
if (is_int($product)) {
|
||||
$product = wc_get_product($product);
|
||||
}
|
||||
if (!$product instanceof WC_Product) {
|
||||
global $product;
|
||||
if (!isset($product) || !$product instanceof WC_Product) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$pid = $product->get_id();
|
||||
$title = $product->get_name();
|
||||
if ($title === '') {
|
||||
$title = esc_html__('Untitled', 'xshop');
|
||||
}
|
||||
$permalink = get_permalink($pid);
|
||||
$priceHtml = $product->get_price_html();
|
||||
$rating = $product->get_average_rating();
|
||||
$count = $product->get_rating_count();
|
||||
$stock = $product->get_stock_status();
|
||||
$isOnSale = $product->is_on_sale();
|
||||
$featured = $product->is_featured();
|
||||
$type = $product->get_type();
|
||||
$showHooks = !empty($args['show_hooks']);
|
||||
?>
|
||||
<article <?php wc_product_class('xshop-product-card', $product); ?>>
|
||||
<a href="<?php echo esc_url($permalink); ?>" class="xshop-product-card__media" aria-label="<?php echo esc_attr($title); ?>">
|
||||
<?php
|
||||
$imageId = $product->get_image_id();
|
||||
if ($imageId) {
|
||||
echo wp_get_attachment_image($imageId, 'xshop-card', false, ['loading' => 'lazy', 'decoding' => 'async', 'alt' => esc_attr($title)]);
|
||||
} elseif (has_post_thumbnail($pid)) {
|
||||
echo get_the_post_thumbnail($pid, 'xshop-card', ['loading' => 'lazy', 'decoding' => 'async', 'alt' => esc_attr($title)]);
|
||||
} else {
|
||||
echo '<span class="xshop-card__media--placeholder" style="display:flex;inline-size:100%;block-size:100%;align-items:center;justify-content:center;">' . esc_html__('No image', 'xshop') . '</span>';
|
||||
}
|
||||
?>
|
||||
<span class="xshop-product-card__badges" aria-hidden="true">
|
||||
<?php if ($isOnSale): ?>
|
||||
<span class="xshop-badge xshop-badge--sale">
|
||||
<?php
|
||||
if ($product->is_type('variable')) {
|
||||
esc_html_e('Sale', 'xshop');
|
||||
} else {
|
||||
$regular = (float) $product->get_regular_price();
|
||||
$sale = (float) $product->get_sale_price();
|
||||
if ($regular > 0 && $sale > 0) {
|
||||
$pct = (int) round(100 - ($sale / $regular * 100));
|
||||
echo esc_html(sprintf('-%d%%', $pct));
|
||||
} else {
|
||||
esc_html_e('Sale', 'xshop');
|
||||
}
|
||||
}
|
||||
?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<?php if ($stock === 'outofstock'): ?>
|
||||
<span class="xshop-badge xshop-badge--outofstock"><?php esc_html_e('Out of stock', 'xshop'); ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if ($featured): ?>
|
||||
<span class="xshop-badge xshop-badge--featured"><?php esc_html_e('Featured', 'xshop'); ?></span>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<div class="xshop-product-card__body">
|
||||
<h3 class="xshop-product-card__title">
|
||||
<a href="<?php echo esc_url($permalink); ?>"><?php echo esc_html($title); ?></a>
|
||||
</h3>
|
||||
|
||||
<?php if ($priceHtml !== ''): ?>
|
||||
<div class="xshop-product-card__price"><?php echo wp_kses_post($priceHtml); ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="xshop-product-card__meta">
|
||||
<?php if ($rating > 0): ?>
|
||||
<span class="xshop-product-card__rating" aria-label="<?php echo esc_attr(sprintf(esc_html__('Rated %s out of 5', 'xshop'), $rating)); ?>">
|
||||
<?php
|
||||
$stars = str_repeat('★', (int) round($rating)) . str_repeat('☆', 5 - (int) round($rating));
|
||||
echo esc_html($stars) . ' <span style="color:var(--xshop-muted)">(' . esc_html((string) $count) . ')</span>';
|
||||
?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<?php if ($type === 'variable'): ?>
|
||||
<span><?php esc_html_e('Variable', 'xshop'); ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if ($stock === 'outofstock'): ?>
|
||||
<span><?php esc_html_e('Out of stock', 'xshop'); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="xshop-product-card__actions">
|
||||
<?php
|
||||
// Use Woo's add-to-cart template (handles simple/variable correctly)
|
||||
woocommerce_template_loop_add_to_cart();
|
||||
?>
|
||||
<?php if ($showHooks): ?>
|
||||
<span class="xshop-product-card__hook" aria-hidden="true"><?php esc_html_e('Wishlist', 'xshop'); ?></span>
|
||||
<span class="xshop-product-card__hook" aria-hidden="true"><?php esc_html_e('Compare', 'xshop'); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// Hook placeholders for future wishlist/compare (no fake functionality)
|
||||
do_action('xshop_product_card_after_actions', $product);
|
||||
?>
|
||||
</div>
|
||||
</article>
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
/**
|
||||
* Product Gallery — lightweight, accessible, keyboard friendly.
|
||||
*
|
||||
* Args: ['product' => WC_Product]
|
||||
*
|
||||
* @package XShop
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
$product = $args['product'] ?? null;
|
||||
if (is_int($product)) { $product = wc_get_product($product); }
|
||||
if (!$product instanceof WC_Product) { global $product; }
|
||||
if (!$product instanceof WC_Product) { return; }
|
||||
|
||||
$pid = $product->get_id();
|
||||
$mainId = $product->get_image_id();
|
||||
$galleryIds = $product->get_gallery_image_ids();
|
||||
$allIds = $mainId ? array_merge([$mainId], $galleryIds) : $galleryIds;
|
||||
|
||||
if (empty($allIds)) {
|
||||
echo '<div class="xshop-product__gallery-main" style="display:flex;align-items:center;justify-content:center;color:var(--xshop-muted)">' . esc_html__('No image', 'xshop') . '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
$firstUrl = wp_get_attachment_image_url($allIds[0], 'large');
|
||||
?>
|
||||
<div class="xshop-product__gallery" data-xshop-gallery>
|
||||
<div class="xshop-product__gallery-main" id="xshop-gallery-main">
|
||||
<?php echo wp_get_attachment_image($allIds[0], 'large', false, ['loading' => 'eager', 'decoding' => 'async', 'alt' => esc_attr($product->get_name()), 'data-xshop-main-image' => '']); ?>
|
||||
</div>
|
||||
<?php if (count($allIds) > 1): ?>
|
||||
<div class="xshop-product__thumbs" role="tablist" aria-label="<?php esc_attr_e('Product gallery', 'xshop'); ?>">
|
||||
<?php foreach ($allIds as $idx => $attId):
|
||||
$thumbUrl = wp_get_attachment_image_url($attId, 'thumbnail');
|
||||
$largeUrl = wp_get_attachment_image_url($attId, 'large');
|
||||
$isActive = $idx === 0;
|
||||
?>
|
||||
<button type="button" class="xshop-product__thumb <?php echo $isActive ? 'is-active' : ''; ?>" data-large="<?php echo esc_url($largeUrl); ?>" aria-label="<?php echo esc_attr(sprintf(esc_html__('View image %d', 'xshop'), $idx + 1)); ?>" aria-selected="<?php echo $isActive ? 'true' : 'false'; ?>" role="tab">
|
||||
<?php echo wp_get_attachment_image($attId, 'thumbnail', false, ['loading' => 'lazy', 'decoding' => 'async', 'alt' => esc_attr(sprintf(esc_html__('Thumbnail %d', 'xshop'), $idx + 1))]); ?>
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/**
|
||||
* Product Grid — wraps product cards in CSS grid.
|
||||
*
|
||||
* Args: ['products' => WC_Product[]|int[], 'columns'=>int, 'empty_text'=>string]
|
||||
*
|
||||
* @package XShop
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
$products = $args['products'] ?? [];
|
||||
$columns = isset($args['columns']) ? max(1, min(6, (int) $args['columns'])) : 4;
|
||||
|
||||
if (empty($products)) {
|
||||
$text = $args['empty_text'] ?? esc_html__('No products found.', 'xshop');
|
||||
get_template_part('template-parts/components/empty-state', null, ['title' => esc_html__('No products', 'xshop'), 'text' => $text]);
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<ul class="xshop-products" data-columns="<?php echo esc_attr((string) $columns); ?>">
|
||||
<?php foreach ($products as $p):
|
||||
$product = is_int($p) ? wc_get_product($p) : $p;
|
||||
if (!$product instanceof WC_Product) { continue; }
|
||||
?>
|
||||
<li class="xshop-products__item">
|
||||
<?php get_template_part('template-parts/components/product-card', null, ['product' => $product, 'show_hooks' => $args['show_hooks'] ?? false]); ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
Reference in New Issue
Block a user