Add Campaigns module for grouping short links.
Deploy Ladill Link / deploy (push) Successful in 1m26s

Let accounts organise links by promo or launch, attach or detach links, and view combined click totals from the sidebar.
This commit is contained in:
isaacclad
2026-07-16 20:43:11 +00:00
parent 32a174c826
commit f45fdcd159
13 changed files with 702 additions and 2 deletions
@@ -0,0 +1,156 @@
<?php
namespace App\Http\Controllers\Link;
use App\Http\Controllers\Controller;
use App\Models\Campaign;
use App\Models\ShortLink;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use Illuminate\View\View;
class CampaignController extends Controller
{
public function index(Request $request): View
{
$account = ladill_account();
$campaigns = Campaign::query()
->where('user_id', $account->id)
->withCount('shortLinks')
->withSum('shortLinks', 'clicks_total')
->latest()
->paginate(20)
->withQueryString();
$base = Campaign::query()->where('user_id', $account->id);
return view('links.campaigns.index', [
'campaigns' => $campaigns,
'campaignCount' => (clone $base)->count(),
'activeCount' => (clone $base)->where('status', Campaign::STATUS_ACTIVE)->count(),
'linksInCampaigns' => ShortLink::query()
->where('user_id', $account->id)
->whereNotNull('campaign_id')
->count(),
]);
}
public function create(): View
{
return view('links.campaigns.create', [
'campaign' => new Campaign(['status' => Campaign::STATUS_ACTIVE]),
]);
}
public function store(Request $request): RedirectResponse
{
$account = ladill_account();
$data = $this->validated($request);
$campaign = Campaign::create([
...$data,
'user_id' => $account->id,
]);
return redirect()
->route('link.campaigns.show', $campaign)
->with('success', 'Campaign created.');
}
public function show(Campaign $campaign): View
{
$this->authorizeCampaign($campaign);
$links = $campaign->shortLinks()->latest()->get();
$availableLinks = ShortLink::query()
->where('user_id', $campaign->user_id)
->where(function ($q) use ($campaign) {
$q->whereNull('campaign_id')->orWhere('campaign_id', $campaign->id);
})
->orderBy('slug')
->get();
return view('links.campaigns.show', [
'campaign' => $campaign,
'links' => $links,
'availableLinks' => $availableLinks,
'totalClicks' => (int) $links->sum('clicks_total'),
]);
}
public function update(Request $request, Campaign $campaign): RedirectResponse
{
$this->authorizeCampaign($campaign);
$campaign->update($this->validated($request));
return back()->with('success', 'Campaign updated.');
}
public function destroy(Campaign $campaign): RedirectResponse
{
$this->authorizeCampaign($campaign);
ShortLink::query()
->where('campaign_id', $campaign->id)
->update(['campaign_id' => null]);
$campaign->delete();
return redirect()
->route('link.campaigns.index')
->with('success', 'Campaign deleted. Links were unassigned, not removed.');
}
public function attach(Request $request, Campaign $campaign): RedirectResponse
{
$this->authorizeCampaign($campaign);
$validated = $request->validate([
'short_link_ids' => ['required', 'array', 'min:1'],
'short_link_ids.*' => ['integer'],
]);
ShortLink::query()
->where('user_id', $campaign->user_id)
->whereIn('id', $validated['short_link_ids'])
->update(['campaign_id' => $campaign->id]);
return back()->with('success', 'Links added to campaign.');
}
public function detach(Campaign $campaign, ShortLink $link): RedirectResponse
{
$this->authorizeCampaign($campaign);
abort_unless((int) $link->user_id === (int) $campaign->user_id, 403);
abort_unless((int) $link->campaign_id === (int) $campaign->id, 404);
$link->update(['campaign_id' => null]);
return back()->with('success', 'Link removed from campaign.');
}
/** @return array<string, mixed> */
private function validated(Request $request): array
{
$data = $request->validate([
'name' => ['required', 'string', 'max:120'],
'description' => ['nullable', 'string', 'max:2000'],
'status' => ['required', Rule::in(array_keys(Campaign::STATUSES))],
'starts_at' => ['nullable', 'date'],
'ends_at' => ['nullable', 'date', 'after_or_equal:starts_at'],
]);
$data['description'] = $data['description'] ?? null;
$data['starts_at'] = $data['starts_at'] ?? null;
$data['ends_at'] = $data['ends_at'] ?? null;
return $data;
}
private function authorizeCampaign(Campaign $campaign): void
{
abort_unless((int) $campaign->user_id === (int) ladill_account()->id, 403);
}
}