Some checks failed
ci/woodpecker/push/ci Pipeline failed
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
97 lines
3.1 KiB
PHP
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']
|
|
);
|
|
}
|
|
}
|