diff --git a/app/Http/Controllers/Bo/NewsController.php b/app/Http/Controllers/Bo/NewsController.php new file mode 100644 index 0000000..bc58f6f --- /dev/null +++ b/app/Http/Controllers/Bo/NewsController.php @@ -0,0 +1,171 @@ +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', + 'subtitle', + 'content', + 'category', + ); + } + $searchFields = \implode(', ', [ + Str::ucfirst(trans('validation.attributes.title')), + trans('validation.custom.subtitle'), + trans('validation.attributes.content'), + trans('validation.custom.category'), + ]); + + /** 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(); + $news->fill($request->validated()); + + if ($news->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) { + $news->fill($request->validated()); + + if ($news->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..9f05f11 --- /dev/null +++ b/app/Http/Requests/Bo/News/StoreNewsRequest.php @@ -0,0 +1,91 @@ +check(); + } + + /** + * Prepare the data for validation. + * + * @return void + */ + protected function prepareForValidation() + { + $this->merge([ + 'published' => $this->published ? true : false + ]); + $this->mergePicture('picture'); + $this->mergePicture('picture_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', + 'category' => ['required', new Enum(CategoryEnum::class)], + 'picture_alt' => 'required|string|between:3,255', + 'picture_title' => 'required|string|between:3,255', + 'picture_thumbnail_alt' => 'required|string|between:3,255', + 'picture_thumbnail_title' => 'required|string|between:3,255', + 'subtitle' => 'required|string|between:3,255', + 'content' => 'required|string|min:15', + 'published' => 'required|boolean', + 'publish_at' => 'nullable|date', + 'unpublish_at' => 'nullable|date|after:publish_at', + + ]; + return \array_merge( + $rules, + $this->pictureRules('picture', 1002, 565, 1002, 565, false), + $this->pictureRules('picture_thumbnail', 265, 354, 265, 354, 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'), + 'category' => trans('validation.custom.category'), + 'subtitle' => trans('validation.custom.subtitle'), + 'content' => trans('validation.attributes.content'), + 'picture_alt' => trans('validation.custom.picture_alt'), + 'picture_title' => trans('validation.custom.picture_title'), + 'picture_thumbnail_alt' => trans('validation.custom.picture_thumbnail_alt'), + 'picture_thumbnail_title' => trans('validation.custom.picture_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 @@ +create([ - 'picture_thumbnail' => FileStorageHelper::storeFile(new News(), new \SplFileInfo(\resource_path( + News::factory(10)->make()->each(function (News $news) { + $news->picture_thumbnail = FileStorageHelper::storeFile($news, new \SplFileInfo(\resource_path( '../database/factories/assets/news/default-picture-265x354.png' - ))), - 'picture' => FileStorageHelper::storeFile(new News(), new \SplFileInfo(\resource_path( + ))); + $news->picture = FileStorageHelper::storeFile($news, new \SplFileInfo(\resource_path( '../database/factories/assets/news/default-picture-1002x565.png' - ))), - ]); + ))); + $news->save(); + }); } } diff --git a/resources/lang/en/crud.php b/resources/lang/en/crud.php index 175bcc3..34bb441 100644 --- a/resources/lang/en/crud.php +++ b/resources/lang/en/crud.php @@ -28,6 +28,9 @@ return [ 'seo_title' => 'Must give a precise description of the page, which is recommended for optimum positioning in search engines ( tag).', 'seo_description' => 'Must give a precise description of the page content (<meta> description tag).', + 'visibility_with_dates' => 'This field will only have an effect if no date is entered.', + 'publish_at' => 'The :model will be published from that date.', + 'unpublish_at' => 'The :model will be unpublished from that date.', ], 'libs' => [ diff --git a/resources/lang/en/models.php b/resources/lang/en/models.php index 707a36d..9e729c9 100644 --- a/resources/lang/en/models.php +++ b/resources/lang/en/models.php @@ -6,6 +6,7 @@ return [ 'activity_log' => 'activity log|activity logs', 'static_page' => 'static page|static pages', 'commitment' => 'commitment', - 'range' => 'range' + 'range' => 'range', + 'news' => 'news', ] ]; diff --git a/resources/lang/en/validation.php b/resources/lang/en/validation.php index 0a1e271..facd48b 100644 --- a/resources/lang/en/validation.php +++ b/resources/lang/en/validation.php @@ -238,6 +238,8 @@ return [ 'picture_productsheet' => 'productsheet picture', 'picture_thumbnail' => 'thumbnail', 'background' => 'background', + 'category' => 'category', + 'publish_at' => 'publication date', + 'unpublish_at' => 'end of publication date', ], - ]; diff --git a/resources/lang/fr/crud.php b/resources/lang/fr/crud.php index ede6326..8efb22f 100644 --- a/resources/lang/fr/crud.php +++ b/resources/lang/fr/crud.php @@ -28,7 +28,9 @@ return [ 'seo_title' => 'Doit décrire précisément la page, recommandation pour un positionnement optimal dans les moteurs de recherche (balise <title>).', 'seo_description' => 'Doit décrire précisément le contenu de la page (balise <meta> description).', - + '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.', ], 'libs' => [ @@ -89,13 +91,9 @@ return [ 'sort_delete' => 'Supprimer le tri', 'sort_arrow' => 'Ici, les flèches sont utilisées pour trier et non pour filtrer', 'right' => 'Vous n\'avez pas les droits', - ], 'placeholders' => [ - 'beautiful' => 'Un beau :field', - 'crossbreeding' => 'M99 X JA57', - 'growth_potential' => '2.18 kg à 49 jours', + 'beautiful' => 'Un beau :field', ] - ]; diff --git a/resources/lang/fr/models.php b/resources/lang/fr/models.php index 0aa86a7..ffea96d 100644 --- a/resources/lang/fr/models.php +++ b/resources/lang/fr/models.php @@ -8,5 +8,6 @@ return [ 'commitment' => 'engagement', 'range' => 'gamme', 'seasonal_section' => 'section saisonnière|sections saisonnières', + 'news' => 'actualité', ] ]; diff --git a/resources/lang/fr/validation.php b/resources/lang/fr/validation.php index 3a0d7a6..907d2ed 100644 --- a/resources/lang/fr/validation.php +++ b/resources/lang/fr/validation.php @@ -240,5 +240,8 @@ return [ 'picture_productsheet' => 'image de la fiche produit', 'picture_thumbnail' => 'vignette', 'background' => 'fond', + 'category' => 'catégorie', + '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 c805b5c..e9e3dec 100644 --- a/resources/views/bo/layouts/nav-list.blade.php +++ b/resources/views/bo/layouts/nav-list.blade.php @@ -41,6 +41,15 @@ </a> </li> @endcan + @can('viewAny', \App\Models\News::class) + <li class="nav-item"> + <a class="nav-link @if (request()->routeIs('bo.news.*')) active @endif" data-bs-toggle="tooltip" + href="{{ route('bo.news.index') }}" + title="{{ __('crud.helpers.list_all', ['model' => Str::of(__('models.classes.news'))->plural()->title()]) }}"> + {{ Str::of(__('models.classes.news'))->plural()->title() }} + </a> + </li> + @endcan @canAny(['isConceptor', 'viewAny'], \App\Models\User::class) <li class="nav-item"> <div class="dropdown"> 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..aedd76b --- /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\'un :model', ['model' => __('models.classes.news')])) +@section('description', __('Création d\'un tout nouveau :model', ['model' => __('models.classes.news')])) +@section('breadcrumb', + request()->route()->getName()) + +@section('content') + <div class="d-flex justify-content-between flex-md-nowrap align-items-center flex-wrap pb-3"> + <div class="d-flex align-items-start flex-row"> + <a class="btn btn-primary text-decoration-none m-0" data-bs-toggle="tooltip" href="{{ route('bo.news.index') }}" + title="{{ __('crud.helpers.list_all', ['model' => __('models.classes.news')]) }}"> + <i class="fa-solid fa-arrow-left"></i> + </a> + @include('modules.breadcrumbs.breadcrumb-body', ['brParam' => $newsModel]) + </div> + <div class="mb-md-0 mb-2"> + <button class="btn btn-primary" id="formSubmitClone" data-bs-toggle="tooltip" type="submit" + title="{{ __('crud.helpers.save_model', ['model' => __('models.classes.news')]) }}"> + {{ Str::ucfirst(__('crud.actions.save')) }} + </button> + </div> + </div> + <form action="{{ route('bo.news.store') }}" method="POST" enctype="multipart/form-data"> + @csrf + @include('bo.pages.news.form') + <div class="row mt-3"> + <div class="col text-center"> + <button class="btn btn-primary" id="formSubmit" data-bs-toggle="tooltip" type="submit" + title="{{ __('crud.helpers.save_model', ['model' => __('models.classes.news')]) }}"> + {{ Str::ucfirst(__('crud.actions.save')) }} + </button> + </div> + </div> + </form> +@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..35f1a4d --- /dev/null +++ b/resources/views/bo/pages/news/edit.blade.php @@ -0,0 +1,52 @@ +@extends('bo.layout', ['brParam' => $newsModel]) + +@section('title', __('Édition d\'un :model', ['model' => __('models.classes.news')])) +@section('description', __('Édition d\'un :model déjà existant', ['model' => __('models.classes.news')])) +@section('breadcrumb', + request()->route()->getName()) + +@section('content') + <div class="d-flex justify-content-between flex-md-nowrap align-items-center flex-wrap pb-3"> + <div class="d-flex align-items-start flex-row"> + <a class="btn btn-primary text-decoration-none m-0" data-bs-toggle="tooltip" href="{{ route('bo.news.index') }}" + title="{{ __('crud.helpers.list_all', ['model' => Str::plural(__('models.classes.news'))]) }}"> + <i class="fa-solid fa-arrow-left"></i> + </a> + @include('modules.breadcrumbs.breadcrumb-body', ['brParam' => $newsModel]) + </div> + <div class="mb-md-0 mb-2"> + <form class="btn-group confirmDeleteTS" action="{{ route('bo.news.destroy', $newsModel) }}" method="POST" novalidate> + @can('duplicate', $newsModel) + <a class="btn btn-secondary" data-bs-toggle="tooltip" href="{{ route('bo.news.duplicate', ['news' => $newsModel]) }}" + title="{{ Str::ucfirst(__('crud.actions.duplicate')) }}"> + <i class="fa-solid fa-copy"></i> + </a> + @endcan + <button class="btn btn-primary" id="formSubmitClone" data-bs-toggle="tooltip" type="submit" + title="{{ __('crud.helpers.save_model', ['model' => __('models.classes.news')]) }}"> + {{ Str::ucfirst(__('crud.actions.save')) }} + </button> + @can('delete', $newsModel) + @csrf + @method('DELETE') + <button class="btn btn-danger" data-bs-toggle="tooltip" type="submit" title="{{ Str::ucfirst(__('crud.actions.delete')) }}"> + <i class="fa-solid fa-trash"></i> + </button> + @endcan + </form> + </div> + </div> + <form action="{{ route('bo.news.update', $newsModel) }}" method="POST" enctype="multipart/form-data"> + @csrf + @method('PUT') + @include('bo.pages.news.form') + <div class="row"> + <div class="col text-center"> + <button class="btn btn-primary" id="formSubmit" data-bs-toggle="tooltip" type="submit" + title="{{ __('crud.helpers.save_model', ['model' => __('models.classes.news')]) }}"> + {{ Str::ucfirst(__('crud.actions.save')) }} + </button> + </div> + </div> + </form> +@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..d54c3f6 --- /dev/null +++ b/resources/views/bo/pages/news/form.blade.php @@ -0,0 +1,399 @@ +<div class="row"> + {{-- SEO --}} + <div class="col-12 pb-3"> + <fieldset class="h-100 border p-3"> + <legend>{{ Str::ucfirst(__('validation.custom.seo')) }}</legend> + <div class="row mb-3"> + {{-- SEO_TITLE --}} + <div class="col-12 col-md-6 m-md-0 mb-3"> + @include('bo.partials.inputs.varchar', [ + 'id' => 'newsSeoTitle', + 'label' => __(':field :inter:model', [ + 'field' => Str::ucfirst(__('validation.custom.seo_title')), + 'inter' => __('validation.custom.of_the.vowel'), + 'model' => __('models.classes.news'), + ]), + 'hasWordCounter' => true, + 'name' => 'seo_title', + 'required' => true, + 'model' => $newsModel ?? null, + 'helper' => sprintf( + '%s %s', + __('crud.helpers.seo_title'), + __('validation.between.string', [ + 'attribute' => __(':field :inter:model', [ + 'field' => __('validation.custom.seo_title'), + 'inter' => __('validation.custom.of_the.female'), + 'model' => __('models.classes.news'), + ]), + 'min' => 45, + 'max' => 70, + ])), + 'placeholder' => __(':field :inter:model', [ + 'field' => Str::ucfirst(__('validation.custom.seo_title')), + 'inter' => __('validation.custom.of_the.vowel'), + 'model' => __('models.classes.news'), + ]), + ]) + </div> + {{-- SEO_DESCRIPTION --}} + <div class="col-12 col-md-6 m-md-0 mb-3"> + @include('bo.partials.inputs.varchar-multiline', [ + 'id' => 'newsSeoDescription', + 'label' => __(':field :inter:model', [ + 'field' => Str::ucfirst(__('validation.custom.seo_description')), + 'inter' => __('validation.custom.of_the.vowel'), + 'model' => __('models.classes.news'), + ]), + 'hasWordCounter' => true, + 'name' => 'seo_description', + 'required' => true, + 'model' => $newsModel ?? null, + 'helper' => sprintf( + '%s %s', + __('crud.helpers.seo_description'), + __('validation.between.string', [ + 'attribute' => __(':field :inter:model', [ + 'field' => __('validation.custom.seo_description'), + 'inter' => __('validation.custom.of_the.female'), + 'model' => __('models.classes.news'), + ]), + 'min' => 50, + 'max' => 160, + ])), + 'placeholder' => __(':field :inter:model', [ + 'field' => Str::ucfirst(__('validation.custom.seo_description')), + 'inter' => __('validation.custom.of_the.vowel'), + 'model' => __('models.classes.news'), + ]), + ]) + </div> + </div> + </fieldset> + </div> + {{-- TITLE --}} + <div class="col-12 col-md-6 mb-3"> + <fieldset class="h-100 border p-3"> + <legend>{{ Str::ucfirst(__('validation.attributes.title')) }}</legend> + <div class="row"> + <div class="col-12"> + @include('bo.partials.inputs.varchar', [ + 'id' => 'newsTitle', + 'label' => __(':field :inter:model', [ + 'field' => Str::ucfirst(__('validation.attributes.title')), + 'inter' => __('validation.custom.of_the.vowel'), + 'model' => __('models.classes.news'), + ]), + 'hasWordCounter' => true, + 'name' => 'title', + 'required' => true, + 'model' => $newsModel ?? null, + 'helper' => __('validation.between.string', [ + 'attribute' => __(':field :inter:model', [ + 'field' => __('validation.attributes.title'), + 'inter' => __('validation.custom.of_the.female'), + 'model' => __('models.classes.news'), + ]), + 'min' => 3, + 'max' => 100, + ]), + 'placeholder' => __('Un titre clair et concis'), + ]) + </div> + </div> + </fieldset> + </div> + {{-- SUBTITLE --}} + <div class="col-12 col-md-6 mb-3"> + <fieldset class="h-100 border p-3"> + <legend>{{ Str::ucfirst(__('validation.custom.subtitle')) }}</legend> + <div class="row"> + <div class="col-12"> + @include('bo.partials.inputs.varchar', [ + 'id' => 'newsSubtitle', + 'label' => __(':field :inter:model', [ + 'field' => Str::ucfirst(__('validation.custom.subtitle')), + 'inter' => __('validation.custom.of_the.vowel'), + 'model' => __('models.classes.news'), + ]), + 'hasWordCounter' => true, + 'name' => 'subtitle', + 'required' => true, + 'model' => $newsModel ?? null, + 'helper' => __('validation.between.string', [ + 'attribute' => __(':field :inter:model', [ + 'field' => Str::ucfirst(__('validation.custom.subtitle')), + 'inter' => __('validation.custom.of_the.female'), + 'model' => __('models.classes.news'), + ]), + 'min' => 3, + 'max' => 255, + ]), + 'placeholder' => __('Un sous-titre clair et concis'), + ]) + </div> + </div> + </fieldset> + </div> + {{-- CATEGORY --}} + <div class="col-12 col-md-6 mb-3"> + <fieldset class="h-100 border p-3"> + <legend>{{ Str::ucfirst(__('validation.custom.category')) }}</legend> + <div class="row"> + <div class="col-12"> + @include('bo.partials.inputs.tinyint-enum', [ + 'id' => 'newsCategory', + 'label' => __('Associer une catégorie'), + 'name' => 'category', + 'model' => $newsModel ?? null, + 'required' => true, + 'associatedModels' => \App\Enums\News\CategoryEnum::toArray(), + 'helper' => __('Sélectionnez une catégorie dans la liste déroulante.'), + ]) + </div> + </div> + </fieldset> + </div> + {{-- CONTENT --}} + <div class="col-12 mb-3"> + <fieldset class="border p-3"> + <legend>{{ Str::ucfirst(__('validation.attributes.content')) }}</legend> + <div class="row mb-3"> + <div class="col-12"> + @include('bo.partials.inputs.text-html', [ + 'id' => 'newsContent', + 'label' => __(':field :inter:model', [ + 'field' => Str::ucfirst(__('validation.attributes.content')), + 'inter' => __('validation.custom.of_the.vowel'), + 'model' => __('models.classes.news'), + ]), + 'name' => 'content', + 'model' => $newsModel ?? null, + 'required' => true, + 'helper' => __('crud.minicrud.text_warning'), + ]) + </div> + </div> + </fieldset> + </div> + {{-- PICTURE --}} + <div class="col-12 pb-3"> + <fieldset class="h-100 border p-3"> + <legend>{{ Str::ucfirst(__('validation.attributes.image')) }}</legend> + <div class="row mb-3"> + {{-- PICTURE_ALT --}} + <div class="col-12 col-md-6 m-md-0 mb-3"> + @include('bo.partials.inputs.varchar', [ + 'id' => 'newsPictureAlt', + 'label' => __(':field1 :inter1:field2 :inter2:model', [ + 'field1' => Str::ucfirst(__('validation.custom.attribute_alt')), + 'inter1' => __('validation.custom.of_the.vowel'), + 'field2' => __('validation.attributes.image'), + 'inter2' => __('validation.custom.of_the.vowel'), + 'model' => __('models.classes.news'), + ]), + 'hasWordCounter' => true, + 'name' => 'picture_alt', + 'required' => true, + 'model' => $newsModel ?? null, + 'helper' => __('crud.minicrud.picture_alt_helper'), + 'placeholder' => __(':field1 :inter1:field2 :inter2:model', [ + 'field1' => Str::ucfirst(__('validation.custom.attribute_alt')), + 'inter1' => __('validation.custom.of_the.vowel'), + 'field2' => __('validation.attributes.image'), + 'inter2' => __('validation.custom.of_the.vowel'), + 'model' => __('models.classes.news'), + ]), + ]) + </div> + {{-- PICTURE_TITLE --}} + <div class="col-12 col-md-6 mb-3"> + @include('bo.partials.inputs.varchar', [ + 'id' => 'newsPictureTitle', + 'label' => __(':field1 :inter1:field2 :inter2:model', [ + 'field1' => Str::ucfirst(__('validation.custom.attribute_title')), + 'inter1' => __('validation.custom.of_the.vowel'), + 'field2' => __('validation.attributes.image'), + 'inter2' => __('validation.custom.of_the.vowel'), + 'model' => __('models.classes.news'), + ]), + 'hasWordCounter' => true, + 'name' => 'picture_title', + 'required' => true, + 'model' => $newsModel ?? null, + 'helper' => __('crud.minicrud.picture_title_helper'), + 'placeholder' => __(':field1 :inter1:field2 :inter2:model', [ + 'field1' => Str::ucfirst(__('validation.custom.attribute_title')), + 'inter1' => __('validation.custom.of_the.vowel'), + 'field2' => __('validation.attributes.image'), + 'inter2' => __('validation.custom.of_the.vowel'), + 'model' => __('models.classes.news'), + ]), + ]) + </div> + {{-- PICTURE --}} + <div class="col-12"> + @include('bo.partials.inputs.varchar-picture', [ + 'id' => 'newsPicture', + 'name' => 'picture', + 'width' => 1002, + 'height' => 565, + 'model' => $newsModel ?? null, + 'helper' => __('crud.minicrud.dimensions_must_be', [ + 'widthpx' => '1002px', + 'heightpx' => '565px', + ]), + 'required' => true, + 'preview' => true, + ]) + </div> + </div> + </fieldset> + </div> + {{-- PICTURE_THUMBNAIL --}} + <div class="col-12 pb-3"> + <fieldset class="h-100 border p-3"> + <legend>{{ Str::ucfirst(__('validation.custom.picture_thumbnail')) }}</legend> + <div class="row mb-3"> + {{-- PICTURE_THUMBNAIL_ALT --}} + <div class="col-12 col-md-6 m-md-0 mb-3"> + @include('bo.partials.inputs.varchar', [ + 'id' => 'newsPictureThumbnailAlt', + 'label' => __(':field1 :inter1:field2 :inter2:model', [ + 'field1' => Str::ucfirst(__('validation.custom.attribute_alt')), + 'inter1' => __('validation.custom.of_the.female'), + 'field2' => __('validation.custom.picture_thumbnail'), + 'inter2' => __('validation.custom.of_the.vowel'), + 'model' => __('models.classes.news'), + ]), + 'hasWordCounter' => true, + 'name' => 'picture_thumbnail_alt', + 'required' => true, + 'model' => $newsModel ?? null, + 'helper' => __('crud.minicrud.picture_alt_helper'), + 'placeholder' => __(':field1 :inter1:field2 :inter2:model', [ + 'field1' => Str::ucfirst(__('validation.custom.attribute_alt')), + 'inter1' => __('validation.custom.of_the.female'), + 'field2' => __('validation.custom.picture_thumbnail'), + 'inter2' => __('validation.custom.of_the.vowel'), + 'model' => __('models.classes.news'), + ]), + ]) + </div> + {{-- PICTURE_THUMBNAIL_TITLE --}} + <div class="col-12 col-md-6 mb-3"> + @include('bo.partials.inputs.varchar', [ + 'id' => 'newsPictureThumbnailTitle', + 'label' => __(':field1 :inter1:field2 :inter2:model', [ + 'field1' => Str::ucfirst(__('validation.custom.attribute_title')), + 'inter1' => __('validation.custom.of_the.female'), + 'field2' => __('validation.custom.picture_thumbnail'), + 'inter2' => __('validation.custom.of_the.vowel'), + 'model' => __('models.classes.news'), + ]), + 'hasWordCounter' => true, + 'name' => 'picture_thumbnail_title', + 'required' => true, + 'model' => $newsModel ?? null, + 'helper' => __('crud.minicrud.picture_title_helper'), + 'placeholder' => __(':field1 :inter1:field2 :inter2:model', [ + 'field1' => Str::ucfirst(__('validation.custom.attribute_title')), + 'inter1' => __('validation.custom.of_the.female'), + 'field2' => __('validation.custom.picture_thumbnail'), + 'inter2' => __('validation.custom.of_the.vowel'), + 'model' => __('models.classes.news'), + ]), + ]) + </div> + {{-- PICTURE_THUMBNAIL --}} + <div class="col-12"> + @include('bo.partials.inputs.varchar-picture', [ + 'id' => 'newsPictureThumbnail', + 'name' => 'picture_thumbnail', + 'width' => 265, + 'height' => 354, + 'model' => $newsModel ?? null, + 'helper' => __('crud.minicrud.dimensions_must_be', [ + 'widthpx' => '265px', + 'heightpx' => '354px', + ]), + 'required' => true, + 'preview' => true, + ]) + </div> + </div> + </fieldset> + </div> + {{-- PUBLISHED --}} + <div class="col-12 col-md-6 mb-3"> + <fieldset class="h-100 border p-3"> + <legend>{{ __('crud.minicrud.visibility_label') }}</legend> + <div class="row"> + <div class="col-12"> + @include('bo.partials.inputs.tinyint-boolean', [ + 'id' => 'newsPublished', + 'name' => 'published', + 'model' => $newsModel ?? null, + 'label' => __('crud.minicrud.visibility_label'), + 'helper' => sprintf('%s %s', __('crud.minicrud.visibility_helper'), __('crud.helpers.visibility_with_dates')), + ]) + </div> + </div> + </fieldset> + </div> + {{-- PUBLICATION --}} + <div class="col-12 mb-3"> + <fieldset class="h-100 border p-3"> + <legend>{{ __('Publication') }}</legend> + <div class="row mb-3"> + <div class="col"> + <div class="form-group"> + @include('bo.partials.inputs.timestamp-datetime', [ + 'label' => Str::ucfirst(__('validation.custom.publish_at')) . __(' (Europe/Paris)'), + 'id' => 'newsPublishedAt', + 'name' => 'publish_at', + 'model' => $newsModel ?? null, + 'helper' => __('crud.helpers.publish_at', [ + 'model' => __('models.classes.news'), + ]), + ]) + </div> + </div> + <div class="col"> + <div class="form-group"> + @include('bo.partials.inputs.timestamp-datetime', [ + 'label' => Str::ucfirst(__('validation.custom.unpublish_at')) . __(' (Europe/Paris)'), + 'id' => 'newsUnPublishedAt', + 'name' => 'unpublish_at', + 'model' => $newsModel ?? null, + 'helper' => __('crud.helpers.unpublish_at', [ + 'model' => __('models.classes.news'), + ]), + ]) + </div> + </div> + </div> + </fieldset> + </div> + +</div> + +<div class="row"> + <div class="col text-center"> + <p><b>{{ __('* Champs obligatoires') }}</b></p> + </div> +</div> + +@push('scripts') + <script nonce="{{ $nonce }}"> + document.addEventListener("DOMContentLoaded", () => { + // Submit button clone. + const submit = document.getElementById('formSubmit') + submitClone = document.getElementById('formSubmitClone'); + submitClone.addEventListener('click', (event) => { + event.preventDefault(); + submit.click(''); + }) + }); + </script> +@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..42f19e0 --- /dev/null +++ b/resources/views/bo/pages/news/index-list.blade.php @@ -0,0 +1,83 @@ +<table class="table-hover table-fix-action m-0 table"> + @if (count($newsModels) > 0) + <thead> + @include('bo.modules.table-col-sorter', [ + 'cols' => [ + 'title' => Str::ucfirst(__('validation.attributes.title')), + 'category' => Str::ucfirst(__('validation.custom.category')), + 'picture' => Str::ucfirst(__('validation.attributes.image')), + 'order' => Str::ucfirst(__('validation.custom.order')), + 'published' => Str::ucfirst(__('validation.custom.activated')), + 'updated_at' => Str::ucfirst(__('validation.attributes.updated_at')), + ], + ]) + </thead> + <tbody> + @foreach ($newsModels as $newsModel) + <tr class="border-bottom list-item"> + <td class="text-center align-middle">{{ $newsModel->title }}</td> + <td class="text-center align-middle">{{ $newsModel->category->label() }}</td> + <td class="text-center align-middle"> + <img height="50px" src="/{{ $newsModel->picture }}"> + </td> + @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, + ]) + <td class="text-center align-middle"> + <span class="badge bg-secondary">{{ $newsModel->updated_at->isoFormat('LLLL') }}</span> + </td> + <td class="text-end align-middle"> + @canAny(['delete', 'duplicate', 'update'], $newsModel) + <form class="btn-group confirmDeleteTS" action="{{ route('bo.news.destroy', $newsModel) }}" method="POST" + novalidate> + @can('duplicate', $newsModel) + <a class="btn btn-sm btn-secondary" data-bs-toggle="tooltip" + href="{{ route('bo.news.duplicate', ['news' => $newsModel]) }}" + title="{{ Str::ucfirst(__('crud.actions.duplicate')) }}"> + <i class="fa-solid fa-copy"></i> + </a> + @endcan + @can('update', $newsModel) + <a class="btn btn-sm btn-primary" data-bs-toggle="tooltip" + href="{{ route('bo.news.edit', ['news' => $newsModel]) }}" + title="{{ Str::ucfirst(__('crud.actions.update')) }}"> + <i class="fa-solid fa-pencil"></i> + </a> + @endcan + @can('delete', $newsModel) + @csrf + @method('DELETE') + <button class="btn btn-sm btn-danger" data-bs-toggle="tooltip" type="submit" + title="{{ Str::ucfirst(__('crud.actions.delete')) }}"> + <i class="fa-solid fa-trash"></i> + </button> + @endcan + </form> + @else + <span class="text-danger" data-bs-toggle="tooltip" title="{{ __('list.right') }}"> + <i class="fa-solid fa-ban"></i> + </span> + @endcan + </td> + </tr> + @endforeach + </tbody> + @else + <tr> + <td class="border-0">{{ __('crud.other.no_model_found', ['model' => __('models.classes.news')]) }}</td> + </tr> + @endif +</table> 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') + <div class="d-flex justify-content-between flex-md-nowrap align-items-center flex-wrap"> + @include('modules.breadcrumbs.breadcrumb-body') + @can('create', \App\Models\News::class) + <a class="btn btn-primary float-right" data-bs-toggle="tooltip" href="{{ route('bo.news.create') }}" + title="{{ __('crud.helpers.create_model', ['model' => __('models.classes.news')]) }}"> + <i class="fa-solid fa-plus"></i> + </a> + @endcan + </div> + @include('bo.modules.search-bar') + <div class="table-responsive mb-3"> + @include('bo.pages.news.index-list') + </div> + {{ $newsModels->links() }} +@endsection diff --git a/resources/views/bo/pages/ranges/form.blade.php b/resources/views/bo/pages/ranges/form.blade.php index c0cf11c..deae11e 100644 --- a/resources/views/bo/pages/ranges/form.blade.php +++ b/resources/views/bo/pages/ranges/form.blade.php @@ -95,7 +95,7 @@ 'model' => __('models.classes.range'), ]), 'min' => 3, - 'max' => 255, + 'max' => 100, ]), 'placeholder' => __('Un titre clair et concis'), ]) diff --git a/routes/breadcrumbs-bo.php b/routes/breadcrumbs-bo.php index d4659fe..6f3b70f 100644 --- a/routes/breadcrumbs-bo.php +++ b/routes/breadcrumbs-bo.php @@ -4,6 +4,7 @@ use App\Models\ActivityLog; use App\Models\Commitment; use App\Models\Range; use App\Models\SeasonalSection; +use App\Models\News; use App\Models\StaticPage; use App\Models\User; use Diglactic\Breadcrumbs\Breadcrumbs; @@ -113,3 +114,23 @@ Breadcrumbs::for('bo.seasonalsections.duplicate', function (Generator $trail) { $trail->parent('bo.seasonalsections.index'); $trail->push(Str::ucfirst(trans('crud.actions.duplicate'))); }); + +// * 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'))); +}); diff --git a/routes/web-bo.php b/routes/web-bo.php index 6ab8d2a..2383b0b 100644 --- a/routes/web-bo.php +++ b/routes/web-bo.php @@ -3,6 +3,7 @@ use App\Http\Controllers\Bo\ActivityLogsController; use App\Http\Controllers\Bo\CommitmentController; use App\Http\Controllers\Bo\DashboardController; +use App\Http\Controllers\Bo\NewsController; use App\Http\Controllers\Bo\StaticPageController; use App\Http\Controllers\Bo\RangeController; use App\Http\Controllers\Bo\SeasonalSectionController; @@ -65,7 +66,9 @@ Route::prefix('bo') ->middleware('htmlsanitize:content'); // * RANGES. - Route::resource('ranges', RangeController::class)->except('show'); + Route::resource('ranges', RangeController::class)->except('show') + ->withoutMiddleware('htmlsanitize') + ->middleware('htmlsanitize:description'); Route::patch('/ranges/{range}/change-published', [ RangeController::class, 'changePublished' ])->name('ranges.change-published'); @@ -88,6 +91,20 @@ Route::prefix('bo') SeasonalSectionController::class, 'duplicate' ])->name('seasonalsections.duplicate'); + // * 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'); + Route::middleware('can:isConceptor')->group(function () { // * ACTIVITY LOGS. Route::resource('activity_logs', ActivityLogsController::class)->only(['index', 'show']);