diff --git a/admin/admin.php b/admin/admin.php
index eceba5c..eae9802 100644
--- a/admin/admin.php
+++ b/admin/admin.php
@@ -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']);
diff --git a/admin/class-banner-list-table.php b/admin/class-banner-list-table.php
new file mode 100644
index 0000000..64d7e9c
--- /dev/null
+++ b/admin/class-banner-list-table.php
@@ -0,0 +1,142 @@
+ 'sodino_banner',
+ 'plural' => 'sodino_banners',
+ 'ajax' => false,
+ ]);
+
+ $this->repository = $repository;
+ }
+
+ public function get_columns() {
+ return [
+ 'cb' => '',
+ '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('', $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('%s', 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
%s', 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('
',
+ __('نمایش', '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(
+ '%s | %s | %s',
+ 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);
+ }
+ }
+ }
+ }
+}
diff --git a/admin/css/admin.css b/admin/css/admin.css
index ee219ec..50c73be 100644
--- a/admin/css/admin.css
+++ b/admin/css/admin.css
@@ -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;
+}
diff --git a/admin/js/banner-admin.js b/admin/js/banner-admin.js
new file mode 100644
index 0000000..8471907
--- /dev/null
+++ b/admin/js/banner-admin.js
@@ -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);
+ }
+ });
+ });
+ }
+})();
diff --git a/admin/views/banner-form.php b/admin/views/banner-form.php
new file mode 100644
index 0000000..6cbab9a
--- /dev/null
+++ b/admin/views/banner-form.php
@@ -0,0 +1,219 @@
+
+
+
+
+
+
+
id ? __('ویرایش بنر', 'sodino') : __('افزودن بنر جدید', 'sodino'); ?>
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/views/banner-list.php b/admin/views/banner-list.php
new file mode 100644
index 0000000..c7a7635
--- /dev/null
+++ b/admin/views/banner-list.php
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/Controllers/AdminController.php b/app/Controllers/AdminController.php
index 013f83f..613dae2 100644
--- a/app/Controllers/AdminController.php
+++ b/app/Controllers/AdminController.php
@@ -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([]);
diff --git a/app/Models/Banner.php b/app/Models/Banner.php
new file mode 100644
index 0000000..3c52fc6
--- /dev/null
+++ b/app/Models/Banner.php
@@ -0,0 +1,64 @@
+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,
+ ];
+ }
+}
diff --git a/app/Repositories/BannerRepository.php b/app/Repositories/BannerRepository.php
new file mode 100644
index 0000000..07e231d
--- /dev/null
+++ b/app/Repositories/BannerRepository.php
@@ -0,0 +1,81 @@
+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();
+ }
+}
diff --git a/app/Services/BannerService.php b/app/Services/BannerService.php
new file mode 100644
index 0000000..3f915a8
--- /dev/null
+++ b/app/Services/BannerService.php
@@ -0,0 +1,111 @@
+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));
+ }
+}
diff --git a/database/migrations.php b/database/migrations.php
index ac0d0ac..eada823 100644
--- a/database/migrations.php
+++ b/database/migrations.php
@@ -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');
}
\ No newline at end of file
diff --git a/public/css/banner-frontend.css b/public/css/banner-frontend.css
new file mode 100644
index 0000000..fcd1207
--- /dev/null
+++ b/public/css/banner-frontend.css
@@ -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%;
+ }
+}
diff --git a/public/hooks/banner-hooks.php b/public/hooks/banner-hooks.php
new file mode 100644
index 0000000..f7d7f61
--- /dev/null
+++ b/public/hooks/banner-hooks.php
@@ -0,0 +1,165 @@
+ 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('%s', esc_url($banner->link_url), esc_attr($banner->id), '
');
+ } else {
+ $content = '
';
+ }
+ 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 = '';
+ $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 .= '';
+ if ($banner->display_type === 'popup' || $banner->display_type === 'floating_bar') {
+ $html .= $closeButton;
+ }
+ $html .= '
';
+ if ($banner->content_type !== 'image' && !empty($banner->link_url)) {
+ $html .= '
' . $content . '';
+ } else {
+ $html .= $content;
+ }
+ $html .= '
';
+ $html .= '
';
+
+ 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();
+}
diff --git a/public/js/banner-frontend.js b/public/js/banner-frontend.js
new file mode 100644
index 0000000..5f87fa1
--- /dev/null
+++ b/public/js/banner-frontend.js
@@ -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);
+ }
+ });
+})();
diff --git a/sodino.php b/sodino.php
index 4be1f07..50d9366 100644
--- a/sodino.php
+++ b/sodino.php
@@ -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();