move into sonardesk
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
|
||||
return ['name' => 'Newsletter'];
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('newsletter_lists', function (Blueprint $t): void {
|
||||
$t->id();
|
||||
$t->string('name');
|
||||
$t->text('description')->nullable();
|
||||
$t->boolean('is_active')->default(true);
|
||||
$t->timestamps();
|
||||
});
|
||||
Schema::create('newsletter_subscribers', function (Blueprint $t): void {
|
||||
$t->id();
|
||||
$t->string('email')->unique();
|
||||
$t->string('name')->nullable();
|
||||
$t->string('status')->default('pending')->index();
|
||||
$t->string('confirmation_token')->nullable()->unique();
|
||||
$t->timestamp('confirmed_at')->nullable();
|
||||
$t->timestamp('unsubscribed_at')->nullable();
|
||||
$t->json('metadata')->nullable();
|
||||
$t->timestamps();
|
||||
});
|
||||
Schema::create('newsletter_list_subscriber', function (Blueprint $t): void {
|
||||
$t->foreignId('list_id')->constrained('newsletter_lists')->cascadeOnDelete();
|
||||
$t->foreignId('subscriber_id')->constrained('newsletter_subscribers')->cascadeOnDelete();
|
||||
$t->primary(['list_id', 'subscriber_id']);
|
||||
});
|
||||
Schema::create('newsletter_campaigns', function (Blueprint $t): void {
|
||||
$t->id();
|
||||
$t->foreignId('list_id')->nullable()->constrained('newsletter_lists')->nullOnDelete();
|
||||
$t->string('name');
|
||||
$t->string('subject');
|
||||
$t->longText('content')->nullable();
|
||||
$t->string('status')->default('draft')->index();
|
||||
$t->timestamp('scheduled_at')->nullable();
|
||||
$t->timestamp('sent_at')->nullable();
|
||||
$t->unsignedInteger('sent_count')->default(0);
|
||||
$t->unsignedInteger('open_count')->default(0);
|
||||
$t->unsignedInteger('click_count')->default(0);
|
||||
$t->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('newsletter_campaigns');
|
||||
Schema::dropIfExists('newsletter_list_subscriber');
|
||||
Schema::dropIfExists('newsletter_subscribers');
|
||||
Schema::dropIfExists('newsletter_lists');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Newsletter\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\View\View;
|
||||
use Modules\Newsletter\Models\Campaign;
|
||||
use Modules\Newsletter\Models\MailingList;
|
||||
use Modules\Newsletter\Models\Subscriber;
|
||||
|
||||
final class NewsletterController
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
return view('newsletter::admin.index', ['subscribers' => Subscriber::latest()->paginate(20), 'campaigns' => Campaign::latest()->limit(10)->get(), 'lists' => MailingList::orderBy('name')->get()]);
|
||||
}
|
||||
|
||||
public function storeSubscriber(Request $r): RedirectResponse
|
||||
{
|
||||
$d = $r->validate(['email' => ['required', 'email', 'unique:newsletter_subscribers,email'], 'name' => ['nullable', 'string', 'max:255']]);
|
||||
Subscriber::create($d + ['status' => 'confirmed', 'confirmation_token' => Str::random(48), 'confirmed_at' => now()]);
|
||||
|
||||
return back()->with('status', 'Subscriber added.');
|
||||
}
|
||||
|
||||
public function storeCampaign(Request $r): RedirectResponse
|
||||
{
|
||||
Campaign::create($r->validate(['list_id' => ['nullable', 'exists:newsletter_lists,id'], 'name' => ['required', 'string', 'max:255'], 'subject' => ['required', 'string', 'max:255'], 'content' => ['nullable', 'string'], 'status' => ['required', 'in:draft,scheduled,sent'], 'scheduled_at' => ['nullable', 'date']]));
|
||||
|
||||
return back()->with('status', 'Campaign created.');
|
||||
}
|
||||
|
||||
public function publicSubscribe(Request $r): RedirectResponse
|
||||
{
|
||||
$data = $r->validate(['email' => ['required', 'email', 'max:255'], 'name' => ['nullable', 'string', 'max:255']]);
|
||||
Subscriber::query()->updateOrCreate(
|
||||
['email' => strtolower($data['email'])],
|
||||
['name' => $data['name'] ?? null, 'status' => 'confirmed', 'confirmation_token' => Str::random(48), 'confirmed_at' => now()]
|
||||
);
|
||||
|
||||
return back()->with('status', 'Thank you for subscribing.');
|
||||
}
|
||||
|
||||
public function destroySubscriber(Subscriber $subscriber): RedirectResponse
|
||||
{
|
||||
$subscriber->delete();
|
||||
|
||||
return back()->with('status', 'Subscriber deleted.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Newsletter\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
final class Campaign extends Model
|
||||
{
|
||||
protected $table = 'newsletter_campaigns';
|
||||
|
||||
protected $fillable = ['list_id', 'name', 'subject', 'content', 'status', 'scheduled_at', 'sent_at', 'sent_count', 'open_count', 'click_count'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['scheduled_at' => 'datetime', 'sent_at' => 'datetime'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Newsletter\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
final class MailingList extends Model
|
||||
{
|
||||
protected $table = 'newsletter_lists';
|
||||
|
||||
protected $fillable = ['name', 'description', 'is_active'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['is_active' => 'boolean'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Newsletter\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
final class Subscriber extends Model
|
||||
{
|
||||
protected $table = 'newsletter_subscribers';
|
||||
|
||||
protected $fillable = ['email', 'name', 'status', 'confirmation_token', 'confirmed_at', 'unsubscribed_at', 'metadata'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['confirmed_at' => 'datetime', 'unsubscribed_at' => 'datetime', 'metadata' => 'array'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Newsletter\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
final class NewsletterServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->register(RouteServiceProvider::class);
|
||||
$this->mergeConfigFrom(module_path('Newsletter', 'Config/config.php'), 'newsletter');
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
$this->loadMigrationsFrom(module_path('Newsletter', 'Database/Migrations'));
|
||||
$this->loadViewsFrom(module_path('Newsletter', 'Resources/views'), 'newsletter');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Newsletter\Providers;
|
||||
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
final class RouteServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
parent::boot();
|
||||
Route::middleware('web')->group(module_path('Newsletter', 'Routes/web.php'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<x-layouts.app :title="__('Newsletter')"><div class="space-y-8"><div><h1 class="text-2xl font-semibold">Newsletter</h1><p class="text-sm text-zinc-500">Manage subscribers and email campaigns.</p></div>@if(session('status'))<div class="rounded border border-green-300 bg-green-50 p-3 text-green-800">{{ session('status') }}</div>@endif<div class="grid gap-6 lg:grid-cols-2"><form method="POST" action="{{ route('administration.newsletter.subscribers.store') }}" class="space-y-4 rounded-xl border p-5">@csrf<h2 class="font-semibold">Add subscriber</h2><flux:input name="email" type="email" label="Email" required/><flux:input name="name" label="Name"/><flux:button type="submit" variant="primary">Add subscriber</flux:button></form><form method="POST" action="{{ route('administration.newsletter.campaigns.store') }}" class="space-y-4 rounded-xl border p-5">@csrf<h2 class="font-semibold">Create campaign</h2><flux:input name="name" label="Campaign name" required/><flux:input name="subject" label="Subject" required/><flux:select name="status" label="Status"><option value="draft">Draft</option><option value="scheduled">Scheduled</option></flux:select><flux:textarea name="content" label="Content"/><flux:button type="submit" variant="primary">Create campaign</flux:button></form></div><div class="overflow-hidden rounded-xl border"><table class="w-full text-sm"><thead class="bg-zinc-50"><tr><th class="p-3 text-left">Subscriber</th><th class="p-3 text-left">Status</th><th class="p-3 text-left">Confirmed</th><th class="p-3"></th></tr></thead><tbody>@forelse($subscribers as $subscriber)<tr class="border-t"><td class="p-3"><div class="font-medium">{{ $subscriber->email }}</div><div class="text-zinc-500">{{ $subscriber->name }}</div></td><td class="p-3">{{ ucfirst($subscriber->status) }}</td><td class="p-3">{{ optional($subscriber->confirmed_at)->format('j M Y') ?: '—' }}</td><td class="p-3 text-right"><form method="POST" action="{{ route('administration.newsletter.subscribers.destroy',$subscriber) }}">@csrf @method('DELETE')<flux:button size="sm" variant="danger" type="submit">Delete</flux:button></form></td></tr>@empty<tr><td colspan="4" class="p-6 text-center text-zinc-500">No subscribers yet.</td></tr>@endforelse</tbody></table></div>{{ $subscribers->links() }}</div></x-layouts.app>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Newsletter\Http\Controllers\NewsletterController;
|
||||
|
||||
Route::middleware(['auth', 'verified'])->prefix('administration/newsletter')->name('administration.newsletter.')->group(function (): void {
|
||||
Route::get('/', [NewsletterController::class, 'index'])->name('index');
|
||||
Route::post('/subscribers', [NewsletterController::class, 'storeSubscriber'])->name('subscribers.store');
|
||||
Route::delete('/subscribers/{subscriber}', [NewsletterController::class, 'destroySubscriber'])->name('subscribers.destroy');
|
||||
Route::post('/campaigns', [NewsletterController::class, 'storeCampaign'])->name('campaigns.store');
|
||||
});
|
||||
|
||||
Route::post('/newsletter/subscribe', [NewsletterController::class, 'publicSubscribe'])->middleware('throttle:10,1')->name('newsletter.public.subscribe');
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Newsletter\Tests\Feature;
|
||||
|
||||
use Tests\TestCase;
|
||||
|
||||
final class ModuleBootTest extends TestCase
|
||||
{
|
||||
public function test_module_configuration_is_available(): void
|
||||
{
|
||||
$this->assertSame('Newsletter', config('newsletter.name'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Newsletter",
|
||||
"alias": "newsletter",
|
||||
"version": "1.0.0",
|
||||
"description": "Subscriber lists and email campaign management.",
|
||||
"keywords": ["business", "newsletter"],
|
||||
"priority": 41,
|
||||
"providers": ["Modules\\Newsletter\\Providers\\NewsletterServiceProvider"],
|
||||
"aliases": {},
|
||||
"files": [],
|
||||
"requires": ["Core", "Administration"]
|
||||
}
|
||||
Reference in New Issue
Block a user