vendor and env first commit

This commit is contained in:
2025-03-28 08:52:46 +01:00
parent f8388bc81b
commit 8f26283832
10976 changed files with 1349952 additions and 2 deletions
+63
View File
@@ -0,0 +1,63 @@
<?php
namespace Vanguard\Announcements;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
use Illuminate\Support\HtmlString;
use League\CommonMark\CommonMarkConverter;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\Table\TableExtension;
use Vanguard\Announcements\Database\Factories\AnnouncementFactory;
use Vanguard\User;
/**
* @property int $id
* @property string $title
* @property string $body
* @property Carbon $created_at
* @property Carbon $deleted_at
*/
class Announcement extends Model
{
use HasFactory;
protected $table = 'announcements';
protected $guarded = [];
public function creator(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
public function wasReadBy(User $user): bool
{
return $user->announcements_last_read_at < $this->created_at;
}
public function getParsedBodyAttribute(): HtmlString
{
$environment = Environment::createCommonMarkEnvironment();
$environment->addExtension(new TableExtension);
$converter = new CommonMarkConverter([
'html_input' => 'escape',
'allow_unsafe_links' => false,
], $environment);
return new HtmlString(
$converter->convertToHtml($this->attributes['body'])
);
}
/**
* {@inheritDoc}
*/
protected static function newFactory(): AnnouncementFactory
{
return new AnnouncementFactory;
}
}
+154
View File
@@ -0,0 +1,154 @@
<?php
namespace Vanguard\Announcements;
use Event;
use Route;
use Vanguard\Announcements\Events\EmailNotificationRequested;
use Vanguard\Announcements\Hooks\NavbarItemsHook;
use Vanguard\Announcements\Hooks\ScriptsHook;
use Vanguard\Announcements\Hooks\StylesHook;
use Vanguard\Announcements\Listeners\ActivityLogSubscriber;
use Vanguard\Announcements\Listeners\SendEmailNotification;
use Vanguard\Announcements\Repositories\AnnouncementsRepository;
use Vanguard\Announcements\Repositories\EloquentAnnouncements;
use Vanguard\Plugins\Plugin;
use Vanguard\Plugins\Vanguard;
use Vanguard\Support\Sidebar\Item;
class Announcements extends Plugin
{
/**
* A sidebar item for the plugin.
*/
public function sidebar(): ?Item
{
return Item::create(__('Announcements'))
->icon('fas fa-bullhorn')
->route('announcements.index')
->permissions('announcements.manage')
->active('announcements*');
}
/**
* Register plugin services.
*/
public function register(): void
{
$this->app->singleton(AnnouncementsRepository::class, EloquentAnnouncements::class);
}
/**
* Bootstrap services.
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function boot(): void
{
$this->registerViews();
$this->loadMigrationsFrom(__DIR__.'/../database/migrations');
$this->loadViewsFrom(__DIR__.'/../resources/views', 'announcements');
$this->loadTranslationsFrom(__DIR__.'/../resources/lang', 'announcements');
$this->loadJsonTranslationsFrom(__DIR__.'/../resources/lang');
$this->publishes([
__DIR__.'/../database/migrations' => database_path('migrations'),
], 'migrations');
$this->mapRoutes();
$this->registerHooks();
$this->registerEventListeners();
$this->publishAssets();
}
/**
* Register plugin views.
*/
protected function registerViews(): void
{
$viewsPath = __DIR__.'/../resources/views';
$this->publishes([
$viewsPath => resource_path('views/vendor/plugins/announcements'),
], 'views');
$this->loadViewsFrom($viewsPath, 'announcements');
}
/**
* Map all plugin related routes.
*/
protected function mapRoutes(): void
{
$this->mapWebRoutes();
if ($this->app['config']->get('auth.expose_api')) {
$this->mapApiRoutes();
}
}
/**
* Map web plugin related routes.
*/
protected function mapWebRoutes(): void
{
Route::group([
'namespace' => 'Vanguard\Announcements\Http\Controllers\Web',
'middleware' => 'web',
], function () {
$this->loadRoutesFrom(__DIR__.'/../routes/web.php');
});
}
/**
* Map API plugin related routes.
*/
protected function mapApiRoutes(): void
{
Route::group([
'namespace' => 'Vanguard\Announcements\Http\Controllers\Api',
'middleware' => 'api',
'prefix' => 'api',
], function () {
$this->loadRoutesFrom(__DIR__.'/../routes/api.php');
});
}
/**
* Register plugin event listeners.
*/
private function registerEventListeners(): void
{
// Register activity log subscriber only if
// UserActivity plugin is installed.
if ($this->app->bound('Vanguard\UserActivity\Repositories\Activity\ActivityRepository')) {
Event::subscribe(ActivityLogSubscriber::class);
}
Event::listen(EmailNotificationRequested::class, SendEmailNotification::class);
}
/**
* Register all necessary view hooks for the plugin.
*/
private function registerHooks(): void
{
Vanguard::hook('navbar:items', NavbarItemsHook::class);
Vanguard::hook('app:styles', StylesHook::class);
Vanguard::hook('app:scripts', ScriptsHook::class);
}
/**
* Publish public assets.
*/
protected function publishAssets(): void
{
$this->publishes([
realpath(__DIR__.'/../dist') => $this->app['path.public'].'/vendor/plugins/announcements',
], 'public');
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace Vanguard\Announcements\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Vanguard\Announcements\Announcement;
class Created
{
use Dispatchable;
public function __construct(public Announcement $announcement, public $sendEmailNotification = false)
{
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace Vanguard\Announcements\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Vanguard\Announcements\Announcement;
class Deleted
{
use Dispatchable;
public function __construct(public Announcement $announcement)
{
}
}
@@ -0,0 +1,15 @@
<?php
namespace Vanguard\Announcements\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Vanguard\Announcements\Announcement;
class EmailNotificationRequested
{
use Dispatchable;
public function __construct(public Announcement $announcement)
{
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace Vanguard\Announcements\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Vanguard\Announcements\Announcement;
class Updated
{
use Dispatchable;
public function __construct(public Announcement $announcement)
{
}
}
@@ -0,0 +1,25 @@
<?php
namespace Vanguard\Announcements\Hooks;
use Illuminate\Contracts\View\View;
use Vanguard\Announcements\Repositories\AnnouncementsRepository;
use Vanguard\Plugins\Contracts\Hook;
class NavbarItemsHook implements Hook
{
public function __construct(private readonly AnnouncementsRepository $announcements)
{
}
/**
* Execute the hook action.
*/
public function handle(): View
{
$announcements = $this->announcements->latest(5);
$announcements->load('creator');
return view('announcements::partials.navbar.list', compact('announcements'));
}
}
@@ -0,0 +1,17 @@
<?php
namespace Vanguard\Announcements\Hooks;
use Illuminate\Contracts\View\View;
use Vanguard\Plugins\Contracts\Hook;
class ScriptsHook implements Hook
{
/**
* Execute the hook action.
*/
public function handle(): View
{
return view('announcements::partials.scripts');
}
}
@@ -0,0 +1,17 @@
<?php
namespace Vanguard\Announcements\Hooks;
use Illuminate\Contracts\View\View;
use Vanguard\Plugins\Contracts\Hook;
class StylesHook implements Hook
{
/**
* Execute the hook action.
*/
public function handle(): View
{
return view('announcements::partials.styles');
}
}
@@ -0,0 +1,108 @@
<?php
namespace Vanguard\Announcements\Http\Controllers\Api;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Spatie\QueryBuilder\AllowedFilter;
use Spatie\QueryBuilder\AllowedInclude;
use Spatie\QueryBuilder\QueryBuilder;
use Vanguard\Announcements\Announcement;
use Vanguard\Announcements\Events\EmailNotificationRequested;
use Vanguard\Announcements\Http\Requests\AnnouncementRequest;
use Vanguard\Announcements\Http\Resources\AnnouncementResource;
use Vanguard\Announcements\Repositories\AnnouncementsRepository;
use Vanguard\Http\Controllers\Api\ApiController;
/**
* Class AnnouncementsController
*/
class AnnouncementsController extends ApiController
{
public function __construct(private readonly AnnouncementsRepository $announcements)
{
$this->middleware('permission:announcements.manage')->except('index', 'show');
}
/**
* Returns a paginated list of announcements.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function index(Request $request): AnonymousResourceCollection
{
$this->validate($request, ['per_page' => 'numeric|max:50']);
$announcements = QueryBuilder::for(Announcement::class)
->allowedIncludes([
AllowedInclude::relationship('user', 'creator'),
])
->allowedFilters([
AllowedFilter::partial('title'),
AllowedFilter::partial('body'),
AllowedFilter::exact('user', 'user_id'),
])
->allowedSorts('title', 'created_at')
->defaultSort('-created_at')
->paginate($request->per_page);
return AnnouncementResource::collection($announcements);
}
/**
* Stores the announcement inside the database.
*/
public function store(AnnouncementRequest $request): AnnouncementResource
{
$announcement = $this->announcements->createFor(
auth()->user(),
$request->title,
$request->body
);
if ($request->email_notification) {
EmailNotificationRequested::dispatch($announcement);
}
return new AnnouncementResource($announcement);
}
/**
* Returns a single announcement resource.
*/
public function show($announcementId): AnnouncementResource
{
$announcement = QueryBuilder::for(Announcement::where('id', $announcementId))
->allowedIncludes([
AllowedInclude::relationship('user', 'creator'),
])
->first();
return new AnnouncementResource($announcement);
}
/**
* Updates announcement details.
*/
public function update(Announcement $announcement, AnnouncementRequest $request): AnnouncementResource
{
$announcement = $this->announcements->update(
$announcement,
$request->title,
$request->body
);
return new AnnouncementResource($announcement);
}
/**
* Removes announcement from the system.
*/
public function destroy(Announcement $announcement): JsonResponse
{
$this->announcements->delete($announcement);
return $this->respondWithSuccess();
}
}
@@ -0,0 +1,23 @@
<?php
namespace Vanguard\Announcements\Http\Controllers\Api;
use Vanguard\Http\Controllers\Api\ApiController;
class ReadAnnouncementsController extends ApiController
{
/**
* Update the timestamp when announcements were last read
* by the currently authenticated user.
*
* @return \Illuminate\Http\JsonResponse
*/
public function index()
{
auth()->user()->forceFill([
'announcements_last_read_at' => now(),
])->save();
return $this->respondWithSuccess();
}
}
@@ -0,0 +1,25 @@
<?php
namespace Vanguard\Announcements\Http\Controllers\Web;
use Illuminate\Contracts\View\View;
use Vanguard\Announcements\Repositories\AnnouncementsRepository;
use Vanguard\Http\Controllers\Controller;
class AnnouncementListController extends Controller
{
public function __construct(private readonly AnnouncementsRepository $announcements)
{
}
/**
* Displays the plugin index page.
*/
public function index(): View
{
$announcements = $this->announcements->paginate(7);
$announcements->load('creator');
return view('announcements::list', compact('announcements'));
}
}
@@ -0,0 +1,105 @@
<?php
namespace Vanguard\Announcements\Http\Controllers\Web;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Vanguard\Announcements\Announcement;
use Vanguard\Announcements\Events\EmailNotificationRequested;
use Vanguard\Announcements\Http\Requests\AnnouncementRequest;
use Vanguard\Announcements\Repositories\AnnouncementsRepository;
use Vanguard\Http\Controllers\Controller;
/**
* Class AnnouncementsController
*/
class AnnouncementsController extends Controller
{
public function __construct(private readonly AnnouncementsRepository $announcements)
{
$this->middleware('permission:announcements.manage')->except('show');
}
/**
* Displays the plugin index page.
*/
public function index(): View
{
$announcements = $this->announcements->paginate();
$announcements->load('creator');
return view('announcements::index', compact('announcements'));
}
/**
* Shows the create announcement form.
*/
public function create(): View
{
return view('announcements::add-edit', ['edit' => false]);
}
/**
* Stores the announcement inside the database.
*/
public function store(AnnouncementRequest $request): RedirectResponse
{
$announcement = $this->announcements->createFor(
auth()->user(),
$request->title,
$request->body
);
if ($request->email_notification) {
EmailNotificationRequested::dispatch($announcement);
}
return redirect()->route('announcements.index')
->withSuccess(__('Announcement created successfully.'));
}
/**
* Renders "view announcement" page.
*/
public function show(Announcement $announcement): View
{
return view('announcements::show', compact('announcement'));
}
/**
* Renders the form for editing the announcement.
*/
public function edit(Announcement $announcement): View
{
return view('announcements::add-edit', [
'edit' => true,
'announcement' => $announcement,
]);
}
/**
* Updates announcement details.
*/
public function update(Announcement $announcement, AnnouncementRequest $request): RedirectResponse
{
$this->announcements->update(
$announcement,
$request->title,
$request->body
);
return redirect()->route('announcements.index')
->withSuccess(__('Announcement updated successfully.'));
}
/**
* Removes announcement from the system.
*/
public function destroy(Announcement $announcement): RedirectResponse
{
$this->announcements->delete($announcement);
return redirect()->route('announcements.index')
->withSuccess(__('Announcement deleted successfully.'));
}
}
@@ -0,0 +1,19 @@
<?php
namespace Vanguard\Announcements\Http\Controllers\Web;
use Vanguard\Http\Controllers\Controller;
class ReadAnnouncementsController extends Controller
{
/**
* Update the timestamp when announcements were last read
* by the currently authenticated user.
*/
public function index(): void
{
auth()->user()->forceFill([
'announcements_last_read_at' => now(),
])->save();
}
}
@@ -0,0 +1,20 @@
<?php
namespace Vanguard\Announcements\Http\Requests;
use Vanguard\Http\Requests\Request;
class AnnouncementRequest extends Request
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'title' => 'required|max:150',
'body' => 'required|max:1500',
'email_notifications' => 'boolean',
];
}
}
@@ -0,0 +1,28 @@
<?php
namespace Vanguard\Announcements\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
use Vanguard\Http\Resources\UserResource;
class AnnouncementResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
*/
public function toArray($request): array
{
return [
'id' => (int) $this->id,
'user_id' => (int) $this->user_id,
'title' => $this->title,
'body' => $this->body,
'parsed_body' => (string) $this->parsed_body,
'created_at' => (string) $this->created_at,
'updated_at' => (string) $this->updated_at,
'user' => new UserResource($this->whenLoaded('creator')),
];
}
}
@@ -0,0 +1,51 @@
<?php
namespace Vanguard\Announcements\Listeners;
use Illuminate\Events\Dispatcher;
use Illuminate\Support\Str;
use Vanguard\Announcements\Events\Created;
use Vanguard\Announcements\Events\Deleted;
use Vanguard\Announcements\Events\Updated;
use Vanguard\UserActivity\Logger;
class ActivityLogSubscriber
{
public function __construct(private Logger $logger)
{
}
public function onCreate(Created $event): void
{
$this->logger->log(__('announcements::log.created_announcement', [
'id' => $event->announcement->id,
'title' => Str::limit($event->announcement->title, 50),
]));
}
public function onUpdate(Updated $event): void
{
$this->logger->log(__('announcements::log.created_announcement', [
'id' => $event->announcement->id,
]));
}
public function onDelete(Deleted $event): void
{
$this->logger->log(__('announcements::log.deleted_announcement', [
'id' => $event->announcement->id,
]));
}
/**
* Register the listeners for the subscriber.
*/
public function subscribe(Dispatcher $events): void
{
$class = self::class;
$events->listen(Created::class, "{$class}@onCreate");
$events->listen(Updated::class, "{$class}@onUpdate");
$events->listen(Deleted::class, "{$class}@onDelete");
}
}
@@ -0,0 +1,29 @@
<?php
namespace Vanguard\Announcements\Listeners;
use Mail;
use Vanguard\Announcements\Announcement;
use Vanguard\Announcements\Events\EmailNotificationRequested;
use Vanguard\Announcements\Mail\AnnouncementEmail;
use Vanguard\User;
class SendEmailNotification
{
/**
* Handle the event.
*/
public function handle(EmailNotificationRequested $event): void
{
User::chunk(200, function ($users) use ($event) {
foreach ($users as $user) {
$this->sendEmailTo($user, $event->announcement);
}
});
}
private function sendEmailTo(User $user, Announcement $announcement): void
{
Mail::to($user)->send(new AnnouncementEmail($announcement));
}
}
@@ -0,0 +1,32 @@
<?php
namespace Vanguard\Announcements\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Vanguard\Announcements\Announcement;
class AnnouncementEmail extends Mailable implements ShouldQueue
{
use Queueable, SerializesModels;
/**
* Create a new message instance.
*/
public function __construct(public Announcement $announcement)
{
}
/**
* Build the message.
*/
public function build(): self
{
$subject = sprintf('[%s] %s', __('Announcement'), $this->announcement->title);
return $this->subject($subject)
->markdown('announcements::mail.notification');
}
}
@@ -0,0 +1,43 @@
<?php
namespace Vanguard\Announcements\Repositories;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Collection;
use Vanguard\Announcements\Announcement;
use Vanguard\User;
interface AnnouncementsRepository
{
/**
* Get latest announcements.
*
* @return Collection<Announcement>
*/
public function latest(int $count = 5): Collection;
/**
* Paginate announcements in descending order.
*/
public function paginate(int $perPage = 10): LengthAwarePaginator;
/**
* Create an announcement for user.
*/
public function createFor(User $user, string $title, string $body): Announcement;
/**
* Find announcement by ID.
*/
public function find(int $id): ?Announcement;
/**
* Update announcement.
*/
public function update(Announcement $announcement, string $title, string $body): Announcement;
/**
* Remove announcement from the system.
*/
public function delete(Announcement $announcement): bool;
}
@@ -0,0 +1,87 @@
<?php
namespace Vanguard\Announcements\Repositories;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Collection;
use Vanguard\Announcements\Announcement;
use Vanguard\Announcements\Events\Created;
use Vanguard\Announcements\Events\Deleted;
use Vanguard\Announcements\Events\Updated;
use Vanguard\User;
class EloquentAnnouncements implements AnnouncementsRepository
{
/**
* Get latest announcements.
*
* @return Collection<Announcement>
*/
public function latest(int $count = 5): Collection
{
return Announcement::latest()->take($count)->get();
}
/**
* Paginate announcements in descending order.
*/
public function paginate(int $perPage = 10): LengthAwarePaginator
{
return Announcement::latest()->paginate($perPage);
}
/**
* Create an announcement for user.
*/
public function createFor(User $user, string $title, string $body): Announcement
{
$announcement = Announcement::create([
'title' => $title,
'body' => $body,
'user_id' => $user->id,
]);
Created::dispatch($announcement);
return $announcement;
}
/**
* Find announcement by ID.
*/
public function find($id): ?Announcement
{
return Announcement::find($id);
}
/**
* Update announcement.
*/
public function update(Announcement $announcement, string $title, string $body): Announcement
{
$announcement->update([
'title' => $title,
'body' => $body,
]);
Updated::dispatch($announcement);
return $announcement;
}
/**
* Remove announcement from the system.
*
* @throws \Exception
*/
public function delete(Announcement $announcement): bool
{
if ($announcement->delete()) {
Deleted::dispatch($announcement);
return true;
}
return false;
}
}