diff --git a/app/Http/Controllers/Bo/NewsController.php b/app/Http/Controllers/Bo/NewsController.php
new file mode 100644
index 0000000..a79fa28
--- /dev/null
+++ b/app/Http/Controllers/Bo/NewsController.php
@@ -0,0 +1,166 @@
+authorizeResource(News::class);
+ }
+
+ /**
+ * Display a listing of the resource.
+ *
+ * @param \Illuminate\Http\Request $request
+ * @return \Illuminate\Contracts\View\View
+ */
+ public function index(Request $request): \Illuminate\Contracts\View\View
+ {
+ /** @var \Illuminate\Database\Eloquent\Builder $newsModels */
+ $newsModels = News::query();
+
+ /** @var string $search Search field */
+ $search = $request->search;
+ if ($search) {
+ $this->searchQuery(
+ $newsModels,
+ $search,
+ null,
+ 'title',
+ 'introduction',
+ 'content',
+ );
+ }
+ $searchFields = \implode(', ', [
+ Str::ucfirst(trans('validation.attributes.title')),
+ trans('validation.custom.introduction'),
+ trans('validation.attributes.content'),
+ ]);
+
+ /** Sort columns with a query */
+ $this->sortQuery($newsModels);
+
+ /** Custom pagination */
+ $newsModels = $this->paginate($newsModels);
+
+ return view('bo.pages.news.index', compact('newsModels', 'search', 'searchFields'));
+ }
+
+
+ /**
+ * Show the form for creating a new resource.
+ *
+ * @param \App\Models\News $news
+ * @return \Illuminate\Contracts\View\View
+ */
+ public function create(News $news): \Illuminate\Contracts\View\View
+ {
+ return view('bo.pages.news.create', ['newsModel' => $news]);
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param \App\Http\Requests\Bo\News\StoreNewsRequest $request
+ * @return \Illuminate\Http\RedirectResponse
+ */
+ public function store(StoreNewsRequest $request): \Illuminate\Http\RedirectResponse
+ {
+ return DB::transaction(function () use ($request) {
+ $news = new News();
+
+ if ($news->fill($request->validated())->saveOrFail()) {
+ return redirect()->route('bo.news.edit', $news)
+ ->with('success', __('crud.messages.has_been_created', [
+ 'model' => Str::ucfirst(__('models.classes.news'))
+ ]));
+ }
+ return redirect()->back()
+ ->with('error', __('crud.messages.cannot_be_created', [
+ 'model' => Str::ucfirst(__('models.classes.news'))
+ ]));
+ });
+ }
+
+ /**
+ * Show the form for editing the specified resource.
+ *
+ * @param App\Models\News $news
+ * @return \Illuminate\Contracts\View\View
+ */
+ public function edit(News $news): \Illuminate\Contracts\View\View
+ {
+ return view('bo.pages.news.edit', ['newsModel' => $news]);
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param \App\Http\Requests\Bo\News\UpdateNewsRequest $request
+ * @param \App\Models\News $news
+ * @return \Illuminate\Http\RedirectResponse
+ */
+ public function update(UpdateNewsRequest $request, News $news): \Illuminate\Http\RedirectResponse
+ {
+ return DB::transaction(function () use ($request, $news) {
+ if ($news->fill($request->validated())->saveOrFail()) {
+ return redirect()->route('bo.news.edit', $news)
+ ->with('success', __('crud.messages.has_been_updated', [
+ 'model' => Str::ucfirst(__('models.classes.news'))
+ ]));
+ }
+ return redirect()->route('bo.news.edit', $news)
+ ->with('error', __('crud.messages.cannot_be_updated', [
+ 'model' => Str::ucfirst(__('models.classes.news'))
+ ]));
+ });
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param \App\Models\News $news
+ * @return \Illuminate\Http\RedirectResponse
+ */
+ public function destroy(News $news): \Illuminate\Http\RedirectResponse
+ {
+ if ($news->deleteOrFail()) {
+ return redirect()->route('bo.news.index')
+ ->with('success', __('crud.messages.has_been_deleted', [
+ 'model' => Str::ucfirst(__('models.classes.news'))
+ ]));
+ }
+ return redirect()->back()
+ ->with('error', __('crud.messages.cannot_be_deleted', [
+ 'model' => Str::ucfirst(__('models.classes.news'))
+ ]));
+ }
+
+ /**
+ * Duplicate the specified resource.
+ *
+ * @param \App\Models\News $news
+ * @return \Illuminate\Contracts\View\View
+ */
+ public function duplicate(News $news): \Illuminate\Contracts\View\View
+ {
+ return $this->create($news->replicate());
+ }
+}
diff --git a/app/Http/Requests/Bo/News/StoreNewsRequest.php b/app/Http/Requests/Bo/News/StoreNewsRequest.php
new file mode 100644
index 0000000..783316b
--- /dev/null
+++ b/app/Http/Requests/Bo/News/StoreNewsRequest.php
@@ -0,0 +1,90 @@
+check();
+ }
+
+ /**
+ * Prepare the data for validation.
+ *
+ * @return void
+ */
+ protected function prepareForValidation()
+ {
+ $this->merge([
+ 'published' => $this->published ? true : false
+ ]);
+ $this->mergePicture('picture');
+ $this->mergePicture('thumbnail');
+ }
+
+ /**
+ * Get the validation rules that apply to the request.
+ *
+ * @return array
+ */
+ public function rules(): array
+ {
+ $rules = [
+ 'seo_title' => 'required|between:45,70',
+ 'seo_description' => 'required|between:50,160',
+ 'title' => 'required|string|between:3,100',
+ 'picture_alt' => 'required|string|between:3,255',
+ 'picture_title' => 'required|string|between:3,255',
+ 'thumbnail_alt' => 'required|string|between:3,255',
+ 'thumbnail_title' => 'required|string|between:3,255',
+ 'introduction' => 'required|string|min:15',
+ 'content' => 'nullable|string|min:15',
+ 'linkedin_url' => 'nullable|url',
+ 'published' => 'required|boolean',
+ 'publish_at' => 'nullable|date',
+ 'unpublish_at' => 'nullable|date|after:publish_at',
+
+ ];
+ return \array_merge(
+ $rules,
+ // FIXME: Mettre à jour les tailles.
+ $this->pictureRules('picture', 100, 100, 100, 100, false),
+ $this->pictureRules('thumbnail', 100, 100, 100, 100, false),
+ );
+ }
+
+ /**
+ * Get custom attributes for validator errors.
+ *
+ * @return array
+ */
+ public function attributes(): array
+ {
+ return [
+ 'seo_title' => trans('validation.custom.seo_title'),
+ 'seo_description' => trans('validation.custom.seo_description'),
+ 'title' => trans('validation.attributes.title'),
+ 'introduction' => trans('validation.attributes.content'),
+ 'content' => trans('validation.attributes.content'),
+ 'linkedin_url' => trans('validation.attributes.content'),
+ 'picture_alt' => trans('validation.custom.picture_alt'),
+ 'picture_title' => trans('validation.custom.picture_title'),
+ 'thumbnail_alt' => trans('validation.custom.thumbnail_alt'),
+ 'thumbnail_title' => trans('validation.custom.thumbnail_title'),
+ 'published' => trans('validation.custom.activated'),
+ 'publish_at' => trans('validation.custom.publish_at'),
+ 'unpublish_at' => trans('validation.custom.unpublish_at'),
+ ];
+ }
+}
diff --git a/app/Http/Requests/Bo/News/UpdateNewsRequest.php b/app/Http/Requests/Bo/News/UpdateNewsRequest.php
new file mode 100644
index 0000000..3e4e0b2
--- /dev/null
+++ b/app/Http/Requests/Bo/News/UpdateNewsRequest.php
@@ -0,0 +1,7 @@
+title);
- self::assertFieldIsUnique('slug', Str::slug($model->title));
+ self::assertFieldIsUnique('title', $model->title, $model->id);
+ self::assertFieldIsUnique('slug', Str::slug($model->title), $model->id);
+
+ ToolboxHelper::assertNoInternalBoUrls('introduction', $model->introduction);
+ $model->introduction = clean($model->introduction);
+ if ($model->content) {
+ ToolboxHelper::assertNoInternalBoUrls('content', $model->content);
+ $model->content = clean($model->content);
+ }
// * TRANSFORMATIONS
self::setSlug($model, $model->title);
diff --git a/app/Policies/NewsPolicy.php b/app/Policies/NewsPolicy.php
new file mode 100644
index 0000000..475e697
--- /dev/null
+++ b/app/Policies/NewsPolicy.php
@@ -0,0 +1,120 @@
+json('csp-report'))
- ->flatMap(function ($v, $k) {
- return [$k => is_array($v) ? collect($v)->flatten()->implode(', ') : $v];
- })->all();
+ ->flatMap(function ($v, $k) {
+ return [$k => is_array($v) ? collect($v)->flatten()->implode(', ') : $v];
+ })->all();
// * Filter array keys
$cspReport = collect(array_intersect_key($cspReport, [
'blocked-uri' => '', 'document-uri' => '', 'original-policy' => '',
diff --git a/app/Traits/Requests/HasPicture.php b/app/Traits/Requests/HasPicture.php
new file mode 100644
index 0000000..f79aa60
--- /dev/null
+++ b/app/Traits/Requests/HasPicture.php
@@ -0,0 +1,180 @@
+ $minWith ?? $this->minWith,
+ 'minHeight' => $minHeight ?? $this->minHeight,
+ 'maxWith' => $maxWith ?? $this->maxWith,
+ 'maxHeight' => $maxHeight ?? $this->maxHeight
+ ];
+ return [
+ $name => [
+ !$nullable ? 'required' : 'nullable',
+ (strlen($requiredWith)) ? 'required_with:' . $requiredWith : '',
+ ($size) ? 'max:' . $size : '',
+ function (string $attribute, $value, callable $fail) use ($dimensions) {
+ return ToolboxHelper::validatePicture($attribute, $value, $fail, $dimensions);
+ }
+ ]
+ ];
+ }
+
+ /**
+ * Images rules
+ *
+ * @param string $name
+ * @param integer|null $minWith
+ * @param integer|null $minHeight
+ * @param integer|null $maxWith
+ * @param integer|null $maxHeight
+ * @return array
+ */
+ public function picturesRules(
+ string $name = 'pictures',
+ ?int $minWith = null,
+ ?int $minHeight = null,
+ ?int $maxWith = null,
+ ?int $maxHeight = null
+ ): array {
+ $sizes = (object)[
+ 'minWith' => $minWith ?? $this->minWith,
+ 'minHeight' => $minHeight ?? $this->minHeight,
+ 'maxWith' => $maxWith ?? $this->maxWith,
+ 'maxHeight' => $maxHeight ?? $this->maxHeight
+ ];
+ return [
+ $name => 'sometimes|array',
+ "{$name}Validate" => "required_with:{$name}|array",
+ "{$name}Validate.*" => [
+ 'required',
+ function (string $attribute, $value, callable $fail) use ($sizes) {
+ return $this->validatePicture($attribute, $value, $fail, $sizes);
+ }
+ ]
+ ];
+ }
+
+ /**
+ * Merges products pictures fields with request data
+ *
+ * @param string $name
+ * @return void
+ */
+ protected function mergePicture(string $name = 'picture'): void
+ {
+ $picturePath = $this->{$name};
+ if (is_string($picturePath)) {
+ $this->merge(["{$name}Validate" => []]);
+ // ! Empty picture.
+ if ($picturePath === 'false') {
+ // * Empty picture and exit
+ $this->merge([$name => []]);
+ return;
+ }
+ $decodedPath = \urldecode($picturePath);
+ $this->merge([
+ "{$name}Validate" => array_merge(
+ $this->{"{$name}Validate"},
+ [\public_path(ltrim($decodedPath, '/'))]
+ )
+ ]);
+ $changedPicture = ltrim($decodedPath, '/');
+ $this->merge([
+ $name => $changedPicture
+ ]);
+ } //end if
+ }
+
+ /**
+ * Merges products pictures fields with request data
+ *
+ * @param string $name
+ * @return void
+ */
+ protected function mergePictures(string $name = 'pictures'): void
+ {
+ $pictures = $this->{$name};
+ if (is_array($pictures)) {
+ $this->merge(["{$name}Validate" => []]);
+ foreach ($pictures as $key => $picturePath) {
+ // ! Empty picture.
+ if ($picturePath === 'false') {
+ // * Empty picture and exit
+ $this->merge([$name => []]);
+ return;
+ }
+ $decodedPath = \urldecode($picturePath);
+ $this->merge([
+ "{$name}Validate" => array_merge(
+ $this->{"{$name}Validate"},
+ [\public_path(ltrim($decodedPath, '/'))]
+ )
+ ]);
+ $changedPicture = $this->{$name};
+ $changedPicture[$key] = ltrim($decodedPath, '/');
+ $this->merge([
+ $name => $changedPicture
+ ]);
+ }
+ } //end if
+ }
+}
diff --git a/resources/lang/fr/crud.php b/resources/lang/fr/crud.php
index f3dc47b..34d8196 100644
--- a/resources/lang/fr/crud.php
+++ b/resources/lang/fr/crud.php
@@ -13,21 +13,27 @@ return [
],
'helpers' => [
- 'list' => 'Liste',
- 'list_all' => 'Lister tous les :model',
- 'create_model' => 'Créer :model',
- 'view_model' => 'Voir :model',
- 'update_model' => 'Éditer :model',
- 'duplicate_model' => 'Dupliquer :model',
- 'delete_model' => 'Supprimer :model',
- 'save_model' => 'Sauvegarder :model',
- 'edit_model' => 'Mettre à jour :model',
- 'no_results' => 'Aucun résultat pour :model',
- 'publish' => 'Publié',
- 'unpublish' => 'Dépublié',
- 'seo_title' => 'Doit décrire précisément la page, recommandation pour un positionnement
+ 'list' => 'Liste',
+ 'list_all' => 'Lister tous les :model',
+ 'create_model' => 'Créer :model',
+ 'view_model' => 'Voir :model',
+ 'update_model' => 'Éditer :model',
+ 'duplicate_model' => 'Dupliquer :model',
+ 'delete_model' => 'Supprimer :model',
+ 'save_model' => 'Sauvegarder :model',
+ 'edit_model' => 'Mettre à jour :model',
+ 'no_results' => 'Aucun résultat pour :model',
+ 'publish' => 'Publié',
+ 'unpublish' => 'Dépublié',
+ 'seo_title' => 'Doit décrire précisément la page, recommandation pour un positionnement
optimal dans les moteurs de recherche (balise
).',
- 'seo_description' => 'Doit décrire précisément le contenu de la page (balise description).',
+ 'seo_description' => 'Doit décrire précisément le contenu de la page (balise description).',
+ 'required_fields' => 'Champs obligatoires',
+ 'visibility_with_dates' => 'Ce champ n\'aura d\'effet que si aucune date n\'est renseignée.',
+ 'publish_at' => 'Le :model sera publié à partir de cette date.',
+ 'unpublish_at' => 'Le :model sera dépublié à partir de cette date.',
+ 'url' => 'Doit être une URL valide.',
+
],
'libs' => [
diff --git a/resources/lang/fr/models.php b/resources/lang/fr/models.php
index cc24db9..92d30c3 100644
--- a/resources/lang/fr/models.php
+++ b/resources/lang/fr/models.php
@@ -4,7 +4,8 @@ return [
'classes' => [
'user' => 'utilisateur',
'activity_log' => 'journal d\'activité|journaux d\'activité',
- 'static_page' => 'page statique|pages statiques',
- 'section' => 'section',
+ 'static_page' => 'page statique|pages statiques',
+ 'section' => 'section',
+ 'news' => 'actualité',
]
];
diff --git a/resources/lang/fr/validation.php b/resources/lang/fr/validation.php
index fd9a539..ddab3ef 100644
--- a/resources/lang/fr/validation.php
+++ b/resources/lang/fr/validation.php
@@ -227,10 +227,20 @@ return [
'activated' => 'activé',
'target' => 'cible',
'static_page_type' => 'type de page',
+ 'seo' => 'Référencement',
'seo_title' => 'titre SEO',
'seo_description' => 'description SEO',
'h2_title' => 'titre h2',
'first_content' => 'premier contenu',
- 'specific_content' => 'contenu spécifique'
+ 'specific_content' => 'contenu spécifique',
+ 'linkedin_url' => 'lien du post Linkedin',
+ 'introduction' => 'introduction',
+ 'alt_attribute' => 'attribut alt',
+ 'title_attribute' => 'attribut title',
+ 'thumbnail' => 'vignette',
+ 'publish_at' => 'date de publication',
+ 'unpublish_at' => 'date de fin de publication',
+
+
],
];
diff --git a/resources/views/bo/layouts/nav-list.blade.php b/resources/views/bo/layouts/nav-list.blade.php
index c2e738e..8e38b7a 100644
--- a/resources/views/bo/layouts/nav-list.blade.php
+++ b/resources/views/bo/layouts/nav-list.blade.php
@@ -53,6 +53,14 @@
@endcan
+ @can('viewAny', \App\Models\News::class)
+
+
+ {{ Str::of(trans_choice('models.classes.news', '*'))->title() }}
+
+
+ @endcan
@canAny(['isConceptor', 'viewAny'], \App\Models\User::class)
diff --git a/resources/views/bo/pages/news/create.blade.php b/resources/views/bo/pages/news/create.blade.php
new file mode 100644
index 0000000..c28a4f7
--- /dev/null
+++ b/resources/views/bo/pages/news/create.blade.php
@@ -0,0 +1,36 @@
+@extends('bo.layout', ['brParam' => $newsModel])
+
+@section('title', __('Création d\'une :model', ['model' => __('models.classes.news')]))
+@section('description', __('Création d\'une toute nouvelle :model', ['model' => __('models.classes.news')]))
+@section('breadcrumb',
+ request()->route()->getName())
+
+@section('content')
+
+
+
+
+
+ @include('modules.breadcrumbs.breadcrumb-body', ['brParam' => $newsModel])
+
+
+
+
+
+
+@endsection
diff --git a/resources/views/bo/pages/news/edit.blade.php b/resources/views/bo/pages/news/edit.blade.php
new file mode 100644
index 0000000..c5225ea
--- /dev/null
+++ b/resources/views/bo/pages/news/edit.blade.php
@@ -0,0 +1,52 @@
+@extends('bo.layout', ['brParam' => $newsModel])
+
+@section('title', __('Édition d\'une :model', ['model' => __('models.classes.news')]))
+@section('description', __('Édition d\'une :model déjà existante', ['model' => __('models.classes.news')]))
+@section('breadcrumb',
+ request()->route()->getName())
+
+@section('content')
+
+
+
+
+
+ @include('modules.breadcrumbs.breadcrumb-body', ['brParam' => $newsModel])
+
+
+
+
+@endsection
diff --git a/resources/views/bo/pages/news/form.blade.php b/resources/views/bo/pages/news/form.blade.php
new file mode 100644
index 0000000..745951a
--- /dev/null
+++ b/resources/views/bo/pages/news/form.blade.php
@@ -0,0 +1,407 @@
+
+ {{-- SEO --}}
+
+
+
+ {{-- TITLE --}}
+
+
+
+ {{-- LINKEDIN_URL --}}
+
+
+
+ {{-- INTRODUCTION --}}
+
+
+
+ {{-- CONTENT --}}
+
+
+
+ {{-- PICTURE --}}
+
+
+
+ {{-- PICTURE_THUMBNAIL --}}
+
+
+
+ {{-- PUBLISHED --}}
+
+
+
+ {{-- PUBLICATION --}}
+
+
+
+
+
+
+
+
+
{{ __('* Champs obligatoires') }}
+
+
+
+@push('scripts')
+
+@endpush
diff --git a/resources/views/bo/pages/news/index-list.blade.php b/resources/views/bo/pages/news/index-list.blade.php
new file mode 100644
index 0000000..1f2d7e7
--- /dev/null
+++ b/resources/views/bo/pages/news/index-list.blade.php
@@ -0,0 +1,77 @@
+
+ @if (count($newsModels) > 0)
+
+ @include('bo.modules.table-col-sorter', [
+ 'cols' => [
+ 'title' => Str::ucfirst(__('validation.attributes.title')),
+ 'order' => Str::ucfirst(__('validation.custom.order')),
+ 'published' => Str::ucfirst(__('validation.custom.activated')),
+ 'updated_at' => Str::ucfirst(__('validation.attributes.updated_at')),
+ ],
+ ])
+
+
+ @foreach ($newsModels as $newsModel)
+
+ | {{ $newsModel->title }} |
+ {{-- {!! Str::limit($newsModel->introduction, 50, '...') !!} | --}}
+ @php
+ $routeName = request()
+ ->route()
+ ->getName();
+ @endphp
+ @if (empty(request()->search) && Session::get("$routeName.sort_col") === 'order')
+ @include('bo.modules.change-model-order', [
+ 'routeName' => 'news',
+ 'model' => $newsModel,
+ 'models' => $newsModels,
+ ])
+ @endif
+ @include('bo.modules.change-published-status', [
+ 'routeName' => 'news',
+ 'model' => $newsModel,
+ ])
+
+ {{ $newsModel->updated_at->isoFormat('LLLL') }}
+ |
+
+ @canAny(['delete', 'duplicate', 'update'], $newsModel)
+
+ @else
+
+
+
+ @endcan
+ |
+
+ @endforeach
+
+ @else
+
+ | {{ __('crud.other.no_model_found', ['model' => __('models.classes.news')]) }} |
+
+ @endif
+
diff --git a/resources/views/bo/pages/news/index.blade.php b/resources/views/bo/pages/news/index.blade.php
new file mode 100644
index 0000000..a5251a0
--- /dev/null
+++ b/resources/views/bo/pages/news/index.blade.php
@@ -0,0 +1,21 @@
+@extends('bo.layout')
+
+@section('title', Str::ucfirst(__('models.classes.news')))
+@section('description', __('Liste de tous les :model', ['model' => Str::plural(__('models.classes.news'))]))
+
+@section('content')
+
+ @include('modules.breadcrumbs.breadcrumb-body')
+ @can('create', \App\Models\News::class)
+
+
+
+ @endcan
+
+ @include('bo.modules.search-bar')
+
+ @include('bo.pages.news.index-list')
+
+ {{ $newsModels->links() }}
+@endsection
diff --git a/routes/breadcrumbs-bo.php b/routes/breadcrumbs-bo.php
index 26c9490..647ef93 100644
--- a/routes/breadcrumbs-bo.php
+++ b/routes/breadcrumbs-bo.php
@@ -1,6 +1,7 @@
push(Str::ucfirst(trans('crud.actions.edit')), route('bo.static_pages.edit', $static_page));
});
+// * NEWS
+Breadcrumbs::for('bo.news.index', function (Generator $trail) {
+ $trail->push(
+ Str::of(trans('models.classes.news'))->plural()->ucfirst(),
+ route('bo.news.index', ['sort_col' => 'updated_at', 'sort_way' => 'desc'])
+ );
+});
+Breadcrumbs::for('bo.news.create', function (Generator $trail) {
+ $trail->parent('bo.news.index');
+ $trail->push(Str::ucfirst(trans('crud.actions.create')), route('bo.news.create'));
+});
+Breadcrumbs::for('bo.news.edit', function (Generator $trail, News $news) {
+ $trail->parent('bo.news.index');
+ $trail->push(Str::ucfirst(trans('crud.actions.edit')), route('bo.news.edit', $news));
+});
+Breadcrumbs::for('bo.news.duplicate', function (Generator $trail) {
+ $trail->parent('bo.news.index');
+ $trail->push(Str::ucfirst(trans('crud.actions.duplicate')));
+});
+
// * SECTIONS
Breadcrumbs::for('bo.sections.index', function (Generator $trail) {
$trail->push(
diff --git a/routes/web-bo.php b/routes/web-bo.php
index a5d2a28..48bea97 100644
--- a/routes/web-bo.php
+++ b/routes/web-bo.php
@@ -2,6 +2,7 @@
use App\Http\Controllers\Bo\ActivityLogController;
use App\Http\Controllers\Bo\DashboardController;
+use App\Http\Controllers\Bo\NewsController;
use App\Http\Controllers\Bo\SectionController;
use App\Http\Controllers\Bo\StaticPageController;
use App\Http\Controllers\Bo\UploadController;
@@ -48,6 +49,20 @@ Route::prefix('bo')
])->parameters(['pages' => 'static_page'])
->only(['index', 'edit', 'update']);
+ // * NEWS.
+ Route::resource('news', NewsController::class)->except('show')
+ ->withoutMiddleware('htmlsanitize')
+ ->middleware('htmlsanitize:content');
+ Route::patch('/news/{news}/change-published', [
+ NewsController::class, 'changePublished'
+ ])->name('news.change-published');
+ Route::patch('/news/{news}/change-order/{direction}', [
+ RangeController::class, 'changeOrder'
+ ])->where('direction', 'up|down')->name('news.change-order');
+ Route::get('/news/{news}/duplicate', [
+ NewsController::class, 'duplicate'
+ ])->name('news.duplicate');
+
// * SECTIONS.
Route::resource('sections', SectionController::class)
->only(['index', 'edit', 'update'])