feat(upsell): apply real cart discounts and track performance
This commit is contained in:
213
public/css/upsell-frontend.css
Normal file
213
public/css/upsell-frontend.css
Normal file
@@ -0,0 +1,213 @@
|
||||
.sodino-upsell-panel {
|
||||
direction: rtl;
|
||||
margin: 0 0 28px;
|
||||
padding: 22px;
|
||||
color: #172033;
|
||||
background: #ffffff;
|
||||
border: 1px solid rgba(15, 23, 42, 0.12);
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 18px 42px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.sodino-upsell-panel *,
|
||||
.sodino-upsell-panel *::before,
|
||||
.sodino-upsell-panel *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sodino-upsell-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.sodino-upsell-eyebrow {
|
||||
margin: 0 0 6px;
|
||||
color: #2563eb;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sodino-upsell-header h2 {
|
||||
margin: 0;
|
||||
color: #111827;
|
||||
font-size: 22px;
|
||||
line-height: 1.45;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.sodino-upsell-count {
|
||||
flex: 0 0 auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 32px;
|
||||
padding: 6px 12px;
|
||||
color: #1d4ed8;
|
||||
background: #eff6ff;
|
||||
border-radius: 999px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sodino-upsell-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.sodino-upsell-card {
|
||||
display: grid;
|
||||
grid-template-columns: 96px minmax(0, 1fr);
|
||||
gap: 16px;
|
||||
min-height: 168px;
|
||||
padding: 16px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid rgba(15, 23, 42, 0.1);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.sodino-upsell-media {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.sodino-upsell-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.sodino-upsell-body {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sodino-upsell-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.sodino-upsell-meta span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 26px;
|
||||
padding: 4px 9px;
|
||||
color: #047857;
|
||||
background: #ecfdf5;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sodino-upsell-meta span + span {
|
||||
color: #475569;
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
.sodino-upsell-title {
|
||||
margin: 0 0 4px;
|
||||
color: #475569;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sodino-upsell-card h3 {
|
||||
margin: 0;
|
||||
color: #111827;
|
||||
font-size: 17px;
|
||||
line-height: 1.45;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.sodino-upsell-footer {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.sodino-upsell-price {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
color: #111827;
|
||||
font-size: 17px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.sodino-upsell-price del {
|
||||
margin-right: 8px;
|
||||
color: #94a3b8;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sodino-upsell-price small {
|
||||
color: #047857;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sodino-upsell-button {
|
||||
flex: 0 0 auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 42px;
|
||||
padding: 10px 16px;
|
||||
color: #ffffff !important;
|
||||
background: #2563eb;
|
||||
border-radius: 999px;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
text-decoration: none !important;
|
||||
transition: background-color 160ms ease, transform 160ms ease;
|
||||
}
|
||||
|
||||
.sodino-upsell-button:hover,
|
||||
.sodino-upsell-button:focus {
|
||||
color: #ffffff !important;
|
||||
background: #1d4ed8;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.sodino-upsell-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.sodino-upsell-panel {
|
||||
padding: 16px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.sodino-upsell-header,
|
||||
.sodino-upsell-footer {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.sodino-upsell-card {
|
||||
grid-template-columns: 76px minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.sodino-upsell-media {
|
||||
width: 76px;
|
||||
height: 76px;
|
||||
}
|
||||
|
||||
.sodino-upsell-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,21 @@ $upsellRepository = new UpsellRepository();
|
||||
$sodino_upsell_service = new UpsellService($upsellRepository);
|
||||
|
||||
add_action('woocommerce_before_cart', 'sodino_render_upsell_suggestions');
|
||||
add_action('wp_enqueue_scripts', 'sodino_enqueue_upsell_assets');
|
||||
add_filter('woocommerce_add_cart_item_data', 'sodino_add_upsell_cart_item_data', 10, 4);
|
||||
add_filter('woocommerce_get_cart_item_from_session', 'sodino_restore_upsell_cart_item_data', 10, 2);
|
||||
add_action('woocommerce_before_calculate_totals', 'sodino_apply_upsell_cart_prices', 20);
|
||||
add_filter('woocommerce_get_item_data', 'sodino_display_upsell_cart_item_data', 10, 2);
|
||||
add_action('woocommerce_checkout_create_order_line_item', 'sodino_add_upsell_order_item_meta', 10, 4);
|
||||
add_action('woocommerce_add_to_cart', 'sodino_track_upsell_conversion', 20, 6);
|
||||
|
||||
function sodino_enqueue_upsell_assets() {
|
||||
if (is_admin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_style('sodino-upsell-frontend', plugin_dir_url(__FILE__) . '../css/upsell-frontend.css', [], SODINO_VERSION);
|
||||
}
|
||||
|
||||
function sodino_render_upsell_suggestions() {
|
||||
if (is_admin() || !is_cart()) {
|
||||
@@ -41,19 +56,19 @@ function sodino_render_upsell_suggestions() {
|
||||
return;
|
||||
}
|
||||
|
||||
echo '<div class="sodino-upsell-panel bg-white rounded-2xl border border-gray-200 p-6 mb-8 shadow-sm">';
|
||||
echo '<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">';
|
||||
echo '<div>';
|
||||
echo '<p class="text-sm font-semibold text-blue-600">' . esc_html__('پیشنهاد ویژه آپسل', 'sodino') . '</p>';
|
||||
echo '<h2 class="mt-2 text-xl font-bold text-gray-900">' . esc_html__('این محصول را همراه خرید خود با تخفیف ویژه دریافت کنید', 'sodino') . '</h2>';
|
||||
echo '<section class="sodino-upsell-panel" aria-label="' . esc_attr__('پیشنهادهای ویژه سودینو', 'sodino') . '">';
|
||||
echo '<div class="sodino-upsell-header">';
|
||||
echo '<div>';
|
||||
echo '<p class="sodino-upsell-eyebrow">' . esc_html__('پیشنهاد ویژه', 'sodino') . '</p>';
|
||||
echo '<h2>' . esc_html__('این پیشنهادها را با تخفیف به سبد خود اضافه کنید', 'sodino') . '</h2>';
|
||||
echo '</div>';
|
||||
echo '<span class="inline-flex items-center rounded-full bg-blue-50 px-3 py-1 text-sm font-medium text-blue-700">' . count($upsells) . ' ' . esc_html__('پیشنهاد فعال', 'sodino') . '</span>';
|
||||
echo '<span class="sodino-upsell-count">' . esc_html(sprintf(__('%d پیشنهاد فعال', 'sodino'), count($upsells))) . '</span>';
|
||||
echo '</div>';
|
||||
echo '<div class="mt-6 grid gap-4 lg:grid-cols-'.min(2, count($upsells)).'">';
|
||||
echo '<div class="sodino-upsell-grid">';
|
||||
|
||||
foreach ($upsells as $upsell) {
|
||||
$product = wc_get_product($upsell->target_product_id);
|
||||
if (!$product) {
|
||||
if (!$product || !$product->is_purchasable() || !$product->is_in_stock() || !$product->is_type(['simple', 'variation'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -61,31 +76,161 @@ function sodino_render_upsell_suggestions() {
|
||||
$originalPrice = floatval($product->get_price());
|
||||
$priceHtml = wc_price($discountedPrice);
|
||||
if ($discountedPrice < $originalPrice) {
|
||||
$priceHtml .= ' <span class="mr-2 text-sm text-gray-500 line-through">' . wc_price($originalPrice) . '</span>';
|
||||
$priceHtml .= ' <del>' . wc_price($originalPrice) . '</del>';
|
||||
}
|
||||
|
||||
$addToCartUrl = esc_url(add_query_arg('add-to-cart', $product->get_id(), wc_get_cart_url()));
|
||||
$image = $product->get_image('woocommerce_thumbnail', ['class' => 'h-20 w-20 rounded-xl object-cover']);
|
||||
$addToCartUrl = sodino_get_upsell_add_to_cart_url($product, $upsell);
|
||||
$image = $product->get_image('woocommerce_thumbnail', ['class' => 'sodino-upsell-image']);
|
||||
$savings = max(0, $originalPrice - $discountedPrice);
|
||||
$sodino_upsell_service->incrementImpression($upsell->id);
|
||||
|
||||
echo '<div class="rounded-2xl border border-gray-200 p-5 bg-gray-50">';
|
||||
echo '<div class="flex gap-4">';
|
||||
echo '<div class="flex-shrink-0">' . $image . '</div>';
|
||||
echo '<div class="flex-1">';
|
||||
echo '<p class="text-sm font-medium text-gray-700">' . esc_html($upsell->title) . '</p>';
|
||||
echo '<h3 class="mt-2 text-lg font-semibold text-gray-900">' . esc_html($product->get_name()) . '</h3>';
|
||||
echo '<div class="mt-3 flex items-center gap-3">';
|
||||
echo '<span class="rounded-full bg-green-50 px-3 py-1 text-sm font-medium text-green-700">' . esc_html($sodino_upsell_service->getDiscountLabel($upsell)) . '</span>';
|
||||
echo '<span class="text-sm text-gray-600">' . esc_html($sodino_upsell_service->getTriggerLabel($upsell)) . '</span>';
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
echo '<div class="mt-5 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">';
|
||||
echo '<div class="text-lg font-semibold text-gray-900">' . $priceHtml . '</div>';
|
||||
echo '<a href="' . $addToCartUrl . '" class="inline-flex items-center justify-center rounded-full bg-blue-600 px-5 py-3 text-sm font-semibold text-white hover:bg-blue-700">' . esc_html__('افزودن به سبد', 'sodino') . '</a>';
|
||||
echo '<article class="sodino-upsell-card">';
|
||||
echo '<div class="sodino-upsell-media">' . $image . '</div>';
|
||||
echo '<div class="sodino-upsell-body">';
|
||||
echo '<div class="sodino-upsell-meta">';
|
||||
echo '<span>' . esc_html($sodino_upsell_service->getDiscountLabel($upsell)) . '</span>';
|
||||
echo '<span>' . esc_html($sodino_upsell_service->getTriggerLabel($upsell)) . '</span>';
|
||||
echo '</div>';
|
||||
echo '<p class="sodino-upsell-title">' . esc_html($upsell->title) . '</p>';
|
||||
echo '<h3>' . esc_html($product->get_name()) . '</h3>';
|
||||
echo '<div class="sodino-upsell-footer">';
|
||||
echo '<div class="sodino-upsell-price">' . $priceHtml;
|
||||
if ($savings > 0) {
|
||||
echo '<small>' . esc_html(sprintf(__('صرفهجویی: %s', 'sodino'), wp_strip_all_tags(wc_price($savings)))) . '</small>';
|
||||
}
|
||||
echo '</div>';
|
||||
echo '<a href="' . esc_url($addToCartUrl) . '" class="sodino-upsell-button">' . esc_html__('افزودن با تخفیف', 'sodino') . '</a>';
|
||||
echo '</div></div>';
|
||||
echo '</article>';
|
||||
}
|
||||
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
echo '</section>';
|
||||
}
|
||||
|
||||
function sodino_get_upsell_add_to_cart_url($product, $upsell) {
|
||||
return add_query_arg(
|
||||
[
|
||||
'add-to-cart' => $product->get_id(),
|
||||
'quantity' => 1,
|
||||
'sodino_upsell_id' => $upsell->id,
|
||||
'sodino_upsell_nonce' => wp_create_nonce('sodino_apply_upsell_' . $upsell->id),
|
||||
],
|
||||
wc_get_cart_url()
|
||||
);
|
||||
}
|
||||
|
||||
function sodino_add_upsell_cart_item_data($cart_item_data, $product_id, $variation_id, $quantity) {
|
||||
if (empty($_REQUEST['sodino_upsell_id'])) {
|
||||
return $cart_item_data;
|
||||
}
|
||||
|
||||
$upsellId = absint(wp_unslash($_REQUEST['sodino_upsell_id']));
|
||||
$nonce = sanitize_text_field(wp_unslash($_REQUEST['sodino_upsell_nonce'] ?? ''));
|
||||
if (!$upsellId || !wp_verify_nonce($nonce, 'sodino_apply_upsell_' . $upsellId)) {
|
||||
return $cart_item_data;
|
||||
}
|
||||
|
||||
global $sodino_upsell_service;
|
||||
if (!isset($sodino_upsell_service) || !WC()->cart) {
|
||||
return $cart_item_data;
|
||||
}
|
||||
|
||||
$upsell = $sodino_upsell_service->getById($upsellId);
|
||||
if (
|
||||
!$sodino_upsell_service->canApplyToProduct($upsell, $product_id, $variation_id)
|
||||
|| !$sodino_upsell_service->isValidForCart($upsell, WC()->cart)
|
||||
) {
|
||||
return $cart_item_data;
|
||||
}
|
||||
|
||||
$product = wc_get_product($variation_id ?: $product_id);
|
||||
if (!$product) {
|
||||
return $cart_item_data;
|
||||
}
|
||||
|
||||
$originalPrice = floatval($product->get_price());
|
||||
$discountedPrice = $sodino_upsell_service->calculateDiscountedPrice($originalPrice, $upsell);
|
||||
|
||||
$cart_item_data['sodino_upsell'] = [
|
||||
'id' => (int) $upsell->id,
|
||||
'title' => sanitize_text_field($upsell->title),
|
||||
'discount_type' => sanitize_key($upsell->discount_type),
|
||||
'discount_value' => floatval($upsell->discount_value),
|
||||
'original_price' => $originalPrice,
|
||||
'discounted_price' => $discountedPrice,
|
||||
];
|
||||
$cart_item_data['sodino_upsell_key'] = md5(wp_json_encode($cart_item_data['sodino_upsell']) . microtime(true));
|
||||
|
||||
return $cart_item_data;
|
||||
}
|
||||
|
||||
function sodino_restore_upsell_cart_item_data($cart_item, $values) {
|
||||
if (!empty($values['sodino_upsell'])) {
|
||||
$cart_item['sodino_upsell'] = $values['sodino_upsell'];
|
||||
$cart_item['sodino_upsell_key'] = $values['sodino_upsell_key'] ?? '';
|
||||
}
|
||||
return $cart_item;
|
||||
}
|
||||
|
||||
function sodino_apply_upsell_cart_prices($cart) {
|
||||
if (is_admin() && !defined('DOING_AJAX')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$cart) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($cart->get_cart() as $cartItem) {
|
||||
if (empty($cartItem['sodino_upsell']['discounted_price']) || empty($cartItem['data'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$cartItem['data']->set_price(max(0, floatval($cartItem['sodino_upsell']['discounted_price'])));
|
||||
}
|
||||
}
|
||||
|
||||
function sodino_display_upsell_cart_item_data($item_data, $cart_item) {
|
||||
if (empty($cart_item['sodino_upsell'])) {
|
||||
return $item_data;
|
||||
}
|
||||
|
||||
$upsell = $cart_item['sodino_upsell'];
|
||||
$item_data[] = [
|
||||
'key' => __('پیشنهاد سودینو', 'sodino'),
|
||||
'value' => esc_html($upsell['title']),
|
||||
];
|
||||
|
||||
if (!empty($upsell['original_price']) && floatval($upsell['discounted_price']) < floatval($upsell['original_price'])) {
|
||||
$item_data[] = [
|
||||
'key' => __('تخفیف آپسل', 'sodino'),
|
||||
'value' => wp_kses_post(wc_price(floatval($upsell['original_price']) - floatval($upsell['discounted_price']))),
|
||||
];
|
||||
}
|
||||
|
||||
return $item_data;
|
||||
}
|
||||
|
||||
function sodino_add_upsell_order_item_meta($item, $cart_item_key, $values, $order) {
|
||||
if (empty($values['sodino_upsell'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$upsell = $values['sodino_upsell'];
|
||||
$item->add_meta_data('_sodino_upsell_id', (int) $upsell['id'], true);
|
||||
$item->add_meta_data(__('پیشنهاد سودینو', 'sodino'), sanitize_text_field($upsell['title']), true);
|
||||
$item->add_meta_data(__('قیمت اصلی آپسل', 'sodino'), wc_price(floatval($upsell['original_price'])), true);
|
||||
$item->add_meta_data(__('قیمت تخفیفی آپسل', 'sodino'), wc_price(floatval($upsell['discounted_price'])), true);
|
||||
}
|
||||
|
||||
function sodino_track_upsell_conversion($cart_item_key, $product_id, $quantity, $variation_id, $variation, $cart_item_data) {
|
||||
if (empty($cart_item_data['sodino_upsell']['id'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
global $sodino_upsell_service;
|
||||
if (isset($sodino_upsell_service)) {
|
||||
$sodino_upsell_service->incrementConversion((int) $cart_item_data['sodino_upsell']['id']);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user