feat: Add banner management functionality

- Implemented a new Banner model to represent banner data.
- Created a BannerRepository for database interactions related to banners.
- Developed a BannerService to handle business logic for banners.
- Added admin views for listing and adding banners.
- Integrated banner hooks for frontend rendering and click tracking.
- Created frontend styles and scripts for banner display and interaction.
- Updated database migrations to include a new banners table.
- Enhanced AdminController to manage banner actions and pages.
This commit is contained in:
2026-05-05 01:03:05 +03:30
parent 5930c1ad6f
commit 32c065e4b6
15 changed files with 1350 additions and 4 deletions

View File

@@ -5,13 +5,15 @@ if (!defined('ABSPATH')) {
}
use Sodino\Controllers\AdminController;
use Sodino\Repositories\BannerRepository;
use Sodino\Repositories\RuleRepository;
use Sodino\Repositories\UpsellRepository;
// Initialize admin
$ruleRepository = new RuleRepository();
$upsellRepository = new UpsellRepository();
$adminController = new AdminController($ruleRepository, $upsellRepository);
$bannerRepository = new BannerRepository();
$adminController = new AdminController($ruleRepository, $upsellRepository, $bannerRepository);
// Add menu
add_action('admin_menu', [$adminController, 'addMenu']);
@@ -41,13 +43,20 @@ add_action('admin_enqueue_scripts', function($hook) use ($adminController) {
'nonce' => wp_create_nonce('sodino_search_products'),
]);
}
if (strpos($hook, 'sodino_page_sodino-add-banner') !== false) {
wp_enqueue_media();
wp_enqueue_script('sodino-banner-admin', plugin_dir_url(__FILE__) . 'js/banner-admin.js', ['jquery'], SODINO_VERSION, true);
}
});
// Handle delete for any Sodino admin page
if (isset($_GET['page']) && strpos($_GET['page'], 'sodino') === 0 && isset($_GET['action']) && $_GET['action'] === 'delete') {
add_action('admin_init', [$adminController, 'handleDelete']);
}
if (isset($_GET['page']) && strpos($_GET['page'], 'sodino') === 0 && isset($_GET['action']) && in_array($_GET['action'], ['delete_banner', 'toggle_banner_status'], true)) {
add_action('admin_init', [$adminController, 'handleBannerActions']);
}
// Handle upsell actions
if (isset($_GET['page']) && strpos($_GET['page'], 'sodino') === 0 && isset($_GET['action']) && in_array($_GET['action'], ['delete_upsell', 'toggle_upsell_status'], true)) {
add_action('admin_init', [$adminController, 'handleUpsellActions']);

View File

@@ -0,0 +1,142 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
if (!class_exists('WP_List_Table')) {
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}
class Sodino_Banner_List_Table extends WP_List_Table {
private $repository;
private $items_per_page = 20;
public function __construct($repository) {
parent::__construct([
'singular' => 'sodino_banner',
'plural' => 'sodino_banners',
'ajax' => false,
]);
$this->repository = $repository;
}
public function get_columns() {
return [
'cb' => '<input type="checkbox" />',
'title' => __('عنوان', 'sodino'),
'position' => __('محل نمایش', 'sodino'),
'display_type' => __('نوع نمایش', 'sodino'),
'schedule' => __('زمان‌بندی', 'sodino'),
'status' => __('وضعیت', 'sodino'),
'stats' => __('آمار', 'sodino'),
'actions' => __('عملیات', 'sodino'),
];
}
protected function get_sortable_columns() {
return [
'priority' => ['priority', true],
'title' => ['title', true],
];
}
protected function column_cb($item) {
return sprintf('<input type="checkbox" name="banner_ids[]" value="%d" />', $item->id);
}
public function get_bulk_actions() {
return [
'delete' => __('حذف گروهی', 'sodino'),
];
}
public function column_title($item) {
$edit_url = admin_url('admin.php?page=sodino-add-banner&action=edit&id=' . $item->id);
return sprintf('<strong><a href="%s">%s</a></strong>', esc_url($edit_url), esc_html($item->title));
}
public function column_position($item) {
$map = [
'top' => __('بالای سایت', 'sodino'),
'middle' => __('وسط محتوا', 'sodino'),
'bottom' => __('پایین', 'sodino'),
'product_page' => __('صفحه محصول', 'sodino'),
'cart' => __('سبد خرید', 'sodino'),
];
return $map[$item->position] ?? __('نامشخص', 'sodino');
}
public function column_display_type($item) {
$map = [
'inline' => __('درون صفحه', 'sodino'),
'popup' => __('پاپ‌آپ', 'sodino'),
'floating_bar' => __('نوار شناور', 'sodino'),
];
return $map[$item->display_type] ?? __('نامشخص', 'sodino');
}
public function column_schedule($item) {
if (!empty($item->start_time) || !empty($item->end_time)) {
$start = $item->start_time ? date_i18n('Y/m/d H:i', strtotime($item->start_time)) : __('بدون شروع', 'sodino');
$end = $item->end_time ? date_i18n('Y/m/d H:i', strtotime($item->end_time)) : __('بدون پایان', 'sodino');
return sprintf('%s<br><small class="description">%s</small>', esc_html($start), esc_html($end));
}
return __('بدون محدودیت زمانی', 'sodino');
}
public function column_status($item) {
return $item->status ? __('فعال', 'sodino') : __('غیرفعال', 'sodino');
}
public function column_stats($item) {
$ctr = $item->impressions > 0 ? round(($item->clicks / $item->impressions) * 100, 2) : 0;
return sprintf('<div class="sodino-stats-grid"><p>%s: <strong>%s</strong></p><p>%s: <strong>%s</strong></p><p>CTR: <strong>%s%%</strong></p></div>',
__('نمایش', 'sodino'), esc_html(number_format_i18n($item->impressions)),
__('کلیک', 'sodino'), esc_html(number_format_i18n($item->clicks)),
esc_html(number_format_i18n($ctr))
);
}
public function column_actions($item) {
$edit_url = admin_url('admin.php?page=sodino-add-banner&action=edit&id=' . $item->id);
$toggle_url = wp_nonce_url(admin_url('admin.php?page=sodino-banners&action=toggle_banner_status&id=' . $item->id), 'toggle_banner_status');
$delete_url = wp_nonce_url(admin_url('admin.php?page=sodino-banners&action=delete_banner&id=' . $item->id), 'delete_banner');
$toggle_label = $item->status ? __('غیرفعال کردن', 'sodino') : __('فعال کردن', 'sodino');
return sprintf(
'<a href="%s">%s</a> | <a href="%s">%s</a> | <a href="%s" onclick="return confirm(\'%s\');">%s</a>',
esc_url($edit_url), esc_html__('ویرایش', 'sodino'),
esc_url($toggle_url), esc_html($toggle_label),
esc_url($delete_url), esc_js(__('آیا از حذف این بنر مطمئن هستید؟', 'sodino')),
esc_html__('حذف', 'sodino')
);
}
public function prepare_items() {
$this->_column_headers = [$this->get_columns(), [], $this->get_sortable_columns()];
$this->process_bulk_action();
$all_items = $this->repository->getAll();
$current_page = $this->get_pagenum();
$total_items = count($all_items);
$this->items = array_slice($all_items, ($current_page - 1) * $this->items_per_page, $this->items_per_page);
$this->set_pagination_args([
'total_items' => $total_items,
'per_page' => $this->items_per_page,
'total_pages' => ceil($total_items / $this->items_per_page),
]);
}
public function process_bulk_action() {
if ('delete' === $this->current_action()) {
$banner_ids = isset($_POST['banner_ids']) ? array_map('intval', $_POST['banner_ids']) : [];
if (!empty($banner_ids) && check_admin_referer('bulk-' . $this->_args['plural'])) {
foreach ($banner_ids as $id) {
$this->repository->delete($id);
}
}
}
}
}

View File

@@ -294,3 +294,94 @@
#sodino-app .sd-chart-card {
min-height: 300px;
}
#sodino-app {
min-height: 100vh;
background: radial-gradient(circle at top right, rgba(99, 102, 241, 0.14), transparent 22%),
radial-gradient(circle at bottom left, rgba(59, 130, 246, 0.08), transparent 15%),
#f8fafc;
padding: 30px 16px 50px;
color: #0f172a;
}
#sodino-app .bg-white.rounded-lg.shadow-sm.border,
#sodino-app .bg-white.rounded-2xl.border {
background: rgba(255, 255, 255, 0.98);
border-color: rgba(148, 163, 184, 0.70);
box-shadow: 0 18px 35px rgba(15, 23, 42, 0.10);
}
#sodino-app .bg-white.rounded-lg.shadow-sm.border,
#sodino-app .bg-white.rounded-2xl.border,
#sodino-app .bg-gray-50,
#sodino-app .sd-sidebar,
#sodino-app .sd-card,
#sodino-app .sd-chart-card {
border-width: 1px;
border-style: solid;
}
#sodino-app .bg-gray-50 {
background: #f7fbff !important;
border-color: rgba(148, 163, 184, 0.35);
}
#sodino-app .shadow-sm {
box-shadow: 0 14px 30px rgba(15, 23, 42, 0.09) !important;
}
#sodino-app .sd-sidebar,
#sodino-app .sd-card,
#sodino-app .sd-chart-card {
border-color: rgba(148, 163, 184, 0.65);
}
#sodino-app .rounded-lg,
#sodino-app .rounded-2xl,
#sodino-app .rounded-md {
border-radius: 1.3rem !important;
}
#sodino-app .space-y-2 > a {
display: block;
padding: 0.95rem 1rem;
border-radius: 1rem;
transition: all 0.18s ease-in-out;
border: 1px solid transparent;
}
#sodino-app .space-y-2 > a:hover {
background: rgba(99, 102, 241, 0.1);
}
#sodino-app .text-gray-500,
#sodino-app .text-gray-600,
#sodino-app .text-gray-700 {
color: #334155 !important;
}
#sodino-app h1,
#sodino-app h2,
#sodino-app h3,
#sodino-app h4 {
letter-spacing: -0.02em;
}
#sodino-app .bg-white.rounded-lg.shadow-sm.border p,
#sodino-app .bg-white.rounded-2xl.border p {
color: #475569;
}
#sodino-app .bg-white.rounded-lg.shadow-sm.border .inline-flex,
#sodino-app .bg-white.rounded-2xl.border .inline-flex {
box-shadow: 0 10px 22px rgba(15, 23, 42, 0.06);
}
#sodino-app table thead th {
color: #334155 !important;
}
#sodino-app .wp-list-table .column-name,
#sodino-app .wp-list-table .column-title {
font-weight: 600;
}

75
admin/js/banner-admin.js Normal file
View File

@@ -0,0 +1,75 @@
(function () {
const contentTypeRadios = document.querySelectorAll('.sodino-banner-content-type');
const contentGroups = document.querySelectorAll('.sodino-banner-content-group');
const mediaButton = document.getElementById('sodino-banner-image-upload');
const imageInput = document.getElementById('content_value_image');
const presetButtons = document.querySelectorAll('.sodino-banner-preset');
const startInput = document.getElementById('start_time');
const endInput = document.getElementById('end_time');
function toggleFields() {
const selectedType = document.querySelector('.sodino-banner-content-type:checked');
const type = selectedType ? selectedType.value : 'image';
contentGroups.forEach((group) => {
group.style.display = group.dataset.type === type ? 'block' : 'none';
});
}
if (contentTypeRadios.length) {
contentTypeRadios.forEach((radio) => radio.addEventListener('change', toggleFields));
toggleFields();
}
if (mediaButton && imageInput && window.wp && wp.media) {
mediaButton.addEventListener('click', function () {
const mediaFrame = wp.media({
title: 'انتخاب تصویر بنر',
button: { text: 'انتخاب' },
multiple: false,
});
mediaFrame.on('select', function () {
const attachment = mediaFrame.state().get('selection').first().toJSON();
imageInput.value = attachment.url;
});
mediaFrame.open();
});
}
const setDatetimeLocal = (element, date) => {
if (!element) return;
const tzOffset = date.getTimezoneOffset() * 60000;
const localISO = new Date(date - tzOffset).toISOString().slice(0, 16);
element.value = localISO;
};
if (presetButtons.length) {
presetButtons.forEach((button) => {
button.addEventListener('click', function () {
const preset = this.dataset.preset;
const now = new Date();
if (preset === 'today') {
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0);
const end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59);
setDatetimeLocal(startInput, start);
setDatetimeLocal(endInput, end);
}
if (preset === 'weekend') {
const friday = new Date(now);
friday.setDate(friday.getDate() + ((5 - friday.getDay() + 7) % 7));
const saturday = new Date(friday);
saturday.setDate(friday.getDate() + 1);
setDatetimeLocal(startInput, new Date(friday.getFullYear(), friday.getMonth(), friday.getDate(), 0, 0));
setDatetimeLocal(endInput, new Date(saturday.getFullYear(), saturday.getMonth(), saturday.getDate(), 23, 59));
}
if (preset === 'hourly') {
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 18, 0);
const end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 0);
setDatetimeLocal(startInput, start);
setDatetimeLocal(endInput, end);
}
});
});
}
})();

219
admin/views/banner-form.php Normal file
View File

@@ -0,0 +1,219 @@
<?php
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
$current_page = sanitize_text_field($_GET['page'] ?? 'sodino-add-banner');
?>
<div id="sodino-app" class="min-h-screen bg-gray-50" dir="rtl">
<div class="bg-white border-b border-gray-200 shadow-sm">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<h1 class="text-3xl font-bold text-gray-900"><?php echo $banner->id ? __('ویرایش بنر', 'sodino') : __('افزودن بنر جدید', 'sodino'); ?></h1>
<p class="mt-1 text-sm text-gray-500"><?php _e('تنظیمات تبلیغات هوشمند و زمان‌بندی نمایش بنر در سایت.', 'sodino'); ?></p>
</div>
<a href="<?php echo admin_url('admin.php?page=sodino-banners'); ?>" class="inline-flex items-center gap-2 rounded-full bg-white px-5 py-3 text-sm font-semibold text-gray-700 border border-gray-200 shadow-sm hover:bg-gray-50">
<?php _e('بازگشت به لیست بنرها', 'sodino'); ?>
</a>
</div>
</div>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="grid gap-8 lg:grid-cols-[280px_1fr]">
<aside class="bg-white rounded-2xl border border-gray-200 p-6 shadow-sm">
<h2 class="text-lg font-semibold text-gray-900 mb-4"><?php _e('منوی سودینو', 'sodino'); ?></h2>
<nav class="space-y-2">
<a href="<?php echo admin_url('admin.php?page=sodino-dashboard'); ?>" class="block px-3 py-2 rounded-xl text-sm font-medium <?php echo $current_page === 'sodino-dashboard' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('داشبورد', 'sodino'); ?>
</a>
<a href="<?php echo admin_url('admin.php?page=sodino-rules'); ?>" class="block px-3 py-2 rounded-xl text-sm font-medium <?php echo $current_page === 'sodino-rules' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('قوانین', 'sodino'); ?>
</a>
<a href="<?php echo admin_url('admin.php?page=sodino-banners'); ?>" class="block px-3 py-2 rounded-xl text-sm font-medium <?php echo $current_page === 'sodino-banners' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('بنرهای هوشمند', 'sodino'); ?>
</a>
<a href="<?php echo admin_url('admin.php?page=sodino-settings'); ?>" class="block px-3 py-2 rounded-xl text-sm font-medium <?php echo $current_page === 'sodino-settings' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('تنظیمات', 'sodino'); ?>
</a>
</nav>
</aside>
<main class="space-y-6">
<form method="post">
<?php wp_nonce_field('sodino_save_banner', 'sodino_banner_nonce'); ?>
<div class="bg-white rounded-2xl border border-gray-200 p-6 shadow-sm space-y-6">
<div>
<h2 class="text-xl font-semibold text-gray-900"><?php _e('اطلاعات اصلی', 'sodino'); ?></h2>
<p class="mt-2 text-sm text-gray-500"><?php _e('عنوان، محتوا و لینک بنر را تعریف کنید.', 'sodino'); ?></p>
</div>
<div class="grid gap-6 lg:grid-cols-2">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2" for="title"><?php _e('عنوان بنر', 'sodino'); ?></label>
<input type="text" name="title" id="title" value="<?php echo esc_attr($banner->title); ?>" class="w-full rounded-2xl border border-gray-300 bg-white px-4 py-3 text-gray-700 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-100" required>
</div>
<div>
<span class="block text-sm font-medium text-gray-700 mb-2"><?php _e('نوع محتوا', 'sodino'); ?></span>
<div class="space-y-2">
<?php $content_types = ['image' => __('تصویر', 'sodino'), 'html' => __('HTML', 'sodino'), 'shortcode' => __('شورت‌کد', 'sodino')]; ?>
<?php foreach ($content_types as $value => $label) : ?>
<label class="flex items-center gap-3 rounded-2xl border border-gray-300 bg-white px-4 py-3 cursor-pointer hover:border-blue-300">
<input type="radio" name="content_type" value="<?php echo esc_attr($value); ?>" <?php checked($banner->content_type, $value); ?> class="h-4 w-4 text-blue-600 sodino-banner-content-type">
<span class="text-sm text-gray-700"><?php echo esc_html($label); ?></span>
</label>
<?php endforeach; ?>
</div>
</div>
</div>
<div class="grid gap-6">
<div class="sodino-banner-content-group" data-type="image">
<label class="block text-sm font-medium text-gray-700 mb-2" for="content_value_image"><?php _e('آدرس تصویر بنر', 'sodino'); ?></label>
<div class="flex gap-3">
<input type="text" name="content_value" id="content_value_image" value="<?php echo esc_attr($banner->content_type === 'image' ? $banner->content_value : ''); ?>" class="w-full rounded-2xl border border-gray-300 bg-white px-4 py-3 text-gray-700 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-100">
<button type="button" id="sodino-banner-image-upload" class="inline-flex items-center rounded-2xl border border-gray-300 bg-white px-4 py-3 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50"><?php _e('انتخاب تصویر', 'sodino'); ?></button>
</div>
</div>
<div class="sodino-banner-content-group" data-type="html">
<label class="block text-sm font-medium text-gray-700 mb-2" for="content_value_html"><?php _e('محتوای HTML بنر', 'sodino'); ?></label>
<textarea name="content_value" id="content_value_html" rows="6" class="w-full rounded-2xl border border-gray-300 bg-white px-4 py-3 text-gray-700 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-100"><?php echo esc_textarea($banner->content_type === 'html' ? $banner->content_value : ''); ?></textarea>
</div>
<div class="sodino-banner-content-group" data-type="shortcode">
<label class="block text-sm font-medium text-gray-700 mb-2" for="content_value_shortcode"><?php _e('شورت‌کد بنر', 'sodino'); ?></label>
<textarea name="content_value" id="content_value_shortcode" rows="4" class="w-full rounded-2xl border border-gray-300 bg-white px-4 py-3 text-gray-700 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-100"><?php echo esc_textarea($banner->content_type === 'shortcode' ? $banner->content_value : ''); ?></textarea>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2" for="link_url"><?php _e('لینک بنر (اختیاری)', 'sodino'); ?></label>
<input type="url" name="link_url" id="link_url" value="<?php echo esc_attr($banner->link_url); ?>" class="w-full rounded-2xl border border-gray-300 bg-white px-4 py-3 text-gray-700 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-100" placeholder="https://">
</div>
</div>
<div class="bg-white rounded-2xl border border-gray-200 p-6 shadow-sm space-y-6">
<div>
<h2 class="text-xl font-semibold text-gray-900"><?php _e('نمایش', 'sodino'); ?></h2>
<p class="mt-2 text-sm text-gray-500"><?php _e('محل و سبک نمایش بنر را مشخص کنید.', 'sodino'); ?></p>
</div>
<div class="grid gap-6 lg:grid-cols-2">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2" for="position"><?php _e('محل نمایش', 'sodino'); ?></label>
<select name="position" id="position" class="w-full rounded-2xl border border-gray-300 bg-white px-4 py-3 text-gray-700 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-100">
<option value="top" <?php selected($banner->position, 'top'); ?>><?php _e('بالای سایت', 'sodino'); ?></option>
<option value="middle" <?php selected($banner->position, 'middle'); ?>><?php _e('وسط محتوا', 'sodino'); ?></option>
<option value="bottom" <?php selected($banner->position, 'bottom'); ?>><?php _e('پایین', 'sodino'); ?></option>
<option value="product_page" <?php selected($banner->position, 'product_page'); ?>><?php _e('صفحه محصول', 'sodino'); ?></option>
<option value="cart" <?php selected($banner->position, 'cart'); ?>><?php _e('سبد خرید', 'sodino'); ?></option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2"><?php _e('نوع نمایش', 'sodino'); ?></label>
<div class="grid gap-2">
<?php $display_types = ['inline' => __('داخل صفحه', 'sodino'), 'popup' => __('پاپ‌آپ', 'sodino'), 'floating_bar' => __('نوار شناور', 'sodino')]; ?>
<?php foreach ($display_types as $value => $label) : ?>
<label class="flex items-center gap-3 rounded-2xl border border-gray-300 bg-white px-4 py-3 cursor-pointer hover:border-blue-300">
<input type="radio" name="display_type" value="<?php echo esc_attr($value); ?>" <?php checked($banner->display_type, $value); ?> class="h-4 w-4 text-blue-600">
<span class="text-sm text-gray-700"><?php echo esc_html($label); ?></span>
</label>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<div class="bg-white rounded-2xl border border-gray-200 p-6 shadow-sm space-y-6">
<div>
<h2 class="text-xl font-semibold text-gray-900"><?php _e('زمان‌بندی', 'sodino'); ?></h2>
<p class="mt-2 text-sm text-gray-500"><?php _e('زمان شروع و پایان نمایش بنر را تنظیم کنید.', 'sodino'); ?></p>
</div>
<div class="grid gap-4 md:grid-cols-3">
<button type="button" class="sodino-banner-preset inline-flex items-center justify-center rounded-2xl border border-gray-300 bg-white px-4 py-3 text-sm font-medium text-gray-700 shadow-sm hover:border-blue-300" data-preset="today"><?php _e('فقط امروز', 'sodino'); ?></button>
<button type="button" class="sodino-banner-preset inline-flex items-center justify-center rounded-2xl border border-gray-300 bg-white px-4 py-3 text-sm font-medium text-gray-700 shadow-sm hover:border-blue-300" data-preset="weekend"><?php _e('فقط آخر هفته', 'sodino'); ?></button>
<button type="button" class="sodino-banner-preset inline-flex items-center justify-center rounded-2xl border border-gray-300 bg-white px-4 py-3 text-sm font-medium text-gray-700 shadow-sm hover:border-blue-300" data-preset="hourly"><?php _e('ساعتی (18:00 تا 23:00)', 'sodino'); ?></button>
</div>
<div class="grid gap-6 lg:grid-cols-2">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2" for="start_time"><?php _e('تاریخ شروع', 'sodino'); ?></label>
<input type="datetime-local" name="start_time" id="start_time" value="<?php echo esc_attr($banner->start_time ? date('Y-m-d\TH:i', strtotime($banner->start_time)) : ''); ?>" class="w-full rounded-2xl border border-gray-300 bg-white px-4 py-3 text-gray-700 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-100">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2" for="end_time"><?php _e('تاریخ پایان', 'sodino'); ?></label>
<input type="datetime-local" name="end_time" id="end_time" value="<?php echo esc_attr($banner->end_time ? date('Y-m-d\TH:i', strtotime($banner->end_time)) : ''); ?>" class="w-full rounded-2xl border border-gray-300 bg-white px-4 py-3 text-gray-700 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-100">
</div>
</div>
</div>
<div class="bg-white rounded-2xl border border-gray-200 p-6 shadow-sm space-y-6">
<div>
<h2 class="text-xl font-semibold text-gray-900"><?php _e('هدف‌گذاری', 'sodino'); ?></h2>
<p class="mt-2 text-sm text-gray-500"><?php _e('نوع کاربر و دستگاه نمایش بنر را مشخص کنید.', 'sodino'); ?></p>
</div>
<div class="grid gap-6 lg:grid-cols-2">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2" for="user_target"><?php _e('نوع کاربر', 'sodino'); ?></label>
<select name="user_target" id="user_target" class="w-full rounded-2xl border border-gray-300 bg-white px-4 py-3 text-gray-700 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-100">
<option value="all" <?php selected($banner->user_target, 'all'); ?>><?php _e('همه', 'sodino'); ?></option>
<option value="new" <?php selected($banner->user_target, 'new'); ?>><?php _e('کاربر جدید', 'sodino'); ?></option>
<option value="returning" <?php selected($banner->user_target, 'returning'); ?>><?php _e('بازگشتی', 'sodino'); ?></option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2" for="device_target"><?php _e('دستگاه', 'sodino'); ?></label>
<select name="device_target" id="device_target" class="w-full rounded-2xl border border-gray-300 bg-white px-4 py-3 text-gray-700 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-100">
<option value="all" <?php selected($banner->device_target, 'all'); ?>><?php _e('همه', 'sodino'); ?></option>
<option value="desktop" <?php selected($banner->device_target, 'desktop'); ?>><?php _e('دسکتاپ', 'sodino'); ?></option>
<option value="mobile" <?php selected($banner->device_target, 'mobile'); ?>><?php _e('موبایل', 'sodino'); ?></option>
</select>
</div>
</div>
</div>
<div class="bg-white rounded-2xl border border-gray-200 p-6 shadow-sm space-y-6">
<div>
<h2 class="text-xl font-semibold text-gray-900"><?php _e('تنظیمات پیشرفته', 'sodino'); ?></h2>
<p class="mt-2 text-sm text-gray-500"><?php _e('اولویت و وضعیت نمایش بنر را تنظیم کنید.', 'sodino'); ?></p>
</div>
<div class="grid gap-6 lg:grid-cols-3">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2" for="priority"><?php _e('اولویت', 'sodino'); ?></label>
<input type="number" name="priority" id="priority" value="<?php echo esc_attr($banner->priority); ?>" min="1" class="w-full rounded-2xl border border-gray-300 bg-white px-4 py-3 text-gray-700 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-100">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2" for="impressions"><?php _e('نمایش‌ها', 'sodino'); ?></label>
<input type="number" id="impressions" value="<?php echo esc_attr($banner->impressions); ?>" disabled class="w-full rounded-2xl border border-gray-300 bg-gray-100 px-4 py-3 text-gray-700 shadow-sm">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2" for="clicks"><?php _e('کلیک‌ها', 'sodino'); ?></label>
<input type="number" id="clicks" value="<?php echo esc_attr($banner->clicks); ?>" disabled class="w-full rounded-2xl border border-gray-300 bg-gray-100 px-4 py-3 text-gray-700 shadow-sm">
</div>
</div>
<div class="flex items-center gap-3">
<label class="inline-flex items-center gap-3 rounded-2xl border border-gray-300 bg-white px-4 py-3 cursor-pointer hover:border-blue-300">
<input type="checkbox" name="status" value="1" <?php checked($banner->status, 1); ?> class="h-4 w-4 text-blue-600">
<span class="text-sm text-gray-700"><?php _e('فعال باشد', 'sodino'); ?></span>
</label>
</div>
</div>
<div class="bg-white rounded-2xl border border-gray-200 p-6 shadow-sm text-right">
<button type="submit" class="inline-flex items-center justify-center rounded-2xl bg-blue-600 px-6 py-3 text-sm font-semibold text-white shadow-sm hover:bg-blue-700 transition-colors">
<?php echo $banner->id ? __('به‌روزرسانی بنر', 'sodino') : __('ذخیره بنر', 'sodino'); ?>
</button>
</div>
</form>
</main>
</div>
</div>
</div>

View File

@@ -0,0 +1,66 @@
<?php
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
$current_page = sanitize_text_field($_GET['page'] ?? 'sodino-banners');
?>
<div id="sodino-app" class="min-h-screen bg-gray-50" dir="rtl">
<div class="bg-white border-b border-gray-200 shadow-sm">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div class="flex items-center justify-between gap-4">
<div>
<h1 class="text-3xl font-bold text-gray-900"><?php _e('بنرهای هوشمند', 'sodino'); ?></h1>
<p class="mt-1 text-sm text-gray-500"><?php _e('مدیریت بنرهای زمان‌بندی شده و هدفمند سایت.', 'sodino'); ?></p>
</div>
<a href="<?php echo admin_url('admin.php?page=sodino-add-banner'); ?>" class="inline-flex items-center gap-2 rounded-full bg-blue-600 px-5 py-3 text-sm font-semibold text-white shadow-sm hover:bg-blue-700">
<?php _e('افزودن بنر جدید', 'sodino'); ?>
</a>
</div>
</div>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="grid gap-8 lg:grid-cols-[280px_1fr]">
<aside class="bg-white rounded-2xl border border-gray-200 p-6 shadow-sm">
<h2 class="text-lg font-semibold text-gray-900 mb-4"><?php _e('منوی سودینو', 'sodino'); ?></h2>
<nav class="space-y-2">
<a href="<?php echo admin_url('admin.php?page=sodino-dashboard'); ?>" class="block px-3 py-2 rounded-xl text-sm font-medium <?php echo $current_page === 'sodino-dashboard' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('داشبورد', 'sodino'); ?>
</a>
<a href="<?php echo admin_url('admin.php?page=sodino-rules'); ?>" class="block px-3 py-2 rounded-xl text-sm font-medium <?php echo $current_page === 'sodino-rules' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('قوانین', 'sodino'); ?>
</a>
<a href="<?php echo admin_url('admin.php?page=sodino-banners'); ?>" class="block px-3 py-2 rounded-xl text-sm font-medium <?php echo $current_page === 'sodino-banners' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('بنرهای هوشمند', 'sodino'); ?>
</a>
<a href="<?php echo admin_url('admin.php?page=sodino-settings'); ?>" class="block px-3 py-2 rounded-xl text-sm font-medium <?php echo $current_page === 'sodino-settings' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('تنظیمات', 'sodino'); ?>
</a>
</nav>
</aside>
<main class="space-y-6">
<div class="bg-white rounded-2xl border border-gray-200 p-6 shadow-sm">
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<h2 class="text-xl font-semibold text-gray-900"><?php _e('لیست بنرها', 'sodino'); ?></h2>
<p class="mt-1 text-sm text-gray-500"><?php _e('نمایش، ویرایش، فعال/غیرفعال و حذف گروهی بنرها.', 'sodino'); ?></p>
</div>
<span class="inline-flex items-center rounded-full bg-blue-50 px-4 py-2 text-sm font-medium text-blue-700">
<?php _e('حداکثر ۲ بنر همزمان نمایش داده می‌شوند', 'sodino'); ?>
</span>
</div>
</div>
<div class="bg-white rounded-2xl border border-gray-200 p-6 shadow-sm">
<form method="post">
<?php wp_nonce_field('bulk-sodino_banners'); ?>
<?php $bannerTable->display(); ?>
</form>
</div>
</main>
</div>
</div>
</div>

View File

@@ -3,8 +3,10 @@ namespace Sodino\Controllers;
use Sodino\Repositories\RuleRepository;
use Sodino\Repositories\UpsellRepository;
use Sodino\Repositories\BannerRepository;
use Sodino\Models\Rule;
use Sodino\Models\Upsell;
use Sodino\Models\Banner;
/**
* Admin Controller
@@ -12,10 +14,12 @@ use Sodino\Models\Upsell;
class AdminController {
private $ruleRepository;
private $upsellRepository;
private $bannerRepository;
public function __construct(RuleRepository $ruleRepository, UpsellRepository $upsellRepository) {
public function __construct(RuleRepository $ruleRepository, UpsellRepository $upsellRepository, BannerRepository $bannerRepository) {
$this->ruleRepository = $ruleRepository;
$this->upsellRepository = $upsellRepository;
$this->bannerRepository = $bannerRepository;
}
/**
@@ -68,6 +72,24 @@ class AdminController {
[$this, 'addUpsellPage']
);
add_submenu_page(
'sodino-rules',
__('بنرهای هوشمند', 'sodino'),
__('بنرهای هوشمند', 'sodino'),
'manage_options',
'sodino-banners',
[$this, 'bannersPage']
);
add_submenu_page(
'sodino-rules',
__('افزودن بنر', 'sodino'),
__('افزودن بنر', 'sodino'),
'manage_options',
'sodino-add-banner',
[$this, 'addBannerPage']
);
add_submenu_page(
'sodino-rules',
__('قیمت رقبا (به‌زودی)', 'sodino'),
@@ -192,6 +214,29 @@ class AdminController {
}
}
/**
* Banners list page
*/
public function bannersPage() {
$this->listBannersPage();
}
/**
* Add or edit banner page
*/
public function addBannerPage() {
if (isset($_GET['action']) && $_GET['action'] === 'edit') {
return $this->editBannerPage();
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$this->saveBanner();
} else {
$banner = new Banner();
include SODINO_PLUGIN_DIR . 'admin/views/banner-form.php';
}
}
/**
* Competitor price page
*/
@@ -206,6 +251,28 @@ class AdminController {
include SODINO_PLUGIN_DIR . 'admin/views/upsell-list.php';
}
private function listBannersPage() {
require_once SODINO_PLUGIN_DIR . 'admin/class-banner-list-table.php';
$bannerTable = new \Sodino_Banner_List_Table($this->bannerRepository);
$bannerTable->prepare_items();
include SODINO_PLUGIN_DIR . 'admin/views/banner-list.php';
}
private function editBannerPage() {
$id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
$banner = $this->bannerRepository->getById($id);
if (!$banner) {
wp_die(__('بنر پیدا نشد', 'sodino'));
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$this->saveBanner($banner);
} else {
include SODINO_PLUGIN_DIR . 'admin/views/banner-form.php';
}
}
private function editUpsellPage() {
$id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
$upsell = $this->upsellRepository->getById($id);
@@ -245,6 +312,34 @@ class AdminController {
exit;
}
private function saveBanner($banner = null) {
if (!isset($_POST['sodino_banner_nonce']) || !wp_verify_nonce($_POST['sodino_banner_nonce'], 'sodino_save_banner')) {
wp_die(__('خطای امنیتی رخ داد.', 'sodino'));
}
if (!$banner) {
$banner = new Banner();
}
$banner->title = sanitize_text_field($_POST['title'] ?? '');
$banner->content_type = sanitize_text_field($_POST['content_type'] ?? 'image');
$banner->content_value = isset($_POST['content_value']) ? wp_kses_post($_POST['content_value']) : '';
$banner->link_url = esc_url_raw($_POST['link_url'] ?? '');
$banner->position = sanitize_text_field($_POST['position'] ?? 'top');
$banner->display_type = sanitize_text_field($_POST['display_type'] ?? 'inline');
$banner->start_time = sanitize_text_field($_POST['start_time'] ?? '');
$banner->end_time = sanitize_text_field($_POST['end_time'] ?? '');
$banner->user_target = sanitize_text_field($_POST['user_target'] ?? 'all');
$banner->device_target = sanitize_text_field($_POST['device_target'] ?? 'all');
$banner->priority = max(1, intval($_POST['priority'] ?? 10));
$banner->status = isset($_POST['status']) ? 1 : 0;
$this->bannerRepository->save($banner);
wp_safe_redirect(admin_url('admin.php?page=sodino-banners'));
exit;
}
public function handleUpsellActions() {
if (!isset($_GET['_wpnonce']) || !in_array($_GET['action'], ['delete_upsell', 'toggle_upsell_status'], true) || !wp_verify_nonce($_GET['_wpnonce'], $_GET['action'])) {
return;
@@ -272,6 +367,33 @@ class AdminController {
}
}
public function handleBannerActions() {
if (!isset($_GET['_wpnonce']) || !in_array($_GET['action'], ['delete_banner', 'toggle_banner_status'], true) || !wp_verify_nonce($_GET['_wpnonce'], $_GET['action'])) {
return;
}
$id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
if (!$id) {
return;
}
if ($_GET['action'] === 'delete_banner') {
$this->bannerRepository->delete($id);
wp_safe_redirect(admin_url('admin.php?page=sodino-banners'));
exit;
}
if ($_GET['action'] === 'toggle_banner_status') {
$banner = $this->bannerRepository->getById($id);
if ($banner) {
$banner->status = $banner->status ? 0 : 1;
$this->bannerRepository->save($banner);
}
wp_safe_redirect(admin_url('admin.php?page=sodino-banners'));
exit;
}
}
public function searchProductsAjax() {
if (!check_ajax_referer('sodino_search_products', 'security', false)) {
wp_send_json([]);

64
app/Models/Banner.php Normal file
View File

@@ -0,0 +1,64 @@
<?php
namespace Sodino\Models;
/**
* Banner Model
*/
class Banner {
public $id;
public $title;
public $content_type;
public $content_value;
public $link_url;
public $position;
public $display_type;
public $start_time;
public $end_time;
public $user_target;
public $device_target;
public $priority;
public $status;
public $impressions;
public $clicks;
public $created_at;
public function __construct($data = []) {
$this->id = $data['id'] ?? null;
$this->title = $data['title'] ?? '';
$this->content_type = $data['content_type'] ?? 'image';
$this->content_value = $data['content_value'] ?? '';
$this->link_url = $data['link_url'] ?? '';
$this->position = $data['position'] ?? 'top';
$this->display_type = $data['display_type'] ?? 'inline';
$this->start_time = $data['start_time'] ?? null;
$this->end_time = $data['end_time'] ?? null;
$this->user_target = $data['user_target'] ?? 'all';
$this->device_target = $data['device_target'] ?? 'all';
$this->priority = isset($data['priority']) ? (int) $data['priority'] : 10;
$this->status = isset($data['status']) ? (int) $data['status'] : 1;
$this->impressions = isset($data['impressions']) ? (int) $data['impressions'] : 0;
$this->clicks = isset($data['clicks']) ? (int) $data['clicks'] : 0;
$this->created_at = $data['created_at'] ?? null;
}
public function toArray() {
return [
'id' => $this->id,
'title' => $this->title,
'content_type' => $this->content_type,
'content_value' => $this->content_value,
'link_url' => $this->link_url,
'position' => $this->position,
'display_type' => $this->display_type,
'start_time' => $this->start_time,
'end_time' => $this->end_time,
'user_target' => $this->user_target,
'device_target' => $this->device_target,
'priority' => $this->priority,
'status' => $this->status,
'impressions' => $this->impressions,
'clicks' => $this->clicks,
'created_at' => $this->created_at,
];
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace Sodino\Repositories;
use Sodino\Models\Banner;
/**
* Banner Repository
*/
class BannerRepository {
private $table_name;
public function __construct() {
global $wpdb;
$this->table_name = $wpdb->prefix . 'sodino_banners';
}
public function getAll() {
global $wpdb;
$results = $wpdb->get_results("SELECT * FROM {$this->table_name} ORDER BY priority DESC, id ASC", ARRAY_A);
$banners = [];
foreach ($results as $result) {
$banners[] = new Banner($result);
}
return $banners;
}
public function getById($id) {
global $wpdb;
$result = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$this->table_name} WHERE id = %d", $id), ARRAY_A);
return $result ? new Banner($result) : null;
}
public function getEnabled() {
global $wpdb;
$results = $wpdb->get_results("SELECT * FROM {$this->table_name} WHERE status = 1 ORDER BY priority DESC, id ASC", ARRAY_A);
$banners = [];
foreach ($results as $result) {
$banners[] = new Banner($result);
}
return $banners;
}
public function save(Banner $banner) {
global $wpdb;
$data = $banner->toArray();
unset($data['id'], $data['created_at']);
if ($banner->id) {
$wpdb->update($this->table_name, $data, ['id' => $banner->id]);
$this->clearCache();
return $banner->id;
}
$wpdb->insert($this->table_name, $data);
$this->clearCache();
return $wpdb->insert_id;
}
public function delete($id) {
global $wpdb;
$result = $wpdb->delete($this->table_name, ['id' => $id]);
$this->clearCache();
return $result;
}
public function incrementImpression($id) {
global $wpdb;
$wpdb->query($wpdb->prepare("UPDATE {$this->table_name} SET impressions = impressions + 1 WHERE id = %d", $id));
$this->clearCache();
}
public function incrementClick($id) {
global $wpdb;
$wpdb->query($wpdb->prepare("UPDATE {$this->table_name} SET clicks = clicks + 1 WHERE id = %d", $id));
$this->clearCache();
}
public function clearCache() {
wp_cache_flush();
}
}

View File

@@ -0,0 +1,111 @@
<?php
namespace Sodino\Services;
use Sodino\Models\Banner;
use Sodino\Repositories\BannerRepository;
/**
* Banner Service
*/
class BannerService {
private $repository;
public function __construct(BannerRepository $repository) {
$this->repository = $repository;
}
public function getActiveBanners(array $context = []) {
$position = $context['position'] ?? '';
$cache_key = $this->getCacheKey($position, $context);
$banners = wp_cache_get($cache_key, 'sodino_banners');
if ($banners === false) {
$banners = array_filter($this->repository->getEnabled(), function (Banner $banner) use ($context) {
return $this->filterByPosition($banner, $context)
&& $this->filterByTime($banner)
&& $this->filterByUserType($banner)
&& $this->filterByDevice($banner);
});
usort($banners, function (Banner $a, Banner $b) {
return $b->priority <=> $a->priority;
});
wp_cache_set($cache_key, $banners, 'sodino_banners', MINUTE_IN_SECONDS);
}
$limit = isset($context['limit']) ? (int) $context['limit'] : 1;
return $limit > 0 ? array_slice($banners, 0, $limit) : $banners;
}
public function filterByTime(Banner $banner) {
$now = current_time('timestamp');
if (!empty($banner->start_time)) {
$start = strtotime($banner->start_time);
if ($start !== false && $now < $start) {
return false;
}
}
if (!empty($banner->end_time)) {
$end = strtotime($banner->end_time);
if ($end !== false && $now > $end) {
return false;
}
}
return true;
}
public function filterByUserType(Banner $banner) {
if ($banner->user_target === 'all') {
return true;
}
$userType = is_user_logged_in() ? 'returning' : 'new';
return $banner->user_target === $userType;
}
public function filterByDevice(Banner $banner) {
if ($banner->device_target === 'all') {
return true;
}
$device = wp_is_mobile() ? 'mobile' : 'desktop';
return $banner->device_target === $device;
}
public function filterByPosition(Banner $banner, array $context) {
if (empty($context['position'])) {
return false;
}
if ($banner->position !== $context['position']) {
return false;
}
if ($banner->position === 'product_page' && !is_singular('product')) {
return false;
}
if ($banner->position === 'cart' && !is_cart()) {
return false;
}
return true;
}
public function increaseImpression($bannerId) {
$this->repository->incrementImpression($bannerId);
}
public function increaseClick($bannerId) {
$this->repository->incrementClick($bannerId);
}
private function getCacheKey($position, array $context) {
return 'sodino_active_banners_' . md5($position . '|' . serialize($context));
}
}

View File

@@ -69,11 +69,34 @@ function sodino_create_tables() {
PRIMARY KEY (id)
) $charset_collate;";
// Banner table
$banner_table = $wpdb->prefix . 'sodino_banners';
$banner_sql = "CREATE TABLE $banner_table (
id mediumint(9) NOT NULL AUTO_INCREMENT,
title varchar(255) NOT NULL,
content_type varchar(50) NOT NULL DEFAULT 'image',
content_value longtext NOT NULL,
link_url varchar(255) DEFAULT NULL,
position varchar(50) NOT NULL DEFAULT 'top',
display_type varchar(50) NOT NULL DEFAULT 'inline',
start_time datetime DEFAULT NULL,
end_time datetime DEFAULT NULL,
user_target varchar(50) NOT NULL DEFAULT 'all',
device_target varchar(50) NOT NULL DEFAULT 'all',
priority int(11) NOT NULL DEFAULT 10,
status tinyint(1) NOT NULL DEFAULT 1,
impressions bigint(20) NOT NULL DEFAULT 0,
clicks bigint(20) NOT NULL DEFAULT 0,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($rules_sql);
dbDelta($events_sql);
dbDelta($upsell_sql);
dbDelta($banner_sql);
// Add version option
add_option('sodino_db_version', '1.2');
update_option('sodino_db_version', '1.3');
}

View File

@@ -0,0 +1,112 @@
.sodino-banner {
direction: rtl;
position: relative;
z-index: 9999;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
.sodino-banner-wrap {
margin: 0 auto;
max-width: 1200px;
padding: 18px 22px;
background: #ffffff;
border: 1px solid rgba(148, 163, 184, 0.35);
border-radius: 1rem;
box-shadow: 0 18px 35px rgba(15, 23, 42, 0.08);
}
.sodino-banner-top {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
width: calc(100% - 32px);
max-width: 1100px;
}
.sodino-banner-bottom {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
width: calc(100% - 32px);
max-width: 1100px;
}
.sodino-banner-floating_bar {
position: fixed;
left: 50%;
transform: translateX(-50%);
width: calc(100% - 32px);
max-width: 1100px;
}
.sodino-banner-position-top.sodino-banner-floating_bar,
.sodino-banner-position-middle.sodino-banner-floating_bar {
top: 20px;
}
.sodino-banner-position-bottom.sodino-banner-floating_bar,
.sodino-banner-position-cart.sodino-banner-floating_bar,
.sodino-banner-position-product_page.sodino-banner-floating_bar {
bottom: 20px;
}
.sodino-banner-popup {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
max-width: 95%;
width: 680px;
background: #ffffff;
box-shadow: 0 30px 60px rgba(15, 23, 42, 0.12);
}
.sodino-banner-close {
position: absolute;
top: 14px;
right: 16px;
background: rgba(15, 23, 42, 0.06);
border: none;
color: #0f172a;
width: 36px;
height: 36px;
border-radius: 9999px;
font-size: 1.2rem;
cursor: pointer;
}
.sodino-banner-content {
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
flex-wrap: wrap;
}
.sodino-banner-image {
width: 100%;
height: auto;
border-radius: 1rem;
display: block;
}
.sodino-banner-link {
display: inline-block;
}
@media (max-width: 768px) {
.sodino-banner-top,
.sodino-banner-bottom,
.sodino-banner-floating_bar,
.sodino-banner-popup {
width: calc(100% - 20px);
left: 50%;
transform: translateX(-50%);
}
.sodino-banner-popup {
max-width: 95%;
}
}

View File

@@ -0,0 +1,165 @@
<?php
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
use Sodino\Repositories\BannerRepository;
use Sodino\Services\BannerService;
global $sodino_banner_service;
$bannerRepository = new BannerRepository();
$sodino_banner_service = new BannerService($bannerRepository);
add_action('wp_head', 'sodino_render_top_banner', 1);
add_filter('the_content', 'sodino_render_middle_banner');
add_action('wp_footer', 'sodino_render_bottom_banner', 20);
add_action('woocommerce_after_single_product_summary', 'sodino_render_product_banner', 5);
add_action('woocommerce_before_cart', 'sodino_render_cart_banner');
add_action('wp_enqueue_scripts', 'sodino_enqueue_banner_assets');
add_action('wp_ajax_nopriv_sodino_banner_click', 'sodino_handle_banner_click');
add_action('wp_ajax_sodino_banner_click', 'sodino_handle_banner_click');
function sodino_enqueue_banner_assets() {
if (is_admin()) {
return;
}
wp_register_style('sodino-banner-frontend', plugin_dir_url(__FILE__) . '../css/banner-frontend.css', [], SODINO_VERSION);
wp_enqueue_style('sodino-banner-frontend');
wp_register_script('sodino-banner-frontend', plugin_dir_url(__FILE__) . '../js/banner-frontend.js', [], SODINO_VERSION, true);
wp_localize_script('sodino-banner-frontend', 'sodinoBannerFrontend', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('sodino_banner_click'),
]);
wp_enqueue_script('sodino-banner-frontend');
}
function sodino_get_banner_html($banner) {
global $sodino_banner_service;
$html = '';
$content = '';
switch ($banner->content_type) {
case 'image':
$image = esc_url($banner->content_value);
if (!empty($banner->link_url)) {
$content = sprintf('<a href="%s" class="sodino-banner-link" data-banner-id="%d">%s</a>', esc_url($banner->link_url), esc_attr($banner->id), '<img src="' . $image . '" alt="' . esc_attr($banner->title) . '" class="sodino-banner-image" />');
} else {
$content = '<img src="' . $image . '" alt="' . esc_attr($banner->title) . '" class="sodino-banner-image" />';
}
break;
case 'shortcode':
$content = do_shortcode(wp_kses_post($banner->content_value));
break;
case 'html':
default:
$content = wp_kses_post($banner->content_value);
break;
}
$linkAttributes = '';
if (!empty($banner->link_url) && $banner->content_type !== 'image') {
$linkAttributes = sprintf(' data-banner-id="%d" href="%s" class="sodino-banner-link"', esc_attr($banner->id), esc_url($banner->link_url));
}
$closeButton = '<button type="button" class="sodino-banner-close" aria-label="'.esc_attr__('بستن بنر', 'sodino').'">&times;</button>';
$wrapperClass = 'sodino-banner-wrap sodino-banner-' . esc_attr($banner->display_type) . ' sodino-banner-position-' . esc_attr($banner->position);
$style = $banner->display_type === 'popup' ? 'style="display:none;"' : '';
$html .= '<div class="' . $wrapperClass . '" data-banner-id="' . esc_attr($banner->id) . '" ' . $style . '>';
if ($banner->display_type === 'popup' || $banner->display_type === 'floating_bar') {
$html .= $closeButton;
}
$html .= '<div class="sodino-banner-content">';
if ($banner->content_type !== 'image' && !empty($banner->link_url)) {
$html .= '<a' . $linkAttributes . '>' . $content . '</a>';
} else {
$html .= $content;
}
$html .= '</div>';
$html .= '</div>';
return $html;
}
function sodino_render_banner_position($position) {
global $sodino_banner_service;
if (!isset($sodino_banner_service)) {
return '';
}
$banners = $sodino_banner_service->getActiveBanners(['position' => $position, 'limit' => 1]);
if (empty($banners)) {
return '';
}
$banner = reset($banners);
$sodino_banner_service->increaseImpression($banner->id);
return sodino_get_banner_html($banner);
}
function sodino_render_top_banner() {
if (is_admin()) {
return;
}
echo sodino_render_banner_position('top');
}
function sodino_render_middle_banner($content) {
if (is_admin() || !is_singular() || !in_the_loop() || is_feed()) {
return $content;
}
$banner = sodino_render_banner_position('middle');
if (empty($banner)) {
return $content;
}
return $banner . $content;
}
function sodino_render_bottom_banner() {
if (is_admin()) {
return;
}
echo sodino_render_banner_position('bottom');
}
function sodino_render_product_banner() {
if (is_admin()) {
return;
}
echo sodino_render_banner_position('product_page');
}
function sodino_render_cart_banner() {
if (is_admin()) {
return;
}
echo sodino_render_banner_position('cart');
}
function sodino_handle_banner_click() {
if (!isset($_POST['banner_id']) || !isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'sodino_banner_click')) {
wp_send_json_error();
}
$bannerId = intval($_POST['banner_id']);
if (!$bannerId) {
wp_send_json_error();
}
global $sodino_banner_service;
if (!isset($sodino_banner_service)) {
wp_send_json_error();
}
$sodino_banner_service->increaseClick($bannerId);
wp_send_json_success();
}

View File

@@ -0,0 +1,65 @@
(function () {
const closeButtons = document.querySelectorAll('.sodino-banner-close');
const clicked = new Set();
function getCookie(name) {
const value = '; ' + document.cookie;
const parts = value.split('; ' + name + '=');
if (parts.length === 2) {
return parts.pop().split(';').shift();
}
return null;
}
function sendClick(bannerId) {
if (!bannerId || clicked.has(bannerId) || !window.sodinoBannerFrontend) {
return;
}
clicked.add(bannerId);
fetch(window.sodinoBannerFrontend.ajaxUrl, {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body: 'action=sodino_banner_click&banner_id=' + encodeURIComponent(bannerId) + '&security=' + encodeURIComponent(window.sodinoBannerFrontend.nonce),
});
}
document.addEventListener('click', function (event) {
const target = event.target.closest('.sodino-banner-link');
if (target) {
const bannerId = target.dataset.bannerId;
sendClick(bannerId);
}
});
closeButtons.forEach(function (button) {
button.addEventListener('click', function () {
const wrapper = this.closest('.sodino-banner-wrap');
if (!wrapper) {
return;
}
wrapper.style.display = 'none';
const bannerId = wrapper.dataset.bannerId;
if (bannerId) {
document.cookie = 'sodino_banner_' + bannerId + '=hidden; path=/; max-age=' + 60 * 60 * 24;
}
});
});
document.querySelectorAll('.sodino-banner-wrap').forEach(function (banner) {
const bannerId = banner.dataset.bannerId;
if (getCookie('sodino_banner_' + bannerId) === 'hidden') {
banner.style.display = 'none';
}
if (banner.classList.contains('sodino-banner-popup')) {
setTimeout(function () {
if (getCookie('sodino_banner_' + bannerId) !== 'hidden') {
banner.style.display = 'block';
}
}, 2000);
}
});
})();

View File

@@ -89,6 +89,7 @@ function sodino_init() {
require_once SODINO_PLUGIN_DIR . 'public/hooks/pricing-hooks.php';
require_once SODINO_PLUGIN_DIR . 'public/hooks/analytics-hooks.php';
require_once SODINO_PLUGIN_DIR . 'public/hooks/upsell-hooks.php';
require_once SODINO_PLUGIN_DIR . 'public/hooks/banner-hooks.php';
// Schedule analytics aggregation if needed
sodino_schedule_analytics();