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

@@ -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));
}
}