From a82dcf4204e475fe79d4766070ecbe6efa866f3d Mon Sep 17 00:00:00 2001 From: Nicolas Demangeat Date: Fri, 30 Aug 2024 15:46:52 +0200 Subject: [PATCH] feat: :sparkles: add CRUD News --- app/Http/Controllers/Bo/NewsController.php | 202 +++++++++ app/Http/Controllers/Bo/UploadController.php | 30 ++ .../Requests/Bo/News/StoreNewsRequest.php | 95 +++++ .../Requests/Bo/News/UpdateNewsRequest.php | 18 + app/Http/Requests/Bo/UploadFileRequest.php | 30 ++ app/Models/News.php | 241 +++++++++++ app/Models/Product.php | 2 +- app/Policies/NewsPolicy.php | 97 +++++ app/Providers/RouteServiceProvider.php | 2 + database/factories/NewsFactory.php | 42 ++ .../factories/assets/news/default-700x400.png | Bin 0 -> 11965 bytes .../assets/news/thumbnail-360x250.png | Bin 0 -> 5390 bytes .../2024_08_10_153246_create_news_table.php | 44 ++ database/seeders/Development/NewsSeeder.php | 41 ++ database/seeders/DevelopmentSeeder.php | 2 + resources/js/ckUploadAdapter.js | 67 +++ resources/lang/en/crud.php | 2 + resources/lang/en/models.php | 1 + resources/lang/en/validation.php | 1 + resources/lang/fr/crud.php | 4 + resources/lang/fr/models.php | 1 + resources/lang/fr/validation.php | 1 + .../ts/bo/components/CkEditorComponent.vue | 8 + resources/views/bo/layouts/sidemenu.blade.php | 16 +- .../views/bo/pages/news/create.blade.php | 36 ++ resources/views/bo/pages/news/edit.blade.php | 58 +++ resources/views/bo/pages/news/form.blade.php | 403 ++++++++++++++++++ .../views/bo/pages/news/index-list.blade.php | 80 ++++ resources/views/bo/pages/news/index.blade.php | 21 + resources/views/bo/pages/news/show.blade.php | 131 ++++++ .../bo/pages/static_pages/form.blade.php | 5 +- .../bo/partials/inputs/text-html.blade.php | 1 + .../views/modules/script-routes.blade.php | 1 + routes/breadcrumbs-bo.php | 25 ++ routes/web-bo.php | 16 + 35 files changed, 1720 insertions(+), 4 deletions(-) create mode 100644 app/Http/Controllers/Bo/NewsController.php create mode 100644 app/Http/Controllers/Bo/UploadController.php create mode 100644 app/Http/Requests/Bo/News/StoreNewsRequest.php create mode 100644 app/Http/Requests/Bo/News/UpdateNewsRequest.php create mode 100644 app/Http/Requests/Bo/UploadFileRequest.php create mode 100644 app/Models/News.php create mode 100644 app/Policies/NewsPolicy.php create mode 100644 database/factories/NewsFactory.php create mode 100644 database/factories/assets/news/default-700x400.png create mode 100644 database/factories/assets/news/thumbnail-360x250.png create mode 100644 database/migrations/2024_08_10_153246_create_news_table.php create mode 100644 database/seeders/Development/NewsSeeder.php create mode 100644 resources/js/ckUploadAdapter.js create mode 100644 resources/views/bo/pages/news/create.blade.php create mode 100644 resources/views/bo/pages/news/edit.blade.php create mode 100644 resources/views/bo/pages/news/form.blade.php create mode 100644 resources/views/bo/pages/news/index-list.blade.php create mode 100644 resources/views/bo/pages/news/index.blade.php create mode 100644 resources/views/bo/pages/news/show.blade.php diff --git a/app/Http/Controllers/Bo/NewsController.php b/app/Http/Controllers/Bo/NewsController.php new file mode 100644 index 00000000..c61944b3 --- /dev/null +++ b/app/Http/Controllers/Bo/NewsController.php @@ -0,0 +1,202 @@ +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()->with('newsCategory'); + + /** @var string $search Search field */ + $search = $request->search; + if ($search) { + $this->searchQuery( + $newsModels, + $search, + null, + 'title', + 'content', + 'news_category_id' + ); + } + $searchFields = \implode(', ', [ + trans('validation.attributes.title'), + 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 + { + $newsCategoryModels = NewsCategory::query()->where('published', true)->orderBy('order')->get(); + return view('bo.pages.news.create', ['newsModel' => $news, 'newsCategoryModels' => $newsCategoryModels]); + } + + /** + * 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()); + $oldestNewsWithHighlight = News::query()->where('highlight', true)->orderBy('created_at')->get(); + if ($news->saveOrFail()) { + if ($news->highlight === true) { + if ($oldestNewsWithHighlight->count() >= 2) { + /** @var \App\Models\News $firstNews */ + $firstNews = $oldestNewsWithHighlight->first(); + $firstNews->highlight = false; + $firstNews->save(); + } + } + 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')) + ])); + }); + } + + /** + * Display the specified resource. + * + * @param \App\Models\News $news + * @return \Illuminate\Contracts\View\View + */ + public function show(News $news): \Illuminate\Contracts\View\View + { + return view('bo.pages.news.show', ['newsModel' => $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 + { + $newsCategoryModels = NewsCategory::query()->where('published', true)->orderBy('order')->get(); + return view('bo.pages.news.edit', ['newsModel' => $news, 'newsCategoryModels' => $newsCategoryModels]); + } + + /** + * 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()); + $oldestNewsWithHighlight = News::query()->where('highlight', true)->orderBy('created_at')->get(); + if ($news->saveOrFail()) { + if ($news->highlight === true) { + if ($oldestNewsWithHighlight->count() >= 2) { + /** @var \App\Models\News $firstNews */ + $firstNews = $oldestNewsWithHighlight->first(); + $firstNews->highlight = false; + $firstNews->save(); + } + } + return redirect()->route('bo.news.edit', $news) + ->with('success', trans('crud.messages.has_been_updated', [ + 'model' => Str::ucfirst(trans('models.classes.news')) + ])); + } + return redirect()->route('bo.news.edit', $news) + ->with('error', trans('crud.messages.cannot_be_updated', [ + 'model' => Str::ucfirst(trans('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 + { + /** @var array */ + $previousQueries = []; + \parse_str(\parse_url(url()->previous(), \PHP_URL_QUERY), $previousQueries); + if ($news->deleteOrFail()) { + return redirect()->route('bo.news.index', $previousQueries) + ->with('success', trans('crud.messages.has_been_deleted', [ + 'model' => Str::ucfirst(trans('models.classes.news')) + ])); + } + return redirect()->back() + ->with('error', trans('crud.messages.cannot_be_deleted', [ + 'model' => Str::ucfirst(trans('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/Controllers/Bo/UploadController.php b/app/Http/Controllers/Bo/UploadController.php new file mode 100644 index 00000000..28f54ea0 --- /dev/null +++ b/app/Http/Controllers/Bo/UploadController.php @@ -0,0 +1,30 @@ +upload; + + $path = FileStorageHelper::storeFile(new HistoricalMilestone(), $image, true); + + return response()->json([ + 'url' => "/{$path}" + ]); + } +} diff --git a/app/Http/Requests/Bo/News/StoreNewsRequest.php b/app/Http/Requests/Bo/News/StoreNewsRequest.php new file mode 100644 index 00000000..a90d35c9 --- /dev/null +++ b/app/Http/Requests/Bo/News/StoreNewsRequest.php @@ -0,0 +1,95 @@ +mergePicture('picture'); + $this->mergePicture('thumbnail'); + $this->merge([ + 'published' => $this->boolean('published'), + 'highlight' => $this->boolean('highlight'), + ]); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules(): array + { + $rules = [ + 'seo_title' => 'required|string|between:45,70', + 'seo_description' => 'required|string|between:50,160', + 'title' => 'required|string|between:3,255', + 'subtitle' => 'sometimes|nullable|string|between:3,255', + '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', + 'news_category_id' => 'required|integer', + 'content' => ['required', 'string', new WysiwygLength()], + 'highlight' => 'required|boolean', + 'published' => 'required|boolean', + 'publish_at' => 'sometimes|nullable|date', + 'unpublish_at' => 'sometimes|nullable|date|after:publish_at', + ]; + return \array_merge( + $rules, + $this->pictureRules('picture', 700, 400, 700, 400), + $this->pictureRules('thumbnail', 360, 250, 360, 250), + ); + } + + /** + * Get custom attributes for validator errors. + * + * @return array + */ + public function attributes(): array + { + return [ + 'seo_title' => trans('validation.attributes.title'), + 'seo_description' => trans('validation.attributes.title'), + 'title' => trans('validation.attributes.title'), + 'subtitle' => trans('validation.attributes.subtitle'), + 'content' => trans('validation.attributes.content'), + 'news_category_id' => trans('validation.custom.category'), + 'picture' => trans('validation.attributes.image'), + 'picture_alt' => trans('validation.custom.attribute_alt'), + 'picture_title' => trans('validation.custom.attribute_title'), + 'thumbnail_alt' => trans('validation.custom.attribute_alt'), + 'thumbnail_title' => trans('validation.custom.attribute_title'), + 'published' => trans('validation.attributes.published'), + 'highlight' => trans('validation.attributes.highlight'), + '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 00000000..debb4ea5 --- /dev/null +++ b/app/Http/Requests/Bo/News/UpdateNewsRequest.php @@ -0,0 +1,18 @@ +route('news')); + } +} diff --git a/app/Http/Requests/Bo/UploadFileRequest.php b/app/Http/Requests/Bo/UploadFileRequest.php new file mode 100644 index 00000000..b6b63479 --- /dev/null +++ b/app/Http/Requests/Bo/UploadFileRequest.php @@ -0,0 +1,30 @@ +check() and $this->ajax(); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules(): array + { + return [ + 'upload' => 'required|image|mimes:jpeg,png,jpg|max:2048' + ]; + } +} diff --git a/app/Models/News.php b/app/Models/News.php new file mode 100644 index 00000000..698d03ff --- /dev/null +++ b/app/Models/News.php @@ -0,0 +1,241 @@ + + */ + protected $fillable = [ + 'seo_title', + 'seo_description', + 'title', + 'subtitle', + 'slug', + 'picture', + 'picture_alt', + 'picture_title', + 'thumbnail', + 'thumbnail_alt', + 'thumbnail_title', + 'news_category_id', + 'content', + 'highlight', + 'published', + 'publish_at', + 'unpublish_at', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'publish_at' => 'datetime', + 'unpublish_at' => 'datetime', + 'published' => 'boolean', + 'highlight' => 'boolean', + ]; + + /** + * Perform any actions required after the model boots. + * + * @return void + */ + protected static function booted(): void + { + static::creating(function (self $news) { + self::makeStatusSense($news); + self::assertTitleIsUnique($news->title); + self::manageFields($news); + }); + static::updating(function (self $news) { + self::makeStatusSense($news); + self::assertTitleIsUnique($news->title, $news->getKey()); + self::manageFields($news); + }); + static::updated(function (self $news) { + FileStorageHelper::removeOldFile($news, 'picture'); + FileStorageHelper::removeOldFile($news, 'thumbnail'); + }); + static::deleted(function (self $news) { + FileStorageHelper::removeFile($news, 'picture'); + FileStorageHelper::removeFile($news, 'thumbnail'); + }); + } + + // * METHODS + + /** + * Adjusts news status using current status and publish dates + * + * @param self $news + * @return void + */ + private static function makeStatusSense(self $news): void + { + /** @var \Carbon\Carbon|null */ + $publish_at = $news->publish_at; + /** @var \Carbon\Carbon|null */ + $unpublish_at = $news->unpublish_at; + $wantedStatus = $news->published; + if ($publish_at or $unpublish_at) { + $news->published = $news->planificated_status; + } + if ($wantedStatus !== $news->published) { + session()->flash('bo.info', \trans( + 'La publication est modifiée en concordance avec les dates enregistrées. + Pour modifier l\'état de publication, veuillez modifier ou enlever les dates correspondantes.' + )); + } + } + + /** + * Getter the current status following planification dates planificated_status + * + * @return boolean + */ + public function getPlanificatedStatusAttribute(): bool + { + /** @var \Carbon\Carbon|null */ + $publish_at = $this->publish_at; + /** @var \Carbon\Carbon|null */ + $unpublish_at = $this->unpublish_at; + if ($publish_at and $unpublish_at) { + return $unpublish_at->greaterThanOrEqualTo(now()) and $publish_at->lessThan(now()); + } + if ($publish_at) { + return $publish_at->lessThan(now()); + } + if ($unpublish_at) { + return $unpublish_at->greaterThanOrEqualTo(now()); + } + return false; + } + + /** + * Manage model fields. + * + * @param self $model + * @return void + */ + private static function manageFields(self $model): void + { + $model->slug = str($model->title)->slug(); + $model->thumbnail = FileStorageHelper::storeFile($model, $model->thumbnail, true); + $model->picture = FileStorageHelper::storeFile($model, $model->picture, true); + } + + /** + * Asserts using validation that the title is unique + * + * @param string|null $title + * @param integer|null $id + * @return void + * @throws \Illuminate\Validation\ValidationException If an actu title already exists. + */ + private static function assertTitleIsUnique(string|null $title, ?int $id = null): void + { + $table = (new self())->getTable(); + ToolboxHelper::assertFieldIsUnique($table, 'title', $title, $id); + ToolboxHelper::assertFieldIsUnique($table, 'slug', str($title)->slug(), $id); + } + + /** + * Get News views by News category. + * + * @param string|null $slug + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator + */ + public static function getNewsViewsByNewsCategory(string|null $slug = null): \Illuminate\Contracts\Pagination\LengthAwarePaginator + { + /** + * @var Detection\MobileDetect + */ + $detect = new MobileDetect(); + /** @var \App\Models\NewsCategory|null */ + $newsCategoryModel = !is_null($slug) && !empty($slug) ? NewsCategory::query()->where('slug', $slug)->first() : null; + return News::query() + ->with('newsCategory') + ->when(!is_null($newsCategoryModel), function (Builder $q) use ($newsCategoryModel) { + $q->whereHas('newsCategory', function (Builder $q) use ($newsCategoryModel) { + $q->where('id', $newsCategoryModel->getKey()); + }); + }) + ->where('published', true) + ->orderByDesc('created_at') + ->paginate($detect->isMobile() ? 3 : 6) + ->through(function ($newsModel) { + return view('fo.partials.news-card', ['newsModel' => $newsModel])->render(); + }); + } + + /** + * Get Social news views by filter social category. + * + * @param string|null $slug + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator + */ + public static function getSocialViewsByFilter(string|null $slug = null): \Illuminate\Contracts\Pagination\LengthAwarePaginator + { + /** + * @var Detection\MobileDetect + */ + $detect = new MobileDetect(); + /** @var \App\Models\NewsCategory|null */ + $socialCategory = !is_null($slug) && !empty($slug) ? NewsCategory::query()->where('slug', $slug)->first() : null; + return News::query() + ->with('newsCategory') + ->when(!is_null($socialCategory), function (Builder $q) use ($socialCategory) { + $q->whereHas('newsCategory', function (Builder $q) use ($socialCategory) { + $q->where('id', $socialCategory->getKey()); + }); + }) + ->where('published', true) + ->orderByDesc('created_at') + ->paginate($detect->isMobile() ? 4 : 12) + ->through(function ($socialModel) { + return view('fo.partials.news-card', ['newsModel' => $socialModel])->render(); + }); + } + + // * RELATIONS + + /** + * Get the category that owns the news. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function newsCategory(): \Illuminate\Database\Eloquent\Relations\BelongsTo + { + return $this->belongsTo(NewsCategory::class); + } +} diff --git a/app/Models/Product.php b/app/Models/Product.php index 9e6dde8a..2800564a 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -160,7 +160,7 @@ class Product extends Model implements SeoInterface { $rangeModel = !is_null($slug) ? Range::query()->where('slug', $slug)->first() : - Range::query()->orderBy('slug', 'ASC')->first(); + Range::query()->orderBy('slug', 'ASC')->where('published', true)->first(); return Product::query()->whereHas('range', function (Builder $q) use ($rangeModel) { $q->where('id', $rangeModel->getKey()); })->inRandomOrder() diff --git a/app/Policies/NewsPolicy.php b/app/Policies/NewsPolicy.php new file mode 100644 index 00000000..c2ce2f16 --- /dev/null +++ b/app/Policies/NewsPolicy.php @@ -0,0 +1,97 @@ + + */ +class NewsFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + $publishedStatus = fake()->boolean(75); + $publish_at = Carbon::now()->addMonth(); + $title = fake()->unique()->words(rand(1, 2), true); + return [ + 'seo_title' => fake()->sentence(), + 'seo_description' => fake()->sentence(), + 'title' => $title, + 'subtitle' => fake()->sentence(), + 'content' => fake()->text(), + 'slug' => Str::slug($title), + 'picture_alt' => fake()->sentence(), + 'picture_title' => fake()->sentence(), + 'thumbnail_alt' => fake()->sentence(), + 'thumbnail_title' => fake()->sentence(), + 'highlight' => false, + 'news_category_id' => 1, + 'published' => $publishedStatus, + 'publish_at' => !$publishedStatus ? $publish_at : null, + 'unpublish_at' => !$publishedStatus ? $publish_at->copy()->addDays(rand(10, 20)) : null, + ]; + } +} diff --git a/database/factories/assets/news/default-700x400.png b/database/factories/assets/news/default-700x400.png new file mode 100644 index 0000000000000000000000000000000000000000..5e99d244ede02f799205ac2132d6eae556d8f27b GIT binary patch literal 11965 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 + $table->string('seo_title')->comment('The seo title of the news.'); + $table->string('seo_description')->comment('The seo description of the news.'); + $table->string('title')->comment('The title of the news.'); + $table->string('subtitle')->nullable()->comment('The title of the news.'); + $table->string('picture')->comment('Picture path.'); + $table->string('picture_alt')->comment('Picture alt attribute.'); + $table->string('picture_title')->comment('Picture title attribute.'); + $table->string('thumbnail')->comment('Thumbnail path.'); + $table->string('thumbnail_alt')->comment('Thumbnail alt attribute.'); + $table->string('thumbnail_title')->comment('Thumbnail title attribute.'); + $table->string('slug')->unique()->comment('Unique title slug'); + $table->text('content')->comment('The content of the news.'); + $table->foreignId('news_category_id')->constrained('news_categories')->restrictOnDelete()->comment('News category id'); + $table->boolean('highlight')->comment('Highlight status.'); + $table->boolean('published')->comment('News publish status.'); + $table->timestamp('publish_at')->nullable()->comment('News publishment date.'); + $table->timestamp('unpublish_at')->nullable()->comment('News unpublishment date.'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('news'); + } +}; diff --git a/database/seeders/Development/NewsSeeder.php b/database/seeders/Development/NewsSeeder.php new file mode 100644 index 00000000..c9ac91b9 --- /dev/null +++ b/database/seeders/Development/NewsSeeder.php @@ -0,0 +1,41 @@ +make()->each(function (News $news) { + $news->picture = FileStorageHelper::storeFile( + $news, + new \SplFileInfo(\resource_path( + '../database/factories/assets/news/default-700x400.png' + )) + ); + $news->thumbnail = FileStorageHelper::storeFile( + $news, + new \SplFileInfo(\resource_path( + '../database/factories/assets/news/thumbnail-360x250.png' + )) + ); + $news->saveQuietly(); + }); + } +} diff --git a/database/seeders/DevelopmentSeeder.php b/database/seeders/DevelopmentSeeder.php index 2e52de1c..74999c40 100644 --- a/database/seeders/DevelopmentSeeder.php +++ b/database/seeders/DevelopmentSeeder.php @@ -23,6 +23,7 @@ use Database\Seeders\Development\SubrangeSeeder; use Database\Seeders\Development\TestimonialSeeder; use Database\Seeders\Development\UserSeeder; use Database\Seeders\Development\NewsCorporateSeeder; +use Database\Seeders\Development\NewsSeeder; use Illuminate\Database\Seeder; class DevelopmentSeeder extends Seeder @@ -56,6 +57,7 @@ class DevelopmentSeeder extends Seeder NewsCorporateSeeder::class, TestimonialSeeder::class, NewsCategorySeeder::class, + NewsSeeder::class, ]); } } diff --git a/resources/js/ckUploadAdapter.js b/resources/js/ckUploadAdapter.js new file mode 100644 index 00000000..19ae8ed0 --- /dev/null +++ b/resources/js/ckUploadAdapter.js @@ -0,0 +1,67 @@ +import axios from "axios"; +import route from "../ts/modules/route"; + +class ckUploadAdapter { + constructor(loader) { + // The file loader instance to use during the upload. + this.loader = loader; + } + + // Starts the upload process. + upload() { + return this.loader.file.then( + (file) => + new Promise((resolve, reject) => { + this._initRequest(); + this._sendRequest(file, resolve, reject); + }), + ); + } + + // Aborts the upload process. + abort() { + if (this.cancel) { + this.cancel.abort(); + } + } + + // Initializes the XMLHttpRequest object using the URL passed to the constructor. + _initRequest() { + this.cancel = axios.CancelToken.source(); + this.route = route.methods.route("bo.upload"); + } + + // Prepares the data and sends the request. + _sendRequest(file, resolve, reject) { + // Prepare the form data. + const data = new FormData(); + data.append("upload", file); + axios + .post(this.route, data, { + headers: { + "Content-Type": "multipart/form-data", + }, + onUploadProgress: (evt) => { + this.loader.uploadTotal = evt.total; + this.loader.uploaded = evt.loaded; + }, + }) + .then((response) => { + resolve({ + default: response.data.url, + }); + }) + .catch((error) => { + console.error(error); + reject("Le fichier à été marbré"); + }); + } +} + +function CkUploadAdapterPlugin(editor) { + editor.plugins.get("FileRepository").createUploadAdapter = (loader) => { + return new ckUploadAdapter(loader); + }; +} + +export default CkUploadAdapterPlugin; diff --git a/resources/lang/en/crud.php b/resources/lang/en/crud.php index 81978fc6..06160129 100644 --- a/resources/lang/en/crud.php +++ b/resources/lang/en/crud.php @@ -62,7 +62,9 @@ return [ 'minicrud' => [ 'visibility_label' => 'Visibility', + 'highlight_label' => 'Highlight', 'visibility_helper' => 'Check to make it visible.', + 'highlight_helper' => 'Check to make it highlight.', 'select_entity' => 'Select :entity from the list.', 'associated_entity' => 'Associated :entity', 'dimensions_must_be' => 'Must be :widthpx (width) by :heightpx (height).', diff --git a/resources/lang/en/models.php b/resources/lang/en/models.php index 57939b40..a3ad9ac4 100644 --- a/resources/lang/en/models.php +++ b/resources/lang/en/models.php @@ -27,6 +27,7 @@ return [ 'claim' => 'claim', 'historical_milestone' => 'historical milestone|historical milestones', 'offer' => 'offer', + 'news' => 'news', 'news_corporate' => 'news of the compagny', 'news_category' => 'category of the news', ] diff --git a/resources/lang/en/validation.php b/resources/lang/en/validation.php index 032ea3ff..2368225c 100644 --- a/resources/lang/en/validation.php +++ b/resources/lang/en/validation.php @@ -289,6 +289,7 @@ return [ 'sustitle' => 'sustitle', 'target' => 'target', 'tips' => 'tips', + 'thumbnail' => 'thumbnail', 'type' => 'type', 'unit' => 'unit', 'unpublish_at' => 'unpublish at', diff --git a/resources/lang/fr/crud.php b/resources/lang/fr/crud.php index 66226f2d..4ef24d27 100644 --- a/resources/lang/fr/crud.php +++ b/resources/lang/fr/crud.php @@ -63,7 +63,11 @@ return [ 'minicrud' => [ 'visibility_label' => 'Visibilité', + 'highlight_label' => 'Mise en avant', + 'category_label' => 'Catégorie de l\'article', 'visibility_helper' => 'Cocher pour rendre visible.', + 'highlight_helper' => 'Cocher pour mettre en avant.', + 'category_helper' => 'Séléctionner la categorie.', 'select_entity' => 'Sélectionnez :entity dans la liste.', 'associated_entity' => ':entity associé(e)', 'dimensions_must_be' => 'Doit être :widthpx (largeur) par :heightpx (hauteur).', diff --git a/resources/lang/fr/models.php b/resources/lang/fr/models.php index 25941eb7..04719ec4 100644 --- a/resources/lang/fr/models.php +++ b/resources/lang/fr/models.php @@ -27,6 +27,7 @@ return [ 'claim' => 'allégations', 'historical_milestone' => 'jalon historique|jalons historique', 'offer' => 'offre', + 'news' => 'actualités', 'news_corporate' => 'actualités de l\'entreprise', 'news_category' => 'catégorie d\'une actualité', ] diff --git a/resources/lang/fr/validation.php b/resources/lang/fr/validation.php index 62115e60..30d88a4a 100644 --- a/resources/lang/fr/validation.php +++ b/resources/lang/fr/validation.php @@ -291,6 +291,7 @@ return [ 'sustitle' => 'sous-titre', 'target' => 'cible', 'tips' => 'astuce', + 'thumbnail' => 'vignette', 'type' => 'type', 'unit' => 'unité', 'unpublish_at' => 'date de fin de publication', diff --git a/resources/ts/bo/components/CkEditorComponent.vue b/resources/ts/bo/components/CkEditorComponent.vue index 72cd7591..65a1261e 100644 --- a/resources/ts/bo/components/CkEditorComponent.vue +++ b/resources/ts/bo/components/CkEditorComponent.vue @@ -20,6 +20,8 @@ import { computed, defineOptions, onMounted, ref, useAttrs, type PropType } from "vue"; import parseLaravelErrorMessages from "../../modules/parseLaravelErrorMessages"; import "./../../../js/vendor/ckeditor"; +// @ts-ignore +import CkUploadAdapterPlugin from "./../../../js/ckUploadAdapter"; defineOptions({ name: "CkEditorComponent" @@ -184,6 +186,7 @@ onMounted(() => { // * Differents CkEditor configs. const minimalConfig = { + extraPlugins: [], wordCount: ckEditorGlobals.wordCount, ui: ckEditorGlobals.ui, placeholder: placeholder.value, @@ -227,6 +230,9 @@ onMounted(() => { }; const basicConfig = { + extraPlugins: [ + CkUploadAdapterPlugin, + ], wordCount: ckEditorGlobals.wordCount, ui: ckEditorGlobals.ui, placeholder: placeholder.value, @@ -277,6 +283,8 @@ onMounted(() => { "removeFormat", "|", "sourceEditing", + "|", + "imageUpload", ], }, image: { diff --git a/resources/views/bo/layouts/sidemenu.blade.php b/resources/views/bo/layouts/sidemenu.blade.php index 6dd1d3f7..5adee578 100644 --- a/resources/views/bo/layouts/sidemenu.blade.php +++ b/resources/views/bo/layouts/sidemenu.blade.php @@ -821,7 +821,8 @@ + +
+ @csrf + @include('bo.pages.news.form') + @include('bo.partials.script-button-clone') +
+
+ +
+
+
+@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 00000000..a42336f3 --- /dev/null +++ b/resources/views/bo/pages/news/edit.blade.php @@ -0,0 +1,58 @@ +@extends('bo.layout', ['brParam' => $newsModel]) + +@section('title', __('Édition d\'une :model', ['model' => __('models.classes.news')])) +@section('description', __('Édition d\'une :model déjà existant', ['model' => __('models.classes.news')])) +@section('breadcrumb', request()->route()->getName()) + +@section('content') +
+
+ + + + @include('modules.breadcrumbs.breadcrumb-body', ['brParam' => $newsModel]) +
+
+
+ @can('view', $newsModel) + + + + @endcan + @can('duplicate', $newsModel) + + + + @endcan + + @can('delete', $newsModel) + @csrf + @method('DELETE') + + @endcan +
+
+
+
+ @csrf + @method('PUT') + @include('bo.pages.news.form') + @include('bo.partials.script-button-clone') +
+
+ +
+
+
+@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 00000000..d2187619 --- /dev/null +++ b/resources/views/bo/pages/news/form.blade.php @@ -0,0 +1,403 @@ +
+ {{-- SEO_TITLE --}} +
+
+ {{ str(__('validation.custom.seo_title'))->ucfirst() }} +
+
+ @include('bo.partials.inputs.varchar', [ + 'id' => 'newsSeoTitle', + 'label' => __(':field :inter:model', [ + 'field' => str(__('validation.custom.seo_title'))->ucfirst(), + 'inter' => __('validation.custom.of_the.female'), + 'model' => trans('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' => trans('models.classes.news'), + ]), + 'min' => 45, + 'max' => 70, + ])), + 'placeholder' => __(':field :inter:model', [ + 'field' => str(__('validation.custom.seo_title'))->ucfirst(), + 'inter' => __('validation.custom.of_the.female'), + 'model' => trans('models.classes.news'), + ]), + ]) +
+
+
+
+ {{-- SEO_DESCRIPTION --}} +
+
+ {{ str(__('validation.custom.seo_description'))->ucfirst() }} +
+
+ @include('bo.partials.inputs.varchar-multiline', [ + 'id' => 'newsSeoDescription', + 'label' => __(':field :inter:model', [ + 'field' => str(__('validation.custom.seo_description'))->ucfirst(), + 'inter' => __('validation.custom.of_the.female'), + 'model' => trans('models.classes.news'), + ]), + 'name' => 'seo_description', + 'model' => $newsModel ?? null, + 'required' => true, + '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' => trans('models.classes.news'), + ]), + 'min' => 50, + 'max' => 160, + ])), + 'placeholder' => __(':field :inter:model', [ + 'field' => str(__('validation.custom.seo_description'))->ucfirst(), + 'inter' => __('validation.custom.of_the.female'), + 'model' => trans('models.classes.news'), + ]), + ]) +
+
+
+
+ {{-- TITLE --}} +
+
+ {{ str(__('validation.attributes.title'))->ucFirst() }} +
+
+ @include('bo.partials.inputs.varchar', [ + 'id' => 'newsTitle', + 'label' => __(':field :inter:model', [ + 'field' => str(__('validation.attributes.title'))->ucFirst(), + '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.vowel'), + 'model' => __('models.classes.news'), + ]), + 'min' => 3, + 'max' => 255, + ]), + 'placeholder' => __(':field :inter:model', [ + 'field' => str(__('validation.attributes.title'))->ucFirst(), + 'inter' => __('validation.custom.of_the.vowel'), + 'model' => __('models.classes.news'), + ]), + ]) +
+
+
+
+ {{-- SUBTITLE --}} +
+
+ {{ str(__('validation.custom.subtitle'))->ucFirst() }} +
+
+ @include('bo.partials.inputs.varchar', [ + 'id' => 'newsSubtitle', + 'label' => __(':field :inter:model', [ + 'field' => str(__('validation.custom.subtitle'))->ucFirst(), + 'inter' => __('validation.custom.of_the.vowel'), + 'model' => __('models.classes.news'), + ]), + 'hasWordCounter' => true, + 'name' => 'subtitle', + 'required' => false, + 'model' => $newsModel ?? null, + 'helper' => __('validation.between.string', [ + 'attribute' => __(':field :inter:model', [ + 'field' => __('validation.custom.subtitle'), + 'inter' => __('validation.custom.of_the.vowel'), + 'model' => __('models.classes.news'), + ]), + 'min' => 3, + 'max' => 255, + ]), + 'placeholder' => __(':field :inter:model', [ + 'field' => str(__('validation.custom.subtitle'))->ucFirst(), + 'inter' => __('validation.custom.of_the.vowel'), + 'model' => __('models.classes.news'), + ]), + ]) +
+
+
+
+ {{-- PICTURE --}} +
+
+ {{ str(__('validation.attributes.image'))->ucFirst() }} +
+ {{-- PICTURE_ALT --}} +
+ @include('bo.partials.inputs.varchar', [ + 'id' => 'newsPictureAlt', + 'label' => __(':field1 :inter1:field2 :inter2:model', [ + 'field1' => str(__('validation.custom.attribute_alt'))->ucFirst(), + '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(__('validation.custom.attribute_alt'))->ucFirst(), + 'inter1' => __('validation.custom.of_the.vowel'), + 'field2' => __('validation.attributes.image'), + 'inter2' => __('validation.custom.of_the.vowel'), + 'model' => __('models.classes.news'), + ]), + ]) +
+ {{-- PICTURE_TITLE --}} +
+ @include('bo.partials.inputs.varchar', [ + 'id' => 'newsPictureTitle', + 'label' => __(':field1 :inter1:field2 :inter2:model', [ + 'field1' => str(__('validation.custom.attribute_title'))->ucFirst(), + '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(__('validation.custom.attribute_title'))->ucFirst(), + 'inter1' => __('validation.custom.of_the.vowel'), + 'field2' => __('validation.attributes.image'), + 'inter2' => __('validation.custom.of_the.vowel'), + 'model' => __('models.classes.news'), + ]), + ]) +
+ {{-- PICTURE --}} +
+ @include('bo.partials.inputs.varchar-picture', [ + 'id' => 'newsPicture', + 'name' => 'picture', + 'width' => 700, + 'height' => 400, + 'model' => $newsModel ?? null, + 'required' => true, + 'preview' => true, + ]) +
+
+
+
+ {{-- THUMBNAIL --}} +
+
+ {{ str(__('validation.custom.thumbnail'))->ucFirst() }} +
+ {{-- THUMBNAIL_ALT --}} +
+ @include('bo.partials.inputs.varchar', [ + 'id' => 'newsThumbnailAlt', + 'label' => __(':field1 :inter1:field2 :inter2:model', [ + 'field1' => str(__('validation.custom.attribute_alt'))->ucFirst(), + 'inter1' => __('validation.custom.of_the.female'), + 'field2' => __('validation.custom.thumbnail'), + 'inter2' => __('validation.custom.of_the.vowel'), + 'model' => __('models.classes.news'), + ]), + 'hasWordCounter' => true, + 'name' => 'thumbnail_alt', + 'required' => true, + 'model' => $newsModel ?? null, + 'helper' => __('crud.minicrud.picture_alt_helper'), + 'placeholder' => __(':field1 :inter1:field2 :inter2:model', [ + 'field1' => str(__('validation.custom.attribute_alt'))->ucFirst(), + 'inter1' => __('validation.custom.of_the.female'), + 'field2' => __('validation.custom.thumbnail'), + 'inter2' => __('validation.custom.of_the.vowel'), + 'model' => __('models.classes.news'), + ]), + ]) +
+ {{-- THUMBNAIL_TITLE --}} +
+ @include('bo.partials.inputs.varchar', [ + 'id' => 'newsThumbnailTitle', + 'label' => __(':field1 :inter1:field2 :inter2:model', [ + 'field1' => str(__('validation.custom.attribute_title'))->ucFirst(), + 'inter1' => __('validation.custom.of_the.female'), + 'field2' => __('validation.custom.thumbnail'), + 'inter2' => __('validation.custom.of_the.vowel'), + 'model' => __('models.classes.news'), + ]), + 'hasWordCounter' => true, + 'name' => 'thumbnail_title', + 'required' => true, + 'model' => $newsModel ?? null, + 'helper' => __('crud.minicrud.picture_title_helper'), + 'placeholder' => __(':field1 :inter1:field2 :inter2:model', [ + 'field1' => str(__('validation.custom.attribute_title'))->ucFirst(), + 'inter1' => __('validation.custom.of_the.female'), + 'field2' => __('validation.custom.thumbnail'), + 'inter2' => __('validation.custom.of_the.vowel'), + 'model' => __('models.classes.news'), + ]), + ]) +
+ {{-- THUMBNAIL --}} +
+ @include('bo.partials.inputs.varchar-picture', [ + 'id' => 'newsThumbnail', + 'name' => 'thumbnail', + 'width' => 360, + 'height' => 250, + 'model' => $newsModel ?? null, + 'required' => true, + 'preview' => true, + ]) +
+
+
+
+ {{-- CONTENT --}} +
+
+ {{ str(__('validation.attributes.content'))->ucfirst() }} +
+
+ @include('bo.partials.inputs.text-html', [ + 'id' => 'newsContent', + 'label' => __(':field :inter:model', [ + 'field' => str(__('validation.attributes.content'))->ucfirst(), + 'inter' => __('validation.custom.of_the.vowel'), + 'model' => trans('models.classes.news'), + ]), + 'name' => 'content', + 'model' => $newsModel ?? null, + 'required' => true, + 'helper' => __('crud.minicrud.text_warning'), + ]) +
+
+
+
+ {{-- HIGHLIGHT --}} +
+
+ {{ str(__('validation.custom.highlight'))->ucFirst() }} +
+
+ @include('bo.partials.inputs.tinyint-boolean', [ + 'id' => 'newsHighlight ', + 'name' => 'highlight', + 'model' => $newsModel ?? null, + 'label' => __('crud.minicrud.highlight_label'), + 'helper' => __('crud.minicrud.highlight_helper'), + ]) +
+
+
+
+ {{-- CATEGORY --}} +
+
+ {{ str(__('validation.custom.category'))->ucFirst() }} +
+
+ @include('bo.partials.inputs.bigint-foreign-id', [ + 'id' => 'newsCategoryId', + 'name' => 'news_category_id', + 'model' => $newsModel ?? null, + 'associatedModels' => $newsCategoryModels, + 'foreignLabel' => 'title', + 'label' => __('crud.minicrud.category_label'), + 'helper' => __('crud.minicrud.category_helper'), + ]) +
+
+
+
+ {{-- PUBLICATION --}} +
+
+ {{ str(__('crud.minicrud.visibility_label'))->ucFirst() }} +
+ {{-- PUBLISHED --}} +
+ @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')), + ]) +
+ {{-- PUBLICATION DATES --}} +
+
+ {{-- PUBLISH_AT --}} +
+
+ @include('bo.partials.inputs.timestamp-datetime', [ + 'label' => str(__('validation.custom.publish_at'))->ucFirst() . __(' (Europe/Paris)'), + 'id' => 'newsPublishedAt', + 'name' => 'publish_at', + 'model' => $newsModel ?? null, + 'helper' => __('crud.helpers.publish_at', [ + 'model' => str(__('models.classes.news'))->ucFirst(), + ]), + ]) +
+
+ {{-- UNPUBLISH_AT --}} +
+
+ @include('bo.partials.inputs.timestamp-datetime', [ + 'label' => str(__('validation.custom.unpublish_at'))->ucfirst() . __(' (Europe/Paris)'), + 'id' => 'newsUnPublishedAt', + 'name' => 'unpublish_at', + 'model' => $newsModel ?? null, + 'helper' => __('crud.helpers.unpublish_at', [ + 'model' => str(__('models.classes.news'))->ucFirst(), + ]), + ]) +
+
+
+
+
+
+
+
+

{{ __('* Champs obligatoires') }}

+
+
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 00000000..bf785c26 --- /dev/null +++ b/resources/views/bo/pages/news/index-list.blade.php @@ -0,0 +1,80 @@ + + @if (count($newsModels) > 0) + + @include('bo.modules.table-col-sorter', [ + 'cols' => [ + 'title' => str(__('validation.attributes.title'))->ucfirst(), + 'news_category_id' => str(__('validation.custom.category'))->ucfirst(), + 'picture' => str(__('validation.attributes.image'))->ucfirst(), + 'published' => str(__('validation.custom.publication'))->ucfirst(), + 'updated_at' => str(__('validation.attributes.updated_at'))->ucfirst(), + ], + ]) + + + @foreach ($newsModels as $newsModel) + + + + + @include('bo.modules.change-published-status', [ + 'routeName' => 'news', + 'model' => $newsModel, + ]) + + + + @endforeach + + @else + + + + @endif +
{{ $newsModel->title }}{{ $newsModel->newsCategory->title }} + {{ $newsModel->picture_alt }} + + {{ $newsModel->updated_at->isoFormat('LLLL') }} + + @canAny(['show', 'duplicate', 'update', 'delete'], $newsModel) +
+ @can('view', $newsModel) + + + + @endcan + @can('duplicate', $newsModel) + + + + @endcan + @can('update', $newsModel) + + + + @endcan + @can('delete', $newsModel) + @csrf + @method('DELETE') + + @endcan +
+ @else + + + + @endcan +
+ {{ trans_choice('crud.other.no_model_found', 1, ['model' => __('models.classes.news')]) }} +
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 00000000..cf16d3f6 --- /dev/null +++ b/resources/views/bo/pages/news/index.blade.php @@ -0,0 +1,21 @@ +@extends('bo.layout') + +@section('title', str(__('models.classes.news'))->ucfirst()->plural()) +@section('description', __('Liste de tous les :model', ['model' => str(__('models.classes.news'))->plural()])) + +@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/resources/views/bo/pages/news/show.blade.php b/resources/views/bo/pages/news/show.blade.php new file mode 100644 index 00000000..caba3afe --- /dev/null +++ b/resources/views/bo/pages/news/show.blade.php @@ -0,0 +1,131 @@ +@extends('bo.layout', ['brParam' => $newsModel]) + +@section('title', __('Aperçu d\'un :model', ['model' => __('models.classes.news')])) +@section('description', __('Aperçu d\'un :model déjà existante', ['model' => __('models.classes.news')])) +@section('breadcrumb', request()->route()->getName()) + +@section('content') +
+
+ + + + @include('modules.breadcrumbs.breadcrumb-body', ['brParam' => $newsModel]) +
+ @canAny(['duplicate', 'update', 'delete'], $newsModel) +
+ @can('duplicate', $newsModel) + + + + @endcan + @can('update', $newsModel) + + + + @endcan + @can('delete', $newsModel) + @csrf + @method('DELETE') + + @endcan +
+ @endcan +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ str(__('validation.attributes.title'))->ucFirst() }} + {{ $newsModel->title }} +
{{ str(__('validation.custom.seo_title'))->ucFirst() }} + {{ $newsModel->seo_title }} +
{{ str(__('validation.custom.seo_description'))->ucFirst() }} + {{ $newsModel->seo_description }} +
{{ str(__('validation.attributes.image'))->ucFirst() }} + {{ $newsModel->picture_alt }} +
{{ str(__('validation.custom.thumbnail'))->ucFirst() }} + {{ $newsModel->thumbnail_alt }} +
{{ str(__('validation.attributes.content'))->ucFirst() }} + {{ $newsModel->content }} +
+ {{ str(__('validation.custom.publication'))->ucFirst() }} + + {{ $newsModel->published + ? str($newsModel->created_at->isoFormat('LLLL'))->ucFirst() + : __('crud.other.model_not_published') }} +
{{ str(__('validation.custom.publish_at'))->ucFirst() }} + {{ $newsModel->publish_at + ? str($newsModel->publish_at->isoFormat('LLLL'))->ucFirst() + : __('crud.other.model_not_published') }} +
{{ str(__('validation.custom.unpublish_at'))->ucFirst() }} + {{ $newsModel->unpublish_at + ? str($newsModel->unpublish_at->isoFormat('LLLL'))->ucFirst() + : __('crud.other.model_not_published') }} +
{{ str(__('validation.attributes.created_at'))->ucFirst() }} + {{ str($newsModel->created_at->isoFormat('LLLL'))->ucFirst() }} +
{{ str(__('validation.attributes.updated_at'))->ucFirst() }} + {{ str($newsModel->updated_at->isoFormat('LLLL'))->ucFirst() }} +
+
+
+
+@endsection diff --git a/resources/views/bo/pages/static_pages/form.blade.php b/resources/views/bo/pages/static_pages/form.blade.php index fe3a3999..1db6ea07 100644 --- a/resources/views/bo/pages/static_pages/form.blade.php +++ b/resources/views/bo/pages/static_pages/form.blade.php @@ -77,8 +77,8 @@ ]) - - + + @endif @if (!$staticPageModel->type->equals(StaticPageType::recipe) && !$staticPageModel->type->equals(StaticPageType::product) && @@ -179,6 +179,7 @@ 'model' => trans_choice('models.classes.static_page', 1), ]), 'name' => 'content', + 'editorConfig' => \App\Enums\CkEditorConfigName::basic->label(), 'model' => $staticPageModel ?? null, 'required' => false, 'helper' => __('crud.minicrud.text_warning'), diff --git a/resources/views/bo/partials/inputs/text-html.blade.php b/resources/views/bo/partials/inputs/text-html.blade.php index 0f775653..828cc255 100644 --- a/resources/views/bo/partials/inputs/text-html.blade.php +++ b/resources/views/bo/partials/inputs/text-html.blade.php @@ -23,6 +23,7 @@ 'editorConfig' => $editorConfig ?? \App\Enums\CkEditorConfigName::basic->label(), 'placeholder' => $placeholder ?? __('Un text riche'), 'errors' => $errors->getBag('default')->getMessages() ?? [], + 'editorConfig' => $editorConfig ?? \App\Enums\CkEditorConfigName::basic->label(), ]; @endphp
diff --git a/resources/views/modules/script-routes.blade.php b/resources/views/modules/script-routes.blade.php index 7bdd355c..cc508ce6 100644 --- a/resources/views/modules/script-routes.blade.php +++ b/resources/views/modules/script-routes.blade.php @@ -8,6 +8,7 @@ // FIXME: Add missing routes for MenuNodeTypeEnum usage in settings vue component // FIXME: Add missing routes for RangesSliderComponent.vue // BO + window.__SYSTEM._routes['bo.upload'] = '{{ route('bo.upload') }}'; window.__SYSTEM._routes['bo.menu_node_data.index'] = '{{ route('bo.menu_node_data.index') }}'; window.__SYSTEM._routes['bo.menu_node_data.update'] = '{{ route('bo.menu_node_data.update', ['menu_node_data' => 'MENU_NODE_DATA']) }}'; window.__SYSTEM._routes['bo.approaches.json-paginate'] = '{{ route('bo.approaches.json-paginate') }}'; diff --git a/routes/breadcrumbs-bo.php b/routes/breadcrumbs-bo.php index 39686035..ae8a056c 100644 --- a/routes/breadcrumbs-bo.php +++ b/routes/breadcrumbs-bo.php @@ -13,6 +13,7 @@ use App\Models\MainSlide; use App\Models\GeneralSetting; use App\Models\HistoricalMilestone; use App\Models\Meat; +use App\Models\News; use App\Models\NewsCategory; use App\Models\NewsCorporate; use App\Models\Offer; @@ -582,6 +583,30 @@ Breadcrumbs::for('bo.historical_milestones.duplicate', function (Generator $trai $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') + ); +}); +Breadcrumbs::for('bo.news.show', function (Generator $trail, News $news) { + $trail->parent('bo.news.index'); + $trail->push(Str::ucfirst(trans('crud.actions.view')), route('bo.news.show', $news)); +}); +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'))); +}); + // * NEWS CORPORATE Breadcrumbs::for('bo.news_corporates.index', function (Generator $trail) { $trail->push( diff --git a/routes/web-bo.php b/routes/web-bo.php index 6941373d..1b1db4e4 100644 --- a/routes/web-bo.php +++ b/routes/web-bo.php @@ -14,6 +14,7 @@ use App\Http\Controllers\Bo\MainSlideController; use App\Http\Controllers\Bo\MenuNodeDataController; use App\Http\Controllers\Bo\MeatController; use App\Http\Controllers\Bo\NewsCategoryController; +use App\Http\Controllers\Bo\NewsController; use App\Http\Controllers\Bo\NewsCorporateController; use App\Http\Controllers\Bo\OfferController; use App\Http\Controllers\Bo\ProductController; @@ -25,6 +26,7 @@ use App\Http\Controllers\Bo\StaticPageController; use App\Http\Controllers\Bo\SubmeatController; use App\Http\Controllers\Bo\SubrangeController; use App\Http\Controllers\Bo\TestimonialController; +use App\Http\Controllers\Bo\UploadController; use App\Http\Controllers\Bo\UserController; use App\Http\Controllers\Bo\VideoController; use App\Http\Controllers\Controller; @@ -431,6 +433,17 @@ Route::middleware(['throttle:web', 'admin-secure'])->prefix('bo')->name('bo.')-> 'duplicate' ])->name('news_corporates.duplicate'); + // * NEWS. + Route::resource('news', NewsController::class) + ->withoutMiddleware('htmlsanitize') + ->middleware('htmlsanitize:content'); + Route::patch('/news/{news}/change-published', [ + NewsController::class, 'changePublished' + ])->name('news.change-published'); + Route::get('/news/{news}/duplicate', [ + NewsController::class, 'duplicate' + ])->name('news.duplicate'); + // * NEWS CATEGORIES. Route::resource('news_categories', NewsCategoryController::class); Route::patch('/news_categories/{news_category}/change-published', [ @@ -452,4 +465,7 @@ Route::middleware(['throttle:web', 'admin-secure'])->prefix('bo')->name('bo.')-> }); // * Others routes Route::post('theme/set', [Controller::class, 'setTheme'])->name('theme.set'); + + // * UPLOAD (USED FOR CKEDITOR) + Route::post('/upload', [UploadController::class, 'uploadFile'])->name('upload'); });