Files
hub/app/Http/Controllers/BannerAdController.php
kelly bcbfdd3c91
Some checks failed
ci/woodpecker/push/ci Pipeline failed
feat: add banner ad system with zones, scheduling, and analytics
Banner Ad System:
- BannerAd model with scheduling (starts_at, ends_at)
- BannerAdEvent model for impression/click tracking
- BannerAdDailyStat model for analytics rollups
- BannerAdZone enum (6 zones: hero, leaderboard, sidebar, inline, brand, deals)
- BannerAdStatus enum (draft, active, scheduled, paused, expired)

Service & Controller:
- BannerAdService with weighted random rotation, caching
- BannerAdController for click tracking and image serving
- Routes for /ads/click, /ads/impression, /images/banner-ad

Filament Admin:
- Full CRUD resource at /admin/banner-ads
- Image upload to MinIO
- Status/zone filters
- Analytics display (impressions, clicks, CTR)

Display Components:
- <x-banner-ad zone="..." /> Blade component
- Automatic impression tracking
- Click tracking via redirect
- Sponsored badge overlay

View Placements:
- Marketplace homepage: leaderboard + sidebar
- Brand page: banner below breadcrumbs
- Deals page: hero banner

Background Jobs:
- UpdateBannerAdStatuses: activate scheduled, expire ended (every minute)
- RollupBannerAdStats: daily aggregation + event cleanup
2025-12-17 22:18:27 -07:00

97 lines
3.1 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\BannerAd;
use App\Services\BannerAdService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Intervention\Image\Drivers\Gd\Driver;
use Intervention\Image\ImageManager;
class BannerAdController extends Controller
{
public function __construct(
protected BannerAdService $bannerAdService
) {}
/**
* Handle click tracking and redirect
* URL: /ads/click/{bannerAd}
*/
public function click(Request $request, BannerAd $bannerAd)
{
$this->bannerAdService->recordClick($bannerAd, [
'business_id' => auth()->user()?->businesses->first()?->id,
'user_id' => auth()->id(),
'session_id' => session()->getId(),
'page_url' => $request->header('referer'),
]);
return redirect()->away($bannerAd->cta_url);
}
/**
* Track impression via AJAX (for lazy-loaded ads)
* URL: POST /ads/impression/{bannerAd}
*/
public function impression(Request $request, BannerAd $bannerAd)
{
$this->bannerAdService->recordImpression($bannerAd, [
'business_id' => auth()->user()?->businesses->first()?->id,
'user_id' => auth()->id(),
'session_id' => session()->getId(),
]);
return response()->json(['success' => true]);
}
/**
* Serve banner ad image at specific width
* URL: /images/banner-ad/{bannerAd}/{width?}
*/
public function image(BannerAd $bannerAd, ?int $width = null)
{
if (! $bannerAd->image_path || ! Storage::exists($bannerAd->image_path)) {
abort(404);
}
// Return original if no width specified
if (! $width) {
$contents = Storage::get($bannerAd->image_path);
$mimeType = Storage::mimeType($bannerAd->image_path);
return response($contents)
->header('Content-Type', $mimeType)
->header('Cache-Control', 'public, max-age=86400');
}
// Generate and cache resized version
$ext = pathinfo($bannerAd->image_path, PATHINFO_EXTENSION);
$thumbnailName = "banner-ad-{$bannerAd->id}-{$width}w.{$ext}";
$thumbnailPath = "banner-ads/cache/{$thumbnailName}";
if (! Storage::disk('local')->exists($thumbnailPath)) {
$originalContents = Storage::get($bannerAd->image_path);
$manager = new ImageManager(new Driver);
$image = $manager->read($originalContents);
$image->scale(width: $width);
if (! Storage::disk('local')->exists('banner-ads/cache')) {
Storage::disk('local')->makeDirectory('banner-ads/cache');
}
$encoded = $ext === 'png' ? $image->toPng() : $image->toJpeg(quality: 90);
Storage::disk('local')->put($thumbnailPath, $encoded);
}
$mimeType = $ext === 'png' ? 'image/png' : 'image/jpeg';
return response()->file(
storage_path("app/private/{$thumbnailPath}"),
['Content-Type' => $mimeType, 'Cache-Control' => 'public, max-age=86400']
);
}
}