Commit 0183a9af authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat: ads module — direct image upload for ad graphics

- Form now supports file upload (enctype multipart/form-data)
- Accepts PNG, JPG, WebP, GIF up to 10MB
- Uploads to Supabase Storage (assets bucket, ads/ prefix)
- Shows current image preview in edit mode
- Falls back to URL field if no file uploaded
- List view shows image thumbnails
- Added splash and between_games ad slots
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 4e6704c8
......@@ -49,6 +49,12 @@ class AdsController
return;
}
$imageUrl = trim($_POST['image_url'] ?? '');
if (!empty($_FILES['ad_image']) && $_FILES['ad_image']['error'] === UPLOAD_ERR_OK) {
$uploaded = $this->uploadAdImage($_FILES['ad_image']);
if ($uploaded) $imageUrl = $uploaded;
}
$data = [
'name' => trim($_POST['name']),
'name_ar' => trim($_POST['name_ar'] ?? ''),
......@@ -61,7 +67,7 @@ class AdsController
'ends_at' => $_POST['ends_at'] ?: null,
'budget_total' => (int)($_POST['budget_total'] ?? 0),
'budget_daily' => (int)($_POST['budget_daily'] ?? 0),
'image_url' => trim($_POST['image_url'] ?? ''),
'image_url' => $imageUrl,
'click_url' => trim($_POST['click_url'] ?? ''),
'priority' => (int)($_POST['priority'] ?? 0),
'created_by' => Auth::user()['id'] ?? null,
......@@ -98,6 +104,12 @@ class AdsController
Auth::requireCsrf();
$id = $params['id'];
$imageUrl = trim($_POST['image_url'] ?? '');
if (!empty($_FILES['ad_image']) && $_FILES['ad_image']['error'] === UPLOAD_ERR_OK) {
$uploaded = $this->uploadAdImage($_FILES['ad_image']);
if ($uploaded) $imageUrl = $uploaded;
}
$data = [
'name' => trim($_POST['name']),
'name_ar' => trim($_POST['name_ar'] ?? ''),
......@@ -109,7 +121,7 @@ class AdsController
'ends_at' => $_POST['ends_at'] ?: null,
'budget_total' => (int)($_POST['budget_total'] ?? 0),
'budget_daily' => (int)($_POST['budget_daily'] ?? 0),
'image_url' => trim($_POST['image_url'] ?? ''),
'image_url' => $imageUrl,
'click_url' => trim($_POST['click_url'] ?? ''),
'priority' => (int)($_POST['priority'] ?? 0),
'updated_at' => date('c'),
......@@ -140,4 +152,28 @@ class AdsController
AuditLog::log('delete', 'ad_campaign', $params['id']);
Response::success('تم حذف الحملة', '/ads');
}
private function uploadAdImage(array $file): ?string
{
$storage = SupabaseStorage::getInstance();
$allowedMimes = ['image/png', 'image/jpeg', 'image/jpg', 'image/webp', 'image/gif'];
$maxSize = 10 * 1024 * 1024;
$error = $storage->validateFile($file, $allowedMimes, $maxSize);
if ($error) {
Response::error($error, $_SERVER['HTTP_REFERER'] ?? '/ads');
return null;
}
$path = $storage->generatePath('ads', $file['name']);
try {
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
return $storage->upload('assets', $path, $file['tmp_name'], $mime);
} catch (\Throwable $e) {
Response::error('فشل رفع الصورة: ' . $e->getMessage(), $_SERVER['HTTP_REFERER'] ?? '/ads');
return null;
}
}
}
......@@ -6,7 +6,7 @@
</div>
</div>
<div class="card max-w-lg">
<form method="POST" action="<?= $isEdit ? "/ads/{$campaign['id']}/update" : '/ads/store' ?>" data-validate>
<form method="POST" action="<?= $isEdit ? "/ads/{$campaign['id']}/update" : '/ads/store' ?>" enctype="multipart/form-data" data-validate>
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<div class="grid grid-2 gap-4">
<div class="form-group"><label class="form-label">اسم الحملة (English) *</label><input type="text" name="name" class="form-input" value="<?= View::e($campaign['name'] ?? '') ?>" required dir="ltr"><span class="form-error"></span></div>
......@@ -25,7 +25,7 @@
<label class="form-label">المواضع المستهدفة</label>
<select name="target_slots[]" class="form-select" multiple>
<?php
$slots = ['banner_top' => 'بانر علوي', 'banner_bottom' => 'بانر سفلي', 'interstitial' => 'بيني', 'sidebar' => 'جانبي', 'in_game' => 'داخل اللعبة', 'reward_video' => 'فيديو مكافأة'];
$slots = ['banner_top' => 'بانر علوي', 'banner_bottom' => 'بانر سفلي', 'interstitial' => 'بيني (ملء الشاشة)', 'sidebar' => 'جانبي', 'in_game' => 'داخل اللعبة', 'reward_video' => 'فيديو مكافأة', 'splash' => 'شاشة التحميل', 'between_games' => 'بين المباريات'];
$selectedSlots = $campaign['target_slots'] ?? [];
foreach ($slots as $val => $label): ?>
<option value="<?= $val ?>" <?= in_array($val, (array)$selectedSlots) ? 'selected' : '' ?>><?= $label ?></option>
......@@ -43,7 +43,25 @@
</select>
</div>
<?php endif; ?>
<div class="form-group"><label class="form-label">رابط الصورة (Banner)</label><input type="url" name="image_url" class="form-input" value="<?= View::e($campaign['image_url'] ?? '') ?>" dir="ltr" placeholder="https://..."></div>
<!-- Ad Graphics Upload -->
<div class="form-group">
<label class="form-label">صورة الإعلان (رفع مباشر)</label>
<?php if (!empty($campaign['image_url'])): ?>
<div style="margin-bottom:10px;padding:12px;background:#1a1a2e;border-radius:8px;border:1px solid rgba(255,255,255,0.08);">
<img src="<?= View::e($campaign['image_url']) ?>" style="max-width:100%;max-height:200px;border-radius:6px;display:block;margin:0 auto;">
<div style="text-align:center;margin-top:8px;font-size:12px;color:#94a3b8;">الصورة الحالية</div>
</div>
<?php endif; ?>
<input type="file" name="ad_image" class="form-input" accept="image/png,image/jpeg,image/webp,image/gif" style="padding:10px;">
<div style="font-size:11px;color:#64748b;margin-top:4px;">PNG, JPG, WebP, أو GIF — الحد الأقصى 10 ميجابايت — الأبعاد المقترحة: بانر 728×90, بيني 390×844, مربع 300×300</div>
</div>
<div class="form-group">
<label class="form-label">أو رابط الصورة الخارجي</label>
<input type="url" name="image_url" class="form-input" value="<?= View::e($campaign['image_url'] ?? '') ?>" dir="ltr" placeholder="https://... (اختياري إذا رفعت صورة أعلاه)">
</div>
<div class="form-group"><label class="form-label">رابط النقر (Click URL)</label><input type="url" name="click_url" class="form-input" value="<?= View::e($campaign['click_url'] ?? '') ?>" dir="ltr" placeholder="https://..."></div>
<div class="form-group"><label class="form-label">الأولوية (أعلى = يظهر أولاً)</label><input type="number" name="priority" class="form-input" value="<?= $campaign['priority'] ?? 0 ?>" min="0" max="100"></div>
<div class="grid grid-2 gap-4">
......
......@@ -21,8 +21,8 @@
<table class="data-table">
<thead>
<tr>
<th>صورة</th>
<th>الحملة</th>
<th>المعلن</th>
<th>الموضع</th>
<th>الحالة</th>
<th>المشاهدات</th>
......@@ -34,8 +34,17 @@
<tbody>
<?php foreach ($campaigns as $c): ?>
<tr>
<td class="font-medium"><?= View::e($c['name'] ?? '') ?></td>
<td class="text-secondary"><?= View::e($c['name_ar'] ?? '') ?></td>
<td>
<?php if (!empty($c['image_url'])): ?>
<img src="<?= View::e($c['image_url']) ?>" style="width:60px;height:40px;object-fit:cover;border-radius:4px;border:1px solid rgba(255,255,255,0.1);">
<?php else: ?>
<div style="width:60px;height:40px;background:#1a1a2e;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:16px;opacity:0.4;">🖼️</div>
<?php endif; ?>
</td>
<td class="font-medium">
<?= View::e($c['name'] ?? '') ?>
<?php if (!empty($c['name_ar'])): ?><br><span class="text-secondary text-xs"><?= View::e($c['name_ar']) ?></span><?php endif; ?>
</td>
<td><span class="badge badge-info"><?= View::e($c['campaign_type'] ?? 'cpm') ?></span></td>
<td>
<?php $sb = ['active' => 'success', 'paused' => 'warning', 'draft' => 'default', 'completed' => 'info', 'expired' => 'danger']; ?>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment