From 00d9db93aa7620220647b9df153ef376b8cc18c1 Mon Sep 17 00:00:00 2001 From: Abdel LAMRANI Date: Thu, 4 Jun 2026 14:20:18 +0200 Subject: [PATCH] feat :sparkles: final commit for respect earth page --- app/Enums/Block/BlockContentTypeEnum.php | 30 ++ app/Enums/Block/BlockPositionEnum.php | 39 +++ app/Enums/BlockTypeEnum.php | 85 +++++ app/Enums/InputType.php | 1 + app/Enums/StaticPageType.php | 17 +- app/Http/Controllers/Bo/BlockController.php | 31 ++ app/Http/Controllers/Bo/PictoController.php | 199 ++++++++++++ .../Controllers/Bo/SectionPageController.php | 79 +++++ .../Controllers/Bo/StaticPageController.php | 73 ++++- .../Controllers/Fo/CommitmentController.php | 22 ++ .../Bo/BlockPage/UpdateBlockRequest.php | 191 ++++++++++++ .../Requests/Bo/Pictos/StorePictoRequest.php | 90 ++++++ .../SectionPages/UpdateSectionPageRequest.php | 76 +++++ .../StaticPages/UpdateStaticPageRequest.php | 6 +- .../Requests/Bo/UpdateMiniCrudRequest.php | 32 +- app/Models/Block.php | 55 ++++ app/Models/Picto.php | 62 ++++ app/Models/SectionPage.php | 39 +++ app/Models/StaticPage.php | 5 + app/Policies/BlockPolicy.php | 70 +++++ app/Policies/PictoPolicy.php | 98 ++++++ app/Policies/SectionPagePolicy.php | 53 ++++ app/Providers/ViewServiceProvider.php | 1 + app/Services/Picto/PictoService.php | 49 +++ app/Services/StaticPage/BlockService.php | 95 ++++++ .../StaticPage/SectionPageService.php | 144 +++++++++ ...934_add_data_to_respcter_la_terre_page.php | 83 +++++ ...5_21_125623_create_section_pages_table.php | 26 ++ .../2026_05_26_131337_create_blocks_table.php | 31 ++ .../2026_05_26_152751_add_data_for_blocks.php | 290 ++++++++++++++++++ .../2026_05_28_164713_create_pictos_table.php | 37 +++ .../2026_06_03_102408_add_data_for_pictos.php | 162 ++++++++++ .../commitments/commitment-5/picto-life.png | Bin 0 -> 3150 bytes .../pictograms/png/picto-human-footprint.png | Bin 0 -> 12427 bytes resources/lang/fr/crud.php | 1 + resources/lang/fr/models.php | 3 +- resources/lang/fr/validation.php | 1 + resources/ts/bo/base/common.ts | 72 +++-- resources/ts/bo/bo.ts | 5 + resources/ts/bo/utilities/media-selector.ts | 41 ++- resources/ts/bo/utilities/picto-form.ts | 241 +++++++++++++++ .../ts/bo/utilities/picto-picture-size.ts | 113 +++++++ resources/ts/bo/utilities/submit-form.ts | 184 +++++++++++ resources/ts/bo/utilities/toogle-block.ts | 27 ++ .../ts/bo/utilities/toogle-visibility.ts | 88 ++++++ resources/ts/bo/utilities/vue-bo.ts | 53 ++-- resources/views/bo/layouts/sidemenu.blade.php | 224 +++++++++----- .../bo/modules/change-model-order.blade.php | 36 ++- .../bo/modules/minicrud-form-inputs.blade.php | 8 +- .../bo/modules/table-col-sorter.blade.php | 6 + .../views/bo/pages/pictos/create.blade.php | 24 ++ .../views/bo/pages/pictos/edit.blade.php | 25 ++ .../views/bo/pages/pictos/form.blade.php | 176 +++++++++++ .../bo/pages/pictos/index-list.blade.php | 76 +++++ .../views/bo/pages/pictos/index.blade.php | 28 ++ .../common_section/commitments/edit.blade.php | 35 +++ .../bo/pages/static_pages/edit.blade.php | 31 ++ .../bo/pages/static_pages/form.blade.php | 3 +- .../sections/form_section.blade.php | 139 +++++++++ .../blocks/redirection-block-form.blade.php | 140 +++++++++ .../partials/inputs/media-selector.blade.php | 140 +++++---- .../search-belongs-to-dropdown.blade.php | 13 +- .../views/fo/layouts/header-banner.blade.php | 8 +- .../fo/pages/commitments/respect.blade.php | 270 +++++----------- .../partials/block/reduction-card.blade.php | 48 +++ .../commitments/complex-flex-items.blade.php | 8 +- ...h-with-visual-complex-flex-items.blade.php | 37 ++- .../paragraph-with-visual.blade.php | 50 ++- .../views/modules/script-routes.blade.php | 1 + routes/breadcrumbs-bo.php | 6 + routes/web-bo.php | 42 +++ routes/web-fo.php | 3 +- 72 files changed, 4210 insertions(+), 467 deletions(-) create mode 100644 app/Enums/Block/BlockContentTypeEnum.php create mode 100644 app/Enums/Block/BlockPositionEnum.php create mode 100644 app/Enums/BlockTypeEnum.php create mode 100644 app/Http/Controllers/Bo/BlockController.php create mode 100644 app/Http/Controllers/Bo/PictoController.php create mode 100644 app/Http/Controllers/Bo/SectionPageController.php create mode 100644 app/Http/Controllers/Fo/CommitmentController.php create mode 100644 app/Http/Requests/Bo/BlockPage/UpdateBlockRequest.php create mode 100644 app/Http/Requests/Bo/Pictos/StorePictoRequest.php create mode 100644 app/Http/Requests/Bo/SectionPages/UpdateSectionPageRequest.php create mode 100644 app/Models/Block.php create mode 100644 app/Models/Picto.php create mode 100644 app/Models/SectionPage.php create mode 100644 app/Policies/BlockPolicy.php create mode 100644 app/Policies/PictoPolicy.php create mode 100644 app/Policies/SectionPagePolicy.php create mode 100644 app/Services/Picto/PictoService.php create mode 100644 app/Services/StaticPage/BlockService.php create mode 100644 app/Services/StaticPage/SectionPageService.php create mode 100644 database/migrations/2026_05_21_123934_add_data_to_respcter_la_terre_page.php create mode 100644 database/migrations/2026_05_21_125623_create_section_pages_table.php create mode 100644 database/migrations/2026_05_26_131337_create_blocks_table.php create mode 100644 database/migrations/2026_05_26_152751_add_data_for_blocks.php create mode 100644 database/migrations/2026_05_28_164713_create_pictos_table.php create mode 100644 database/migrations/2026_06_03_102408_add_data_for_pictos.php create mode 100644 resources/assets/images/commitments/commitment-5/picto-life.png create mode 100644 resources/assets/images/commitments/pictograms/png/picto-human-footprint.png create mode 100644 resources/ts/bo/utilities/picto-form.ts create mode 100644 resources/ts/bo/utilities/picto-picture-size.ts create mode 100644 resources/ts/bo/utilities/submit-form.ts create mode 100644 resources/ts/bo/utilities/toogle-block.ts create mode 100644 resources/ts/bo/utilities/toogle-visibility.ts create mode 100644 resources/views/bo/pages/pictos/create.blade.php create mode 100644 resources/views/bo/pages/pictos/edit.blade.php create mode 100644 resources/views/bo/pages/pictos/form.blade.php create mode 100644 resources/views/bo/pages/pictos/index-list.blade.php create mode 100644 resources/views/bo/pages/pictos/index.blade.php create mode 100644 resources/views/bo/pages/static_pages/common_section/commitments/edit.blade.php create mode 100644 resources/views/bo/pages/static_pages/sections/form_section.blade.php create mode 100644 resources/views/bo/partials/blocks/redirection-block-form.blade.php create mode 100644 resources/views/fo/partials/block/reduction-card.blade.php diff --git a/app/Enums/Block/BlockContentTypeEnum.php b/app/Enums/Block/BlockContentTypeEnum.php new file mode 100644 index 00000000..d4caf2a9 --- /dev/null +++ b/app/Enums/Block/BlockContentTypeEnum.php @@ -0,0 +1,30 @@ + 'Bouton', + 'Actu' => 'Encart actu', + 'Both' => 'Les deux', + ]; + + public static function options(): array + { + return self::all(); + } + + public static function validationRule(): string + { + return 'required|in:' . implode(',', array_column(self::cases(), 'value')); + } +} diff --git a/app/Enums/Block/BlockPositionEnum.php b/app/Enums/Block/BlockPositionEnum.php new file mode 100644 index 00000000..837cf016 --- /dev/null +++ b/app/Enums/Block/BlockPositionEnum.php @@ -0,0 +1,39 @@ + 'Gauche', + 'Centre' => 'Centre', + 'Droite' => 'Droite', + ]; + + public static function options(): array + { + return self::all(); + } + + public static function validationRule(): string + { + return 'required|in:' . implode(',', array_column(self::cases(), 'value')); + } + + public function toBootstrap(): string + { + return match ($this) { + self::Gauche => 'start', + self::Centre => 'center', + self::Droite => 'end', + }; + } +} diff --git a/app/Enums/BlockTypeEnum.php b/app/Enums/BlockTypeEnum.php new file mode 100644 index 00000000..b6ff5d35 --- /dev/null +++ b/app/Enums/BlockTypeEnum.php @@ -0,0 +1,85 @@ +name => 'Redirection', + self::picto->name => 'Pictogramme', + ]; + + public function defaultConfig(): array + { + return match ($this) { + self::redirection => [ + 'type' => self::redirection, + 'data' => [ + 'title' => [ + 'label' => 'Titre', + 'inputType' => InputType::string->value, + 'value' => 'Redirections actualités', + 'validate' => 'nullable|string|between:3,255', + 'required' => false, + ], + 'position' => [ + 'label' => 'Position', + 'inputType' => InputType::selectEnum->value, + 'options' => BlockPositionEnum::options(), + 'value' => 'gauche', + 'validate' => BlockPositionEnum::validationRule(), + 'required' => true, + ], + 'block_type' => [ + 'label' => 'Type de contenu', + 'inputType' => InputType::selectEnum->value, + 'options' => BlockContentTypeEnum::options(), + 'value' => 'cta', + 'validate' => BlockContentTypeEnum::validationRule(), + 'required' => true, + ], + 'cta_url' => [ + 'label' => 'URL du bouton', + 'inputType' => InputType::url->value, + 'value' => null, + 'validate' => 'required|url', + 'required' => true, + 'showWhen' => ['block_type' => [BlockContentTypeEnum::Cta->value, BlockContentTypeEnum::Both->value]], + ], + 'cta_label' => [ + 'label' => 'Nom du CTA', + 'inputType' => InputType::string->value, + 'value' => null, + 'validate' => 'required|string|between:3,100', + 'required' => true, + 'showWhen' => ['block_type' => [BlockContentTypeEnum::Cta->value, BlockContentTypeEnum::Both->value]], + ], + 'article_id' => [ + 'label' => 'Article associé', + 'inputType' => InputType::searchBelongsTo->value, + 'source' => \App\Models\News::class, + 'required' => true, + 'value' => null, + 'validate' => ['integer', 'exists:news,id'], + 'showWhen' => ['block_type' => [BlockContentTypeEnum::Actu->value, BlockContentTypeEnum::Both->value]], + ], + ], + ], + }; + } +} diff --git a/app/Enums/InputType.php b/app/Enums/InputType.php index 49edf721..b540866d 100644 --- a/app/Enums/InputType.php +++ b/app/Enums/InputType.php @@ -26,4 +26,5 @@ enum InputType: int case video = 16; case slider = 17; case selectImageVideo = 18; + case searchBelongsTo = 19; } diff --git a/app/Enums/StaticPageType.php b/app/Enums/StaticPageType.php index 7964fb7e..50ecabeb 100644 --- a/app/Enums/StaticPageType.php +++ b/app/Enums/StaticPageType.php @@ -34,6 +34,7 @@ enum StaticPageType: int case ouiCBon = 24; case ourFarms = 25; case tdf = 26; + case respectTheEarth = 27; /** * Optionnal labels definition. @@ -66,7 +67,8 @@ enum StaticPageType: int self::exitOutside->name => 'Sortir à l\'extérieur', self::ouiCBon->name => 'Oui c\'est bon', self::ourFarms->name => 'Nos élevages', - self::tdf->name => 'Tour de France' + self::tdf->name => 'Tour de France', + self::respectTheEarth->name => 'Respecter la terre', ]; /** @@ -96,4 +98,17 @@ enum StaticPageType: int ->map(fn($page) => $page->value) ->toArray(); } + + /** + * Get all commitment pages type values. + * + * @return array + */ + public static function getCommitmentsPages(): array + { + return [ + self::respectTheEarth, + // self::forLivingTogether + ]; + } } diff --git a/app/Http/Controllers/Bo/BlockController.php b/app/Http/Controllers/Bo/BlockController.php new file mode 100644 index 00000000..f6aead1d --- /dev/null +++ b/app/Http/Controllers/Bo/BlockController.php @@ -0,0 +1,31 @@ +service->update($block, $request->validated()); + + return response()->json([ + 'success' => true, + 'message' => __('Bloc mis à jour avec succès.'), + ]); + } + +} diff --git a/app/Http/Controllers/Bo/PictoController.php b/app/Http/Controllers/Bo/PictoController.php new file mode 100644 index 00000000..c2ad0bdb --- /dev/null +++ b/app/Http/Controllers/Bo/PictoController.php @@ -0,0 +1,199 @@ +authorizeResource(Picto::class); + } + + public function index(Request $request) +{ + if ($request->ajax() && $request->has('section_id')) { + $allowedSorts = ['title', 'description', 'updated_at']; + + $query = Picto::where('section_id', $request->section_id); + + $sortCol = in_array($request->sort_col, $allowedSorts) ? $request->sort_col : 'order'; + $sortWay = in_array($request->sort_way, ['asc', 'desc']) ? $request->sort_way : 'asc'; + + $query->orderBy($sortCol, $sortWay); + + $pictos = $query->get(); + return view('bo.pages.pictos.index-list', compact('pictos')); + } + abort(404); +} + + + public function create(Picto $picto) + { + return view('bo.pages.pictos.create', [ + 'pictoModel' => $picto, + ]); + } + + /** + * Store Picto. + * + * @param StorePictoRequest $request Request + * @param PictoService $pictoService Service for Picto. + * @return RedirectResponse + * @throws DdException + */ + public function store(StorePictoRequest $request, PictoService $pictoService) + { + $result = $pictoService->store($request->validated()); + if (is_string($result)) { + return response()->json([ + 'success' => false, + 'message' => $result, + ], 422); + } + + return response()->json([ + 'success' => true, + 'message' => __( + 'crud.messages.has_been_updated', + ['model' => Str::ucfirst(trans_choice('models.classes.picto', 1))] + ), + 'data' => $result, + ], 201); + } + + /** + * Show the form for editing the specified picto (AJAX returns a partial view). + * + * @param \App\Models\Picto $picto + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Contracts\View\View + */ + public function edit(Picto $picto, Request $request) + { + if ($request->ajax()) { + return view('bo.pages.pictos.edit', [ + 'pictoModel' => $picto, + 'section' => $picto->section, + ]); + } + return view('bo.pages.pictos.edit', ['pictoModel' => $picto, 'section' => $picto->section]); + } + + /** + * Update the specified picto in storage. + * + * @param \App\Http\Requests\Bo\Pictos\StorePictoRequest $request + * @param \App\Models\Picto $picto + * @return \Illuminate\Http\JsonResponse + */ + public function update(StorePictoRequest $request, Picto $picto) + { + $result = $this->pictoService->update($picto, $request->validated()); + + if (is_string($result)) { + return response()->json([ + 'success' => false, + 'message' => $result, + ], 422); + } + + return response()->json([ + 'success' => true, + 'message' => __( + 'crud.messages.has_been_updated', + ['model' => Str::ucfirst(trans_choice('models.classes.picto', 1))] + ), + 'data' => $result, + ]); + } + + + /** + * Save the picture width and height for a picto. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function savePictureSize(Request $request): JsonResponse + { + $request->validate([ + 'picture_width' => 'required|integer|min:1', + 'picture_height' => 'required|integer|min:1', + ]); + + if ($request->has('picto_id') && $request->picto_id) { + $picto = Picto::findOrFail($request->picto_id); + $picto->update([ + 'picture_width' => $request->picture_width, + 'picture_height' => $request->picture_height, + ]); + } + + return response()->json(['success' => true]); + } + + + /** + * Remove the specified picto and return the refreshed picto list. + * + * @param \App\Models\Picto $picto + * @return \Illuminate\Http\JsonResponse + */ + public function destroy(Picto $picto) + { + $picto->delete(); + + $pictos = Picto::with(['section.page'])->get(); + + return response()->json([ + 'success' => true, + 'message' => __('crud.messages.has_been_deleted', ['model' => Str::ucfirst(trans_choice('models.classes.picto', 1))]), + 'pictos' => $pictos, + ]); + } + + public function changeOrder(UpdateChangeOrderRequest $request, Picto $picto): \Illuminate\Http\JsonResponse|\Illuminate\Http\RedirectResponse + { + $tmp = Picto::query() + ->where('section_id', $picto->section_id) + ->where('order', $request->direction ? '<' : '>', $picto->order) + ->orderBy('order', $request->direction ? 'DESC' : 'ASC') + ->first(); + + if (!$tmp) { + return $request->ajax() + ? response()->json(['message' => trans('crud.messages.order_not_changed')], 422) + : back()->with('error', trans('crud.messages.order_not_changed')); + } + + DB::transaction(function () use ($picto, $tmp) { + $newOrder = $tmp->order; + $tmp->update(['order' => $picto->order]); + $picto->update(['order' => $newOrder]); + }); + + $msg = trans('crud.messages.order_changed'); + return $request->ajax() + ? response()->json(['message' => $msg]) + : back()->with('success', $msg); + } +} diff --git a/app/Http/Controllers/Bo/SectionPageController.php b/app/Http/Controllers/Bo/SectionPageController.php new file mode 100644 index 00000000..5b300712 --- /dev/null +++ b/app/Http/Controllers/Bo/SectionPageController.php @@ -0,0 +1,79 @@ +json([ + 'data' => $this->service->getByPage($page), + ]); + } + + public function store(UpdateSectionPageRequest $request, StaticPage $page): JsonResponse + { + $section = $this->service->create($page, $request->validated()); + + return response()->json($section); + } + + public function show(SectionPage $section): JsonResponse + { + return response()->json([ + 'data' => $section, + ]); + } + + public function update(UpdateSectionPageRequest $request, SectionPage $sectionPage): JsonResponse + { + $validated = $request->validated(); + + // Merge files in data + if ($request->hasFile('data')) { + $validated['data'] = array_replace_recursive( + $validated['data'] ?? [], + $request->file('data') + ); + } + + $this->service->update($sectionPage, $validated); + + session()->flash('success', 'Section mise à jour avec succès.'); + + return response()->json([ + 'message' => 'Section mise à jour', + 'data' => $sectionPage, + ]); + } + + public function destroy(SectionPage $section): JsonResponse + { + $this->service->delete($section); + + return response()->json([ + 'message' => 'Section supprimée', + 'data' => $section, + ]); + } + + public function toggleVisibility(SectionPage $section): JsonResponse + { + $section = $this->service->toggleVisibility($section); + + return response()->json([ + 'data' => $section, + ]); + } +} diff --git a/app/Http/Controllers/Bo/StaticPageController.php b/app/Http/Controllers/Bo/StaticPageController.php index e18c89ca..9d529270 100644 --- a/app/Http/Controllers/Bo/StaticPageController.php +++ b/app/Http/Controllers/Bo/StaticPageController.php @@ -30,9 +30,22 @@ class StaticPageController extends Controller */ public function edit(StaticPage $static_page): \Illuminate\Contracts\View\View { - return view('bo.pages.static_pages.edit', ['staticPageModel' => $static_page]); + $isCommitmentPage = in_array( + $static_page->type, + StaticPageType::getCommitmentsPages() + ); + + if ($isCommitmentPage) { + $static_page->load(['sections.blocks', 'sections.pictos']); + } + + return view('bo.pages.static_pages.edit', [ + 'staticPageModel' => $static_page, + 'isCommitmentPage' => $isCommitmentPage + ]); } + /** * Update the specified resource in storage. * @@ -71,6 +84,7 @@ class StaticPageController extends Controller }); } + /** * Display the common section edit form for all breeding static pages. * @@ -92,7 +106,7 @@ class StaticPageController extends Controller /** * Update the common section for all breeding static pages. * - * @param \App\Http\Requests\Bo\StaticPage\UpdateCommonSectionRequest $request + * @param \App\Http\Requests\Bo\StaticPages\UpdateCommonSectionRequest $request * @return \Illuminate\Http\RedirectResponse */ public function updateCommonSection(UpdateCommonSectionRequest $request): RedirectResponse @@ -124,6 +138,59 @@ class StaticPageController extends Controller return redirect() ->route('bo.static_page.sections.edit') ->with('success', __('crud.messages.has_been_updated', [ - 'model' => Str::ucfirst(__('validation.custom.common_section_part'))])); + 'model' => Str::ucfirst(__('validation.custom.common_section_part')) + ])); + } + + public function updateCommitmentsCommonSection(UpdateCommonSectionRequest $request): RedirectResponse + { + $pages = StaticPage::whereIn('type', StaticPageType::getCommitmentsPages())->get(); + + try { + DB::transaction(function () use ($pages, $request) { + foreach ($pages as $page) { + $data = $page->data ?? []; + + $data['common_section'] = [ + 'title' => $request->validated('title'), + 'content' => $request->validated('content'), + ]; + + $page->data = $data; + $page->saveOrFail(); + } + }); + } catch (Throwable $e) { + return redirect() + ->back() + ->withInput() + ->with('error', __('crud.messages.cannot_be_updated', [ + 'model' => Str::ucfirst(__('validation.custom.common_section_part')) + ])); + } //end try + + return redirect() + ->route('bo.static_page.commitmments.sections.edit') + ->with('success', __('crud.messages.has_been_updated', [ + 'model' => Str::ucfirst(__('validation.custom.common_section_part')) + ])); + } + + /** + * Display the common section edit form for all breeding static pages. + * + * @return \Illuminate\Contracts\View\View + */ + public function getCommitmentsCommonSection(): \Illuminate\Contracts\View\View + { + $commonSection = StaticPage::whereIn('type', StaticPageType::getCommitmentsPages()) + ->get() + ->map(fn($page) => $page->data['common_section'] ?? null) + ->filter() + ->first(); + + return view('bo.pages.static_pages.common_section.commitments.edit', [ + 'commonSection' => $commonSection, + ]); } } diff --git a/app/Http/Controllers/Fo/CommitmentController.php b/app/Http/Controllers/Fo/CommitmentController.php new file mode 100644 index 00000000..d0177360 --- /dev/null +++ b/app/Http/Controllers/Fo/CommitmentController.php @@ -0,0 +1,22 @@ +load(['sections.blocks', 'sections.pictos']); + return view('fo.pages.commitments.respect', ['staticPageModel' => $staticPageModel]); + } +} diff --git a/app/Http/Requests/Bo/BlockPage/UpdateBlockRequest.php b/app/Http/Requests/Bo/BlockPage/UpdateBlockRequest.php new file mode 100644 index 00000000..e36fb765 --- /dev/null +++ b/app/Http/Requests/Bo/BlockPage/UpdateBlockRequest.php @@ -0,0 +1,191 @@ +route($this->modelName)); + } + + /** + * Prepare the data for validation. + * + * @return void + */ + protected function prepareForValidation(): void + { + $data = $this->input('data', []); + $block = $this->route('block'); + + if ($block->type === BlockTypeEnum::redirection) { + $fields = json_decode(json_encode($block->data), true); + $data = $this->processBlockData($data, $fields); + } + + $this->merge(['data' => $data]); + } + + + /** + * Get the validation rules that apply to the request. + * + * @return array|string> + */ + public function rules(): array + { + $parentRules = parent::rules(); + $block = $this->route('block'); + + $rules = [ + 'data' => 'present|array', + 'data.*' => 'required|array', + 'data.block_type.value' => ['required', Rule::in(BlockContentTypeEnum::toValues())], + ]; + + if ($block->type === BlockTypeEnum::redirection) { + $rules = array_merge($rules, $this->resolveRedirectionRules()); + } + + return array_merge($parentRules, $rules); + } + + public function attributes(): array + { + return [ + 'data.cta_url.value' => 'redirection', + 'data.cta_label.value' => 'du bouton', + 'data.block_type.value' => 'type de bloc', + 'data.article_id.value' => 'article lié', + 'data.title.value' => 'titre', + 'data.position.value' => 'position', + ]; + } + + + + protected function failedValidation(Validator $validator) + { + $errors = collect($validator->errors()->all())->join("\n"); + + session()->flash('error', 'Liste des erreurs :' . "\n" . $errors); + + throw new HttpResponseException( + response()->json([ + 'message' => 'Erreur de validation.', + 'errors' => $validator->errors(), + ], 422) + ); + } + + /** + * Normalize input data and apply showWhen visibility logic for redirection blocks. + * + * @param array $data The raw input data from the request. + * @param array $fields The block field configuration (including showWhen rules). + * + * @return array|string> + */ + private function processBlockData(array $data, array $fields): array + { + foreach ($data as $key => $item) { + if (!isset($item['value'])) { + $data[$key] = ['value' => $item]; + } + } + + foreach ($fields as $fieldName => $fieldConfig) { + if (!isset($fieldConfig['showWhen'])) { + continue; + } + + $shouldShow = true; + foreach ($fieldConfig['showWhen'] as $dependentField => $allowedValues) { + if (!in_array($data[$dependentField]['value'] ?? null, (array) $allowedValues)) { + $shouldShow = false; + break; + } + } + + if (!$shouldShow) { + $data[$fieldName] = ['validate' => 'nullable', 'required' => false, 'value' => null]; + } else { + $data[$fieldName]['required'] = true; + unset($data[$fieldName]['validate']); + } + } + + return $data; + } + + /** + * Resolve validation rules for redirection block fields dynamically. + * + * builds a rules array based on: + * - The block's field config (validate key) as fallback. + * - The field's required flag set during prepareForValidation to either + * enforce "required" or replace it with "nullable". + * + * @return array|string> + */ + private function resolveRedirectionRules(): array + { + $data = $this->input('data', []); + $parentRules = parent::rules(); + $block = $this->route('block'); + $config = json_decode(json_encode($block->data), true); + + $rules = []; + + foreach ($data as $fieldName => $item) { + if ($fieldName === 'block_type' || $fieldName === 'position') { + continue; + } + $isRequired = $item['required'] ?? true; + + $baseRules = $parentRules["data.{$fieldName}.value"] + ?? $config[$fieldName]['validate'] + ?? []; + + if (is_string($baseRules)) { + $baseRules = explode('|', $baseRules); + } + + if (empty($baseRules)) { + continue; + } + + if (!$isRequired) { + $baseRules = array_filter($baseRules, fn($r) => $r !== 'required'); + $baseRules[] = 'nullable'; + $baseRules = array_values($baseRules); + } else { + if (!in_array('required', $baseRules)) { + array_unshift($baseRules, 'required'); + } + } + + $rules["data.{$fieldName}.value"] = $baseRules; + } + + return $rules; + } +} diff --git a/app/Http/Requests/Bo/Pictos/StorePictoRequest.php b/app/Http/Requests/Bo/Pictos/StorePictoRequest.php new file mode 100644 index 00000000..ce2fd3dd --- /dev/null +++ b/app/Http/Requests/Bo/Pictos/StorePictoRequest.php @@ -0,0 +1,90 @@ +mergePicture('picture'); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array|string> + */ + public function rules(): array + { + $width = (int) ($this->input('picture_width') ?: Picto::MIN_WIDTH); + $height = (int) ($this->input('picture_height') ?: Picto::MIN_HEIGHT); + + + $rules = [ + 'title' => 'required|string|between:3,255', + 'description' => ['required', 'string', new WysiwygLength(5)], + 'picture_alt' => 'required_with:picture|nullable|between:3,255', + 'picture_title' => 'required_with:picture|nullable|between:3,255', + 'section_id' => 'required|exists:section_pages,id', + ]; + + return array_merge( + $rules, + $this->pictureRules('picture', $width, $height, $width, $height) + ); + } + + /** + * Get custom attributes for validator errors. + * + * @return array + */ + public function attributes() + { + return [ + 'title' => trans('validation.attributes.title'), + 'description' => trans('validation.attributes.description'), + 'picture' => trans('validation.attributes.image'), + 'picture_alt' => trans('validation.custom.attribute_alt'), + 'picture_title' => trans('validation.custom.attribute_title'), + ]; + } + + protected function failedValidation(Validator $validator) + { + $errors = collect($validator->errors()->all())->join("\n"); + + session()->flash('error', 'Liste des erreurs :' . "\n" . $errors); + + throw new HttpResponseException( + response()->json([ + 'message' => 'Erreur de validation.', + 'errors' => $validator->errors(), + ], 422) + ); + } +} diff --git a/app/Http/Requests/Bo/SectionPages/UpdateSectionPageRequest.php b/app/Http/Requests/Bo/SectionPages/UpdateSectionPageRequest.php new file mode 100644 index 00000000..bb3acc8b --- /dev/null +++ b/app/Http/Requests/Bo/SectionPages/UpdateSectionPageRequest.php @@ -0,0 +1,76 @@ +route($this->modelName)); + } + + /** + * Prepare the data for validation. + * + * @return void + */ + protected function prepareForValidation(): void + { + $data = $this->input('data', []); + foreach ($data as $key => $item) { + if (!isset($item['value'])) { + $data[$key] = [ + 'value' => $item + ]; + } + } + + $this->merge([ + 'data' => $data + ]); + } + + + /** + * Get the validation rules that apply to the request. + * + * @return array|string> + */ + public function rules(): array + { + return array_merge(parent::rules(), [ + 'visibility' => 'nullable|boolean', + 'data' => 'present|array', + 'data.*' => 'required|array', + ]); + } + + protected function failedValidation(Validator $validator) + { + $errors = collect($validator->errors()->all())->join("\n"); + + session()->flash('error', 'Liste des erreurs :' . "\n" . $errors); + + throw new HttpResponseException( + response()->json([ + 'message' => 'Erreur de validation.', + 'errors' => $validator->errors(), + ], 422) + ); + } +} diff --git a/app/Http/Requests/Bo/StaticPages/UpdateStaticPageRequest.php b/app/Http/Requests/Bo/StaticPages/UpdateStaticPageRequest.php index efb1fc42..5dd5b9c2 100644 --- a/app/Http/Requests/Bo/StaticPages/UpdateStaticPageRequest.php +++ b/app/Http/Requests/Bo/StaticPages/UpdateStaticPageRequest.php @@ -150,7 +150,8 @@ class UpdateStaticPageRequest extends UpdateMiniCrudRequest !$staticPageModel->type->equals(StaticPageType::accessibility) && !$staticPageModel->type->equals(StaticPageType::ourFarms) && !$staticPageModel->type->equals(StaticPageType::exitOutside) && - !$staticPageModel->type->equals(StaticPageType::ouiCBon) + !$staticPageModel->type->equals(StaticPageType::ouiCBon) && + !$staticPageModel->type->equals(StaticPageType::respectTheEarth) ) { $rules = \array_merge( $rules, @@ -168,7 +169,8 @@ class UpdateStaticPageRequest extends UpdateMiniCrudRequest !$staticPageModel->type->equals(StaticPageType::contact) && !$staticPageModel->type->equals(StaticPageType::search) && !$staticPageModel->type->equals(StaticPageType::exitOutside) && - !$staticPageModel->type->equals(StaticPageType::ouiCBon) + !$staticPageModel->type->equals(StaticPageType::ouiCBon) && + !$staticPageModel->type->equals(StaticPageType::respectTheEarth) ) { $rules = \array_merge( $rules, diff --git a/app/Http/Requests/Bo/UpdateMiniCrudRequest.php b/app/Http/Requests/Bo/UpdateMiniCrudRequest.php index 5412d347..926ea9e5 100644 --- a/app/Http/Requests/Bo/UpdateMiniCrudRequest.php +++ b/app/Http/Requests/Bo/UpdateMiniCrudRequest.php @@ -144,7 +144,8 @@ abstract class UpdateMiniCrudRequest extends FormRequest ]; $rules = array_merge($rules, [ - "data.$iptName.value" => [ + "data.$iptName.value" => array_merge( + [ function (string $attribute, $value, callable $fail) use ($sizes, $input) { // Picture can be null. if ($input['required'] || $value !== null) { @@ -167,6 +168,7 @@ abstract class UpdateMiniCrudRequest extends FormRequest ]), $input ), + ), ]); // Attributes are required if a picture is set. @@ -179,18 +181,21 @@ abstract class UpdateMiniCrudRequest extends FormRequest break; case InputType::url->value: $rules = array_merge($rules, [ - "data.$iptName.value" => [ - 'required', - 'url', - // @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed - function ($attribute, $value, $fail) { + "data.$iptName.value" => array_merge( + $this->compileValidate(['required', 'url'], $input), + [ + // @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed + function ($attribute, $value, $fail) { + if (empty($value)) { + return; + } try { ToolboxHelper::assertNoInternalBoUrls($attribute, $value); } catch (ValidationException $e) { $fail(collect($e->errors())->first()[0]); } - } - ] + }] + ) ]); break; case InputType::boolean->value: @@ -216,7 +221,10 @@ abstract class UpdateMiniCrudRequest extends FormRequest } break; case InputType::selectImageVideo->value: - if (Arr::exists($this->input("data.$iptName.value"), 'picture')) { + + $value = $this->input("data.$iptName.value", []); + + if (Arr::exists($value, 'picture')) { $rules = array_merge($rules, [ "data.$iptName.value.picture" => "required|array", "data.$iptName.value.picture.path" => "required", @@ -225,7 +233,7 @@ abstract class UpdateMiniCrudRequest extends FormRequest ]); } - if (Arr::exists($this->input("data.$iptName.value"), 'video')) { + if (Arr::exists($value, 'video')) { $rules = array_merge($rules, [ "data.$iptName.value.video" => "required|array", "data.$iptName.value.video.id" => "required|string", @@ -259,6 +267,10 @@ abstract class UpdateMiniCrudRequest extends FormRequest if (!\in_array($validate, $inputTypeValidate)) { $inputTypeValidate[] = $validate; } + // if required is hardcode + if (\in_array('nullable', $inputTypeValidate)) { + $inputTypeValidate = \array_filter($inputTypeValidate, fn($r) => $r !== 'required'); + } } return \collect($inputTypeValidate)->unique()->sort(function ($valueA, $valueB) { $valueA = $this->ruleHasToComeFirst($valueA) ? 0 : 2; diff --git a/app/Models/Block.php b/app/Models/Block.php new file mode 100644 index 00000000..257a9e8e --- /dev/null +++ b/app/Models/Block.php @@ -0,0 +1,55 @@ + + */ + protected $fillable = [ + 'section_id', + 'type', + 'data', + 'order', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'type' => BlockTypeEnum::class, + 'data' => 'object', + ]; + + /** + * Get the section that owns this block. + * + * @return BelongsTo + */ + public function section(): BelongsTo + { + return $this->belongsTo(SectionPage::class); + } +} diff --git a/app/Models/Picto.php b/app/Models/Picto.php new file mode 100644 index 00000000..cd6b7be2 --- /dev/null +++ b/app/Models/Picto.php @@ -0,0 +1,62 @@ +belongsTo(SectionPage::class, 'section_id'); + } + + protected static function booted(): void + { + static::creating(function (self $picto) { + $picto->picture = FileStorageHelper::storeFile($picto, $picto->picture, true); + + $maxOrder = self::where('section_id', $picto->section_id)->max('order'); + $picto->order = $maxOrder !== null ? $maxOrder + 1 : 1; + }); + + static::updating(function (self $picto) { + $picto->picture = FileStorageHelper::storeFile($picto, $picto->picture, true); + + if ($picto->isDirty('section_id')) { + $maxOrder = self::where('section_id', $picto->section_id)->max('order'); + $picto->order = $maxOrder !== null ? $maxOrder + 1 : 1; + } + }); + + static::deleting(function (self $picto) { + FileStorageHelper::storeFile($picto, $picto->picture, true); + }); + + static::deleted(function (self $picto) { + self::where('section_id', $picto->section_id) + ->where('order', '>', $picto->order) + ->decrement('order'); + }); + } +} diff --git a/app/Models/SectionPage.php b/app/Models/SectionPage.php new file mode 100644 index 00000000..d28101aa --- /dev/null +++ b/app/Models/SectionPage.php @@ -0,0 +1,39 @@ + 'boolean', + 'data' => 'object', + ]; + + public function page(): BelongsTo + { + return $this->belongsTo(StaticPage::class); + } + + public function blocks(): HasMany + { + return $this->hasMany(Block::class, 'section_id'); + } + + public function pictos(): HasMany + { + return $this->hasMany(Picto::class, 'section_id')->orderBy('order'); + } +} diff --git a/app/Models/StaticPage.php b/app/Models/StaticPage.php index 3df60d30..b75b4f89 100644 --- a/app/Models/StaticPage.php +++ b/app/Models/StaticPage.php @@ -301,4 +301,9 @@ class StaticPage extends Model implements SchemaOrgInterface, RedirectableInterf { return $this->hasMany(SectionApproach::class, 'page_id'); } + + public function sections(): HasMany + { + return $this->hasMany(SectionPage::class, 'page_id'); + } } diff --git a/app/Policies/BlockPolicy.php b/app/Policies/BlockPolicy.php new file mode 100644 index 00000000..a360640e --- /dev/null +++ b/app/Policies/BlockPolicy.php @@ -0,0 +1,70 @@ + 'ouiCestBonStaticPageModel', 'value' => StaticPage::getCachedStaticPage(StaticPageType::ouiCBon)], ['name' => 'ourFarmsStaticPageModel', 'value' => StaticPage::getCachedStaticPage(StaticPageType::ourFarms)], ['name' => 'tdfStaticPageModel', 'value' => StaticPage::getCachedStaticPage(StaticPageType::tdf)], + ['name' => 'respecttheEarthStaticPageModel', 'value' => StaticPage::getCachedStaticPage(StaticPageType::respectTheEarth)], ])->map(function ($array) { View::share($array['name'], (function () use ($array) { return $array['value'] ?: null; diff --git a/app/Services/Picto/PictoService.php b/app/Services/Picto/PictoService.php new file mode 100644 index 00000000..7f53b89a --- /dev/null +++ b/app/Services/Picto/PictoService.php @@ -0,0 +1,49 @@ +data), true) ?? []; + $maxPictos = $sectionData['max_pictos'] ?? 5; + + $count = Picto::where('section_id', $section->id)->count(); + + if ($count >= $maxPictos) { + return "Le nombre maximum de pictos ($maxPictos) est atteint pour cette section."; + } + + return Picto::create($data); + } + + + /** + * Update the given picto with the provided data. + * + * @param \App\Models\Picto $picto + * @param array $data + * @return \App\Models\Picto|string + */ + public function update(Picto $picto, array $data): Picto|string + { + $picto->update($data); + + return $picto; + } +} diff --git a/app/Services/StaticPage/BlockService.php b/app/Services/StaticPage/BlockService.php new file mode 100644 index 00000000..5e677363 --- /dev/null +++ b/app/Services/StaticPage/BlockService.php @@ -0,0 +1,95 @@ +data; + } + + /** + * Create and persist a new block with the given validated attributes. + * + * @param array $validated Validated input + */ + public function store(array $validated): Block + { + return Block::create([ + 'section_id' => $validated['section_id'], + 'type' => $validated['type'], + 'data' => $validated['data'] ?? [], + 'order' => $validated['order'] ?? 0, + ]); + } + + /** + * Update a block by merging existing data with new validated attributes. + * Applies type-specific merge rules before persisting. + * + * @param Block $block The block to update. + * @param array $validated Validated input containing type, data, order. + * @return Block + */ + public function update(Block $block, array $validated): Block + { + $existingData = json_decode(json_encode($block->data), true) ?? []; + $newData = $validated['data'] ?? []; + + $mergedData = $this->mergeBlockDataByType($block, $existingData, $newData); + + $block->update([ + 'type' => $validated['type'] ?? $block->type, + 'data' => $mergedData, + 'order' => $validated['order'] ?? $block->order, + ]); + + return $block->fresh(); + } + + /** + * Merge existing and new block data according to the block type rules. + * For redirection blocks, mutually exclusive fields (CTA vs article) are cleared accordingly. + * + * @param Block $block The block being updated. + * @param array $existingData Current block data. + * @param array $newData Incoming data to merge. + * @return array + */ + private function mergeBlockDataByType(Block $block, array $existingData, array $newData): array + { + $mergedData = array_replace_recursive($existingData, $newData); + + if ($block->type === BlockTypeEnum::redirection) { + $contentType = BlockContentTypeEnum::make($mergedData['block_type']['value']); + + if ( + $contentType === BlockContentTypeEnum::Cta + && (!empty($mergedData['cta_url']['value']) || !empty($mergedData['cta_label']['value'])) + ) { + $mergedData['article_id']['value'] = null; + } + + if ( + $contentType === BlockContentTypeEnum::Actu + && !empty($mergedData['article_id']['value']) + ) { + $mergedData['cta_url']['value'] = null; + $mergedData['cta_label']['value'] = null; + } + } + + return $mergedData; + } +} diff --git a/app/Services/StaticPage/SectionPageService.php b/app/Services/StaticPage/SectionPageService.php new file mode 100644 index 00000000..8a492c8a --- /dev/null +++ b/app/Services/StaticPage/SectionPageService.php @@ -0,0 +1,144 @@ +sections(); + + if ($onlyVisible) { + $query->where('visibility', true); + } + + return $query->get(); + } + + public function create(StaticPage $page, array $data): SectionPage + { + return $page->sections()->create([ + 'name' => $data['name'], + 'visibility' => $data['visibility'] ?? true, + 'data' => $data['data'] ?? [], + ]); + } + + public function update(SectionPage $section, array $data): SectionPage + { + if (isset($data['data'])) { + $data['data'] = $this->processFiles($section->page, $data['data']); + } + + $section->update([ + 'name' => $data['name'] ?? $section->name, + 'visibility' => $data['visibility'] ?? $section->visibility, + 'data' => $this->mergeData((array) json_decode(json_encode($section->data), true), $data['data'] ?? []), + ]); + + return $section->fresh(); + } + + /** + * Recursively traverses the $data array up to 4 levels deep + * and replaces every UploadedFile instance with the stored file path. + * + * Supported structures : + * - data[key][subKey] = UploadedFile (level 2) + * - data[key][subKey][nestedKey][deepKey] = UploadedFile (level 4) + * + * Values that are not UploadedFile instances (strings, integers, null...) + * are left untouched. + * + * @param StaticPage $page The page model used to determine the storage directory. + * @param array $data The form data array (from $request->input('data')). + * + * @return array The $data array with UploadedFile instances replaced by their stored paths. + * + * @uses FileStorageHelper::storeFile() + */ + private function processFiles(StaticPage $page, array $data): array + { + foreach ($data as $key => &$field) { + if (!is_array($field)) { + continue; + } + foreach ($field as $subKey => &$subValue) { + if ($subValue instanceof \Illuminate\Http\UploadedFile) { + $subValue = FileStorageHelper::storeFile($page, $subValue); + } elseif (is_array($subValue)) { + // case media.value.picture.path + foreach ($subValue as $nestedKey => &$nestedValue) { + if (is_array($nestedValue)) { + foreach ($nestedValue as $deepKey => &$deepValue) { + if ($deepValue instanceof \Illuminate\Http\UploadedFile) { + $deepValue = FileStorageHelper::storeFile($page, $deepValue); + } + } + unset($deepValue); + } + } + unset($nestedValue); + } + } + unset($subValue); + } + unset($field); + + return $data; + } + + + private function mergeData(array $original, array $new): array + { + foreach ($new as $key => $field) { + if (isset($original[$key]) && is_array($original[$key]) && is_array($field)) { + $original[$key] = $this->deepMerge($original[$key], $field); + } else { + $original[$key] = $field; + } + } + + return $original; + } + + private function deepMerge(array $original, array $new): array + { + if (isset($new['video'])) { + $original['picture'] = null; + } elseif (isset($new['picture'])) { + $original['video'] = null; + } + + foreach ($new as $key => $field) { + if (isset($original[$key]) && is_array($original[$key]) && is_array($field)) { + $original[$key] = $this->deepMerge($original[$key], $field); + } else { + $original[$key] = $field; + } + } + + return $original; + } + + + + public function delete(SectionPage $section): bool + { + return $section->delete(); + } + + public function toggleVisibility(SectionPage $section): SectionPage + { + $section->update([ + 'visibility' => ! $section->visibility, + ]); + + return $section->fresh(); + } +} diff --git a/database/migrations/2026_05_21_123934_add_data_to_respcter_la_terre_page.php b/database/migrations/2026_05_21_123934_add_data_to_respcter_la_terre_page.php new file mode 100644 index 00000000..0b7f9990 --- /dev/null +++ b/database/migrations/2026_05_21_123934_add_data_to_respcter_la_terre_page.php @@ -0,0 +1,83 @@ + [ + 'label' => 'Banner : Picto standard', + 'inputType' => InputType::image->value, + 'dimensions' => [ + 'width' => 42, + 'height' => 42 + ], + 'alt' => 'Pictogramme d\'un serrage de main.', + 'title' => 'Pictogramme d\'un serrage de main.', + 'value' => $picto, + 'validate' => 'nullable', + 'required' => false + ], + 'common_section' => [ + 'title' => 'découvrez plus d\'engagements le gaulois', + 'content' => '

Face aux enjeux de notre époque, Le Gaulois répond présent.
Chaque jour, nous avançons pas à pas avec les équipes, les partenaires et les consommateurs pour élaborer des produits gourmands, de qualité et ainsi créer de bons moments de convivialité à table et en cuisine. Découvrez nos 5 engagements et nos actions pour minimiser l’impact de notre activité sur l’environnement, veiller au bien-être animal et améliorer notre alimentation et notre qualité de vie.

' + ] + ]; + + $staticPage = StaticPage::where('type', StaticPageType::respectTheEarth)->first(); + if ($staticPage) { + $staticPage->data = $data; + $staticPage->saveQuietly(); + } else { + StaticPage::query()->forceCreateQuietly( + [ + 'type' => StaticPageType::respectTheEarth->value(), + 'seo_title' => 'Respecter la terre', + 'seo_description' => 'Optimiser le transport, privilégier des fournisseurs locaux et réduire les emballages... quelques actions pour agir ensemble sur notre empreinte carbone !', + 'title' => 'Respecter la terre', + 'slug' => 'respecter-la-terre', + 'content' => '

Le Gaulois s\'engage à réduire son empreinte carbone : optimiser le transport, privilégier des fournisseurs locaux et réduire les emballages.

', + 'picture' => $picture, + 'picture_alt' => 'Forêt éclairé par la lumière du soleil.', + 'picture_title' => 'Bannière de la page engagement n°5', + 'data' => $data + + ] + ); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('static_pages', function () { + $staticPage = StaticPage::query()->where('type', StaticPageType::respectTheEarth)->first(); + $staticPage->data = []; + $staticPage->saveQuietly(); + }); + } +}; diff --git a/database/migrations/2026_05_21_125623_create_section_pages_table.php b/database/migrations/2026_05_21_125623_create_section_pages_table.php new file mode 100644 index 00000000..efb5bc4b --- /dev/null +++ b/database/migrations/2026_05_21_125623_create_section_pages_table.php @@ -0,0 +1,26 @@ +id(); + $table->foreignId('page_id')->constrained('static_pages')->cascadeOnDelete()->comment('Reference to the static page'); + $table->string('name')->comment('Section name'); + $table->boolean('visibility')->default(true)->comment('section visibility'); + $table->mediumText('data')->comment('content stored as JSON'); + $table->unique(['page_id', 'name']); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('section_pages'); + } +}; diff --git a/database/migrations/2026_05_26_131337_create_blocks_table.php b/database/migrations/2026_05_26_131337_create_blocks_table.php new file mode 100644 index 00000000..4d9354ef --- /dev/null +++ b/database/migrations/2026_05_26_131337_create_blocks_table.php @@ -0,0 +1,31 @@ +id(); + $table->unsignedTinyInteger('type')->comment('Page type (BlockTypeEnum)'); + $table->foreignId('section_id')->constrained('section_pages')->cascadeOnDelete()->comment('section which belongs to'); + $table->mediumText('data')->comment('content stored as JSON'); + $table->unsignedInteger('order')->default(0); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('blocks'); + } +}; diff --git a/database/migrations/2026_05_26_152751_add_data_for_blocks.php b/database/migrations/2026_05_26_152751_add_data_for_blocks.php new file mode 100644 index 00000000..8efdb12d --- /dev/null +++ b/database/migrations/2026_05_26_152751_add_data_for_blocks.php @@ -0,0 +1,290 @@ + 'section 2', + 'data' => [ + 'title' => [ + 'label' => 'Section 2 : Titre', + 'inputType' => InputType::string->value, + 'value' => 'priorité à la réduction de notre empreinte carbone', + 'validate' => 'required|string|between:3,255', + 'required' => true + ], + 'description' => [ + 'label' => 'Section 2 : Description', + 'inputType' => InputType::text->value, + 'value' => '

Chaque initiative joue un rôle crucial dans la lutte contre les gaz à effet de serre, contribuant ainsi à préserver l\'équilibre de la planète et la biodiversité. Ainsi, Le Gaulois met en en place des actions concrètes face au réchauffement climatique.

', + 'validate' => 'required|string|min:15', + 'required' => true + ], + 'media' => [ + 'label' => 'Section 2 : Media', + 'inputType' => InputType::selectImageVideo->value, + 'dimensions' => [ + 'width' => 479, + 'height' => 291 + ], + 'required' => true, + 'validate' => 'required|array', + 'value' => [ + 'picture' => [ + 'path' => 'commitments/common/footprint.jpg', + 'alt' => 'Vue du dessus d\'une empreinte humaine de pied avec de l\'eau et entouré d\'une forêt verdoyante', + 'title' => 'Empreinte carbone de l\'activité humaine sur la Terre', + ] + ], + ], + 'media_picto' => [ + 'label' => 'Section 2 : Picto du media', + 'inputType' => InputType::image->value, + 'dimensions' => [ + 'width' => 74, + 'height' => 92 + ], + 'alt' => 'Pictogramme d\'une empreinte digitale humaine, symbôle de l\'engagement de Le Gaulois à réduire son empreinte carbone sur la terre.', + 'title' => 'Pictogramme d\'une empreinte de doigt', + 'value' => 'commitments/pictograms/picto-human-footprint.svg', + 'validate' => 'required', + 'required' => true + ], + + ], + 'blocks' => [ + 'reductionBlock' => true, + 'has_pictos' => true, + 'max_pictos' => 6, + ] + ], + [ + 'name' => 'section 3', + 'data' => [ + 'title' => [ + 'label' => 'Section 3 : Titre', + 'inputType' => InputType::string->value, + 'value' => 'stop à la déforestation, halte au soja non responsable', + 'validate' => 'required|string|between:3,255', + 'required' => true + ], + 'description' => [ + 'label' => 'Section 3 : Description', + 'inputType' => InputType::text->value, + 'value' => '

Chez Le Gaulois, pour assurer l\'apport protéique et assurer un équilibre nutritionnel de nos volailles, nous les alimentons avec du soja. C’est la légumineuse la plus riche en protéines et en acides aminés essentiels ! Conscient que la culture du Soja importée est assujettie à de nombreux questionnements, notamment son impact sur la déforestation.

', + 'validate' => 'required|string|min:15', + 'required' => true + ], + 'media' => [ + 'label' => 'Section 3 : Media', + 'inputType' => InputType::selectImageVideo->value, + 'dimensions' => [ + 'width' => 479, + 'height' => 291 + ], + 'required' => true, + 'validate' => 'required|array', + 'value' => [ + 'picture' => [ + 'path' => 'commitments/common/soja.jpg', + 'alt' => 'Graines de soja importées mais garanties zéro déforesation d\'ici 2025', + 'title' => 'Soja responsable et engagé contre la déforestation', + ] + ], + ], + 'media_picto' => [ + 'label' => 'Section 3 : Picto du media', + 'inputType' => InputType::image->value, + 'dimensions' => [ + 'width' => 116, + 'height' => 127 + ], + 'alt' => 'Céréales cultivées en France pour nourrir les volailles Le Gaulois', + 'title' => 'Pictogramme de céréales pour l’alimentation des volailles Le Gaulois', + 'value' => 'commitments/pictograms/picto-cereals.svg', + 'validate' => 'required', + 'required' => true + ], + 'encare_beige_title' => [ + 'label' => 'Section 3 : Titre Picto', + 'inputType' => InputType::string->value, + 'value' => '100% de soja', + 'validate' => 'required|string|between:3,255', + 'required' => true + ], + 'encare_beige_description' => [ + 'label' => 'Section 3 : Description picto', + 'inputType' => InputType::text->value, + 'value' => '
garanti zéro déforestation

conversion au plus tard d\'ici 2025.

', + 'validate' => 'required|string|min:15', + 'required' => true + ], + ], + 'blocks' => [ + 'reductionBlock' => true, + ] + ], + [ + 'name' => 'section 4', + 'data' => [ + 'title' => [ + 'label' => 'Section 4 : Titre', + 'inputType' => InputType::string->value, + 'value' => 'réduire l\'impact de nos emballages sur l\'environnement', + 'validate' => 'required|string|between:3,255', + 'required' => true + ], + 'description' => [ + 'label' => 'Section 4 : Description', + 'inputType' => InputType::text->value, + 'value' => '

Conscient de son rôle, Le Gaulois s ‘est engagé dans une démarche continue de réduction de ses emballages et d’amélioration de leur recyclabilité, tout en garantissant la sécurité alimentaire.
 

  • soit intégrant de la matière recyclée
  • soit en étant recyclables

D’ici fin 2025, nous ambitionnons de passer à près de 95% d’emballages responsables.

', + 'validate' => 'required|string|min:15', + 'required' => true + ], + 'media' => [ + 'label' => 'Section 4 : Media', + 'inputType' => InputType::selectImageVideo->value, + 'dimensions' => [ + 'width' => 479, + 'height' => 291 + ], + 'required' => true, + 'validate' => 'required|array', + 'value' => [ + 'picture' => [ + 'path' => 'commitments/common/box.jpg', + 'alt' => 'Colis en carton avec une estampille de recyclabilité', + 'title' => 'Emballages cartons issus de cartons recyclés ou certifiés FSC', + ] + ], + ], + 'media_picto' => [ + 'label' => 'Section 4 : Picto du media', + 'inputType' => InputType::image->value, + 'dimensions' => [ + 'width' => 89, + 'height' => 84 + ], + 'alt' => 'Pictogramme de recyclabilité sur les emballages et la gestion des déchets', + 'title' => 'Pictogramme sur les emballages et la gestion des déchets', + 'value' => 'commitments/pictograms/picto-recycle.svg', + 'validate' => 'required', + 'required' => true + ], + 'encare_beige' => [ + 'label' => 'Section 3 : Saviez-vous', + 'inputType' => InputType::text->value, + 'value' => '
Le saviez-vous ? 100% des emballages cartons Le Gaulois sont déjà issus de cartons recyclés et certifiés FSC, garantissant une gestion responsable des forêts, et plus de 60% des barquettes de volaille Le Gaulois sont recyclables ou intégrant au moins 50% de matière recyclée.
', + 'validate' => 'required|string|min:15', + 'required' => true + ], + ], + 'blocks' => [ + 'reductionBlock' => true, + 'has_pictos' => true, + 'max_pictos' => 4, + ] + ] + ]; + } + + /** + * Run the migrations. + */ + public function up(): void + { + $pages = StaticPage::where('type', StaticPageType::respectTheEarth)->with('sections.blocks')->get(); + + foreach ($pages as $page) { + foreach ($this->getSections() as $sectionConfig) { + $section = $page->sections->firstWhere('name', $sectionConfig['name']); + + if (!$section) { + $section = $page->sections()->create([ + 'name' => $sectionConfig['name'], + 'visibility' => true, + 'data' => [], + ]); + } + + // handling images in data + $sectionData = $sectionConfig['data']; + + foreach ($sectionData as $key => &$value) { + if (isset($value['inputType']) && $value['inputType'] === InputType::image->value) { + $value['value'] = FileStorageHelper::storeFile( + $page, + new \SplFileInfo(resource_path('assets/images/' . $value['value'])) + ); + } + + if (isset($value['inputType']) && $value['inputType'] === InputType::selectImageVideo->value) { + $value['value']['picture']['path'] = FileStorageHelper::storeFile( + $page, + new \SplFileInfo(resource_path('assets/images/' . $value['value']['picture']['path'])) + ); + } + } + unset($value); + + // has_pictos / max_pictos + if (!empty($sectionConfig['blocks']['has_pictos'])) { + $sectionData['has_pictos'] = true; + $sectionData['max_pictos'] = $sectionConfig['blocks']['max_pictos'] ?? 6; + } + + $section->update([ + 'data' => $sectionData, + 'visibility' => true, + ]); + + // Blocks + $blocks = []; + if (!empty($sectionConfig['blocks']['reductionBlock'])) { + $blocks[] = BlockTypeEnum::redirection->defaultConfig(); + } + + foreach ($blocks as $index => $blockConfig) { + Block::updateOrCreate([ + 'section_id' => $section->id, + 'type' => $blockConfig['type']->value, + 'data' => $blockConfig['data'], + 'order' => $index, + ]); + } + } + } + } + + + /** + * Reverse the migrations. + */ + public function down(): void + { + $pages = StaticPage::where('type', StaticPageType::respectTheEarth)->with('sections.blocks')->get(); + + foreach ($pages as $page) { + foreach ($this->getSections() as $sectionConfig) { + $section = $page->sections->firstWhere('name', $sectionConfig['name']); + + if (!$section) { + continue; + } + + $section->blocks()->delete(); + $section->delete(); + } + } + } +}; diff --git a/database/migrations/2026_05_28_164713_create_pictos_table.php b/database/migrations/2026_05_28_164713_create_pictos_table.php new file mode 100644 index 00000000..77765edf --- /dev/null +++ b/database/migrations/2026_05_28_164713_create_pictos_table.php @@ -0,0 +1,37 @@ +id(); + $table->string('title')->comment('title.'); + $table->string('description')->comment('content.'); + $table->string('picture')->comment('picture path.'); + $table->string('picture_alt')->comment('Picture alt attribute.'); + $table->string('picture_title')->comment('Picture title attribute.'); + $table->integer('picture_width')->default(Picto::MIN_WIDTH)->comment('Picture widht'); + $table->integer('picture_height')->default(Picto::MIN_HEIGHT)->comment('picture height'); + $table->foreignId('section_id')->constrained('section_pages')->cascadeOnDelete(); + $table->integer('order')->default(0); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('pictos'); + } +}; diff --git a/database/migrations/2026_06_03_102408_add_data_for_pictos.php b/database/migrations/2026_06_03_102408_add_data_for_pictos.php new file mode 100644 index 00000000..de0ee70f --- /dev/null +++ b/database/migrations/2026_06_03_102408_add_data_for_pictos.php @@ -0,0 +1,162 @@ + [ + [ + 'title' => 'COLISAGE OPTIMISÉ', + 'description' => 'avec des cartons pleins', + 'picture' => 'commitments/pictograms/picto-open-box.svg', + 'picture_alt' => 'Pictogramme d\'un carton ouvert pour optimiser les colisages', + 'picture_title' => 'Pictogramme d\'un colisage optimisé', + ], + [ + 'title' => 'FOURNISSEURS LOCAUX', + 'description' => 'privilégiés', + 'picture' => 'commitments/pictograms/picto-local-french-suppliers.svg', + 'picture_alt' => 'Pictogramme de la carte de France avec un emplacement de coq, illustrant les fournisseurs locaux privilégiés par Le Gaulois', + 'picture_title' => 'Pictogramme de fournisseurs français locaux privilégiés', + ], + [ + 'title' => 'RÉDUCTION DES DISTANCES', + 'description' => 'de transport des animaux', + 'picture' => 'commitments/pictograms/picto-terminal.svg', + 'picture_alt' => 'Pictogramme d\'une borne de distance kilométrique peu élevé pour réduire les distances de transport des volailles Le Gaulois', + 'picture_title' => 'Pictogramme de réduction des distance de transport des volailles', + ], + [ + 'title' => 'DES BOUCLES DE TRANSPORT VERTUEUSES', + 'description' => 'des produits', + 'picture' => 'commitments/pictograms/picto-map.svg', + 'picture_alt' => 'Pictogramme d\'une carte avec des boucles de transport plus vertueuses pour les produits Le Gaulois', + 'picture_title' => 'Pictogramme d\'une boucle de transport vertueux', + ], + [ + 'title' => 'AMÉLIORATION DE LA FLOTTE', + 'description' => 'dédiée au ramassage', + 'picture' => 'commitments/pictograms/picto-truck.svg', + 'picture_alt' => 'Pictogramme d\'un camion plus écologique pour la flotte dédiée au ramassage des volailles Le Gaulois', + 'picture_title' => 'Pictogramme d\'une flotte de transport dédié au ramassage plus écologique', + ], + [ + 'title' => 'BASSE CONSOMMATION', + 'description' => 'Des poulaillers à', + 'picture' => 'commitments/pictograms/picto-henhouse.svg', + 'picture_alt' => 'Poulaillers d\'élevage à basse consommation', + 'picture_title' => 'Pictogramme d\'un poulailler à basse consommation pour les volailles Le Gaulois', + ], + ], + 'section 4' => [ + [ + 'title' => 'RÉDUCTION DES EMBALLAGES', + 'description' => 'épaisseurs, dimensions et poids', + 'picture' => 'commitments/pictograms/picto-reduction-packaging.svg', + 'picture_alt' => 'Pictogramme d\'un poids "-kg" en baisse sur les emballages grâce à l\'impact des réductions sur les épaisseurs, les dimensions et le poids', + 'picture_title' => 'Pictogramme sur la réduction du poids des emballages', + ], + [ + 'title' => '100% DES EMBALLAGES CARTON', + 'description' => 'issus de cartons recyclés ou certifiés FSC Mixte', + 'picture' => 'commitments/pictograms/picto-recycled-cardboard-packaging.svg', + 'picture_alt' => 'Pictogramme d\'un carton issus de cartons recyclés ou certifiés FSC Mixte', + 'picture_title' => 'Pictogramme d\'un emballage carton recyclé ou certifié FSC', + ], + [ + 'sustitle' => 'Mise au point d\'une', + 'title' => 'FILIÈRE DE RECYCLABILITÉ', + 'description' => 'des barquettes de volaille Le Gaulois', + 'picture' => 'commitments/pictograms/picto-recycling-industry.svg', + 'picture_alt' => 'Pictogramme d\'une filière de recyclabilité en France sur les barquettes de volailles Le Gaulois', + 'picture_title' => 'Pictogramme d\'une filière de recyclabilité des barquettes', + ], + [ + 'title' => 'MOINS DE PLASTIQUE', + 'description' => 'minimiser notre empreinte carbone et agir sur le changement climatique', + 'picture' => 'commitments/pictograms/picto-plastic-decrease.svg', + 'picture_alt' => 'Pictogramme d\'un sac plastique avec l\'estampille de recyclabilité', + 'picture_title' => 'Pictogramme sur la diminution de plastique', + ], + ], + ]; + } + + /** + * Run the migrations. + */ + public function up(): void + { + $pages = StaticPage::where('type', StaticPageType::respectTheEarth)->with('sections')->get(); + + foreach ($pages as $page) { + foreach ($this->getPictos() as $sectionName => $pictos) { + $section = $page->sections->firstWhere('name', $sectionName); + + if (!$section) { + continue; + } + + $data = (array) $section->data ?? []; + + if (!($data['has_pictos'] ?? false)) { + continue; + } + + $maxPictos = $data['max_pictos'] ?? null; + if ($maxPictos !== null && count($pictos) > $maxPictos) { + throw new \Exception("Trop de pictos pour la section '{$sectionName}' (max: {$maxPictos}, fournis: " . count($pictos) . ")"); + } + + foreach ($pictos as $order => $pictoConfig) { + $picturePath = FileStorageHelper::storeFile( + $page, + new \SplFileInfo(resource_path('assets/images/' . $pictoConfig['picture'])) + ); + + Picto::create([ + 'title' => $pictoConfig['title'], + 'description' => $pictoConfig['description'] ?? null, + 'picture' => $picturePath, + 'picture_alt' => $pictoConfig['picture_alt'], + 'picture_title' => $pictoConfig['picture_title'], + 'section_id' => $section->id, + 'order' => $order + 1, + ]); + } + } + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + $pages = StaticPage::where('type', StaticPageType::respectTheEarth)->with('sections')->get(); + + foreach ($pages as $page) { + foreach (array_keys($this->getPictos()) as $sectionName) { + $section = $page->sections->firstWhere('name', $sectionName); + + if (!$section) { + continue; + } + + $data = (array) $section->data ?? []; + if (!($data['has_pictos'] ?? false)) { + continue; + } + + Picto::where('section_id', $section->id)->delete(); + } + } + } +}; diff --git a/resources/assets/images/commitments/commitment-5/picto-life.png b/resources/assets/images/commitments/commitment-5/picto-life.png new file mode 100644 index 0000000000000000000000000000000000000000..b138f6d934a56b422b3c67529d42719ba3ce5bea GIT binary patch literal 3150 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 oM5>{*`=0dN}S&m`chmXsLyep|m2 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 P2LEZOXe$>hS-<^%_kPSB literal 0 HcmV?d00001 diff --git a/resources/lang/fr/crud.php b/resources/lang/fr/crud.php index 0d0c0458..e200d421 100644 --- a/resources/lang/fr/crud.php +++ b/resources/lang/fr/crud.php @@ -12,6 +12,7 @@ return [ 'edit' => 'mettre à jour', 'view' => 'aperçu', 'order_submodels' => 'ordonnancement des sous-élements', + 'back' => 'retour' ], 'helpers' => [ diff --git a/resources/lang/fr/models.php b/resources/lang/fr/models.php index 240ac46f..742ed542 100644 --- a/resources/lang/fr/models.php +++ b/resources/lang/fr/models.php @@ -41,6 +41,7 @@ return [ 'sell_item' => 'objet en vente|objets en vente', 'rating' => 'note', 'categoryApproach' => 'categorie|categories', - 'sectionApproach' => 'section|sections' + 'sectionApproach' => 'section|sections', + 'picto' => 'pictogramme|pictogrammes' ] ]; diff --git a/resources/lang/fr/validation.php b/resources/lang/fr/validation.php index 3bf8b482..5f57228a 100644 --- a/resources/lang/fr/validation.php +++ b/resources/lang/fr/validation.php @@ -354,5 +354,6 @@ return [ 'picture_title' => 'titre de l\'image', 'video_alt' => 'texte alternatif de la vidéo', 'video_title' => 'titre de la vidéo', + 'section' => 'section|sections' ], ]; diff --git a/resources/ts/bo/base/common.ts b/resources/ts/bo/base/common.ts index bb5bd61e..549edb39 100644 --- a/resources/ts/bo/base/common.ts +++ b/resources/ts/bo/base/common.ts @@ -1,34 +1,52 @@ import sweetalert from "../../modules/sweetalertConfirm"; +import { apiFetch, showFlashGlobal } from "../utilities/submit-form"; + window.addEventListener("DOMContentLoaded", () => { /** * Popup delete item. */ - const elements = document.getElementsByClassName( - "confirmDeleteTS" - ) as HTMLCollectionOf; + const elements = document.getElementsByClassName("confirmDeleteTS") as HTMLCollectionOf; + for (const element of elements) { - element.addEventListener("submit", function (e) { - e.preventDefault(); - const el = e.target; - if (!el || !(el instanceof HTMLFormElement)) { - throw new Error( - "confirmDoneJS can only be executed on an exising form" - ); - } - sweetalert.methods.confirm( - "Êtes-vous sûr ?", - "Toutes les données vont être perdu", - el as HTMLFormElement, - function () { - (el as HTMLFormElement).submit(); - }, - { icon: "warning" } - ); - return false; - }); + const submitBtn = element.querySelector('button[type="submit"]'); + + submitBtn?.addEventListener("click", function (e) { + e.preventDefault(); + e.stopPropagation(); + + const form = (e.target as HTMLElement).closest('.confirmDeleteTS'); + if (!form) return; + + const pictoId = form.dataset.picto; + + sweetalert.methods.confirm( + "Êtes-vous sûr ?", + "Toutes les données vont être perdu", + form, + async function () { + if (pictoId) { + try { + const data = await apiFetch(`/bo/pictos/${pictoId}`, 'POST', form, 'DELETE'); + form.closest('tr')?.remove(); + setTimeout(() => { + showFlashGlobal(data?.message ?? 'Picto supprimé avec succès.', 'success'); + }, 300); + } catch (err: any) { + showFlashGlobal(err?.message ?? 'Erreur lors de la suppression.', 'danger'); + console.error(err); + } + } else { + form.submit(); + } + }, + { icon: "warning" } + ); +}); + } + /** * Popup delete item. */ @@ -61,3 +79,13 @@ window.addEventListener("DOMContentLoaded", () => { }); } }); + + + + + + + + + + diff --git a/resources/ts/bo/bo.ts b/resources/ts/bo/bo.ts index f3a1468f..ba8bdf43 100644 --- a/resources/ts/bo/bo.ts +++ b/resources/ts/bo/bo.ts @@ -8,6 +8,11 @@ import "./utilities/bootstrap"; import "./utilities/fontawesome"; import "./utilities/vue-bo"; window.initMediaSelector = initMediaSelector; +import "./utilities/toogle-visibility"; +import "./utilities/submit-form"; +import "./utilities/toogle-block"; +import "./utilities/picto-picture-size"; +import "./utilities/picto-form"; // MODULES import "./../modules/sweetalert"; diff --git a/resources/ts/bo/utilities/media-selector.ts b/resources/ts/bo/utilities/media-selector.ts index 767ece12..0f66e2f4 100644 --- a/resources/ts/bo/utilities/media-selector.ts +++ b/resources/ts/bo/utilities/media-selector.ts @@ -4,31 +4,39 @@ export function initMediaSelector(id: string) { return; } - const mediaVideo = document.getElementById("mediaSelectorVideo") as HTMLInputElement, - mediaImage = document.getElementById("mediaSelectorImage") as HTMLInputElement, - mediaEmpty = document.getElementById("mediaSelectorEmpty") as HTMLInputElement; + const mediaVideo = container.querySelector(`#${id}SelectorVideo`) as HTMLInputElement, + mediaImage = container.querySelector(`#${id}SelectorImage`) as HTMLInputElement, + mediaEmpty = container.querySelector(`#${id}SelectorEmpty`) as HTMLInputElement; mediaVideo?.addEventListener("change", function() { - hideFormPart(`.${id}Picture`); - showFormPart(`.${id}Video`); - hideFormPart(`.${id}None`); + hideFormPart(container, `.${id}Picture`); + showFormPart(container, `.${id}Video`); + hideFormPart(container, `.${id}None`); }); mediaImage?.addEventListener("change", function() { - hideFormPart(`.${id}Video`); - showFormPart(`.${id}Picture`); - hideFormPart(`.${id}None`); + hideFormPart(container, `.${id}Video`); + showFormPart(container, `.${id}Picture`); + hideFormPart(container, `.${id}None`); }); mediaEmpty?.addEventListener("change", function() { - hideFormPart(`.${id}Video`); - hideFormPart(`.${id}Picture`); - showFormPart(`.${id}None`); + hideFormPart(container, `.${id}Video`); + hideFormPart(container, `.${id}Picture`); + showFormPart(container, `.${id}None`); }); + + if (mediaVideo?.checked) { + mediaVideo.dispatchEvent(new Event("change")); + } else if (mediaImage?.checked) { + mediaImage.dispatchEvent(new Event("change")); + } else if (mediaEmpty?.checked) { + mediaEmpty.dispatchEvent(new Event("change")); + } } -function hideFormPart(id: string) { - const parts = document.querySelectorAll(id); +function hideFormPart(container: HTMLElement, selector: string) { + const parts = container.querySelectorAll(selector); parts.forEach(partToHide => { partToHide.classList.add("d-none"); const inputs = partToHide.querySelectorAll("input"); @@ -37,8 +45,9 @@ function hideFormPart(id: string) { } }); } -function showFormPart(id: string) { - const parts = document.querySelectorAll(id); + +function showFormPart(container: HTMLElement, selector: string) { + const parts = container.querySelectorAll(selector); parts.forEach(partToShow => { partToShow.classList.remove("d-none"); const inputs = partToShow.querySelectorAll("input"); diff --git a/resources/ts/bo/utilities/picto-form.ts b/resources/ts/bo/utilities/picto-form.ts new file mode 100644 index 00000000..36ebaf85 --- /dev/null +++ b/resources/ts/bo/utilities/picto-form.ts @@ -0,0 +1,241 @@ +import { apiFetch, initPictureSizeButtons, showFlashGlobal } from "./submit-form"; +import { initVueComponents } from "./vue-bo"; +import * as bootstrap from 'bootstrap'; + + + +function getBtnCreate(sectionId: string | undefined) { + return document.querySelector(`.btn-create-picto[data-section="${sectionId}"]`); +} + + + +// PICTO CREATE +document.addEventListener('DOMContentLoaded', () => { + document.querySelectorAll('.btn-create-picto').forEach(btnCreate => { + const sectionId = btnCreate.dataset.section; + + const pictoList = document.querySelector(`.picto-list[data-section="${sectionId}"]`); + const pictoForm = document.querySelector(`.picto-form-wrapper[data-section="${sectionId}"]`); + const btnBack = pictoForm?.querySelector('.btn-back-picto'); + + const originalHTML = pictoForm!.innerHTML; + + btnCreate.addEventListener('click', (e: Event) => { + e.preventDefault(); + + pictoForm!.innerHTML = originalHTML; + initVueComponents(pictoForm!); + initPictureSizeButtons(pictoForm!); + + pictoForm!.querySelector('.btn-back-picto')?.addEventListener('click', () => { + pictoForm!.style.display = 'none'; + pictoList!.style.display = 'block'; + btnCreate.style.display = ''; + }); + + pictoList!.style.display = 'none'; + pictoForm!.style.display = 'block'; + btnCreate.style.display = 'none'; + }); + + btnBack?.addEventListener('click', () => { + pictoForm!.style.display = 'none'; + pictoList!.style.display = 'block'; + btnCreate.style.display = ''; + }); + }); +}); + + +// PICTO EDIT +document.addEventListener('click', async (e) => { + const btn = (e.target as HTMLElement).closest('.btn-edit-picto'); + if (!btn) return; + e.preventDefault(); + + const pictoId = btn.dataset.picto; + const sectionId = btn.dataset.section; + const wrapper = document.querySelector(`.picto-form-wrapper[data-section="${sectionId}"]`); + const list = document.querySelector(`.picto-list[data-section="${sectionId}"]`); + const btnCreate = getBtnCreate(sectionId); + if (!wrapper || !list) return; + + const res = await fetch(`/bo/pictos/${pictoId}/edit`, { + headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'text/html' } + }); + if (!res.ok) return; + const html = await res.text(); + + const backBtn = wrapper.querySelector('.btn-back-picto')!.cloneNode(true); + wrapper.innerHTML = ''; + wrapper.appendChild(backBtn); + const formContainer = document.createElement('div'); + formContainer.innerHTML = html; + wrapper.appendChild(formContainer); + initVueComponents(formContainer); + initPictureSizeButtons(formContainer); + + list.style.display = 'none'; + wrapper.style.display = 'block'; + if (btnCreate) btnCreate.style.display = 'none'; + + // Rebind back button + wrapper.querySelector('.btn-back-picto')?.addEventListener('click', () => { + wrapper.style.display = 'none'; + list.style.display = 'block'; + if (btnCreate) btnCreate.style.display = ''; + }); + +}); + +// CHANGE ORDER +const pendingOrders = new Set(); + +document.addEventListener('submit', async (e) => { + const form = (e.target as HTMLElement).closest('[data-change-order]'); + if (!form) return; + e.preventDefault(); + + const sectionId = form.closest('.picto-list')?.dataset.section; + if (!sectionId) return; + + // Bloc refresh + if (pendingOrders.has(sectionId)) return; + pendingOrders.add(sectionId); + + (document.activeElement as HTMLElement)?.blur(); + + form.closest('tr')?.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => { + bootstrap.Tooltip.getInstance(el)?.dispose(); + }); + + try { + const data = await apiFetch(form.action, 'POST', form, 'PATCH'); + const flashContainer = document.querySelector(`.picto-list[data-section="${sectionId}"] .flash-container`); + if (flashContainer) { + flashContainer.innerHTML = ` + `; + setTimeout(() => { flashContainer.innerHTML = ''; }, 5000); + } + + await new Promise(resolve => setTimeout(resolve, 800)); + + const freshContainer = document.querySelector(`.picto-list[data-section="${sectionId}"]`); + if (!freshContainer) return; + + freshContainer.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => { + bootstrap.Tooltip.getInstance(el)?.dispose(); + }); + + const res = await fetch(`/bo/pictos?section_id=${sectionId}`, { + headers: { 'X-Requested-With': 'XMLHttpRequest' } + }); + + if (res.ok) { + const tableWrapper = freshContainer.querySelector('.picto-table-wrapper'); + if (!tableWrapper) return; + tableWrapper.innerHTML = await res.text(); + + freshContainer.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => { + const tt = new bootstrap.Tooltip(el); + el.addEventListener('hidden.bs.tooltip', () => tt.dispose(), { once: true }); + }); + } + } catch (e: any) { + const flashContainer = document.querySelector(`.picto-list[data-section="${sectionId}"] .flash-container`); + if (flashContainer) { + flashContainer.innerHTML = ` + `; + setTimeout(() => { flashContainer.innerHTML = ''; }, 4000); + } + console.error(e); + } finally { + pendingOrders.delete(sectionId); + } +}); + +// ORDER TABLE PICTOS +document.addEventListener('click', async (e) => { + const link = (e.target as HTMLElement).closest('#pictos-table thead a'); + if (!link) return; + e.preventDefault(); + + const sectionId = link.closest('.picto-list')?.dataset.section; + if (!sectionId) return; + + const url = new URL(link.href); + const sortCol = url.searchParams.get('sort_col'); + const sortWay = url.searchParams.get('sort_way'); + + const freshContainer = document.querySelector(`.picto-list[data-section="${sectionId}"]`); + if (!freshContainer) return; + + const params = new URLSearchParams({ section_id: sectionId }); + if (sortCol) params.set('sort_col', sortCol); + if (sortWay) params.set('sort_way', sortWay); + + const res = await fetch(`/bo/pictos?${params.toString()}`, { + headers: { 'X-Requested-With': 'XMLHttpRequest' } + }); + + if (res.ok) { + freshContainer.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => { + bootstrap.Tooltip.getInstance(el)?.dispose(); + }); + const tableWrapper = freshContainer.querySelector('.picto-table-wrapper'); + if (!tableWrapper) return; + tableWrapper.innerHTML = await res.text(); + + // Afficher le bouton reset si un tri est actif + const resetBtn = freshContainer.querySelector('.picto-sort-reset'); + if (resetBtn && sortCol) resetBtn.classList.remove('d-none'); + + freshContainer.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => { + const tt = new bootstrap.Tooltip(el); + el.addEventListener('hidden.bs.tooltip', () => tt.dispose(), { once: true }); + }); + } +}); + +// RESET SORT PICTOS +document.addEventListener('click', async (e) => { + const resetLink = (e.target as HTMLElement).closest('.picto-sort-reset a'); + if (!resetLink) return; + e.preventDefault(); + + const sectionId = resetLink.closest('.picto-list')?.dataset.section; + if (!sectionId) return; + + const freshContainer = document.querySelector(`.picto-list[data-section="${sectionId}"]`); + if (!freshContainer) return; + + const res = await fetch(`/bo/pictos?section_id=${sectionId}`, { + headers: { 'X-Requested-With': 'XMLHttpRequest' } + }); + + if (res.ok) { + freshContainer.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => { + bootstrap.Tooltip.getInstance(el)?.dispose(); + }); + + const tableWrapper = freshContainer.querySelector('.picto-table-wrapper'); + if (!tableWrapper) return; + tableWrapper.innerHTML = await res.text(); + + freshContainer.querySelector('.picto-sort-reset')?.classList.add('d-none'); + + freshContainer.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => { + const tt = new bootstrap.Tooltip(el); + el.addEventListener('hidden.bs.tooltip', () => tt.dispose(), { once: true }); + }); + } +}); \ No newline at end of file diff --git a/resources/ts/bo/utilities/picto-picture-size.ts b/resources/ts/bo/utilities/picto-picture-size.ts new file mode 100644 index 00000000..10df0f66 --- /dev/null +++ b/resources/ts/bo/utilities/picto-picture-size.ts @@ -0,0 +1,113 @@ +import { createApp } from 'vue'; +import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; +import ImageInputComponent from '../components/ImageInputComponent.vue'; + +function remountImageInput(el: Element, json: object): void { + const app = createApp(ImageInputComponent, { json: JSON.stringify(json) }); + app.config.globalProperties.window = window; + app.config.globalProperties.document = document; + app.component('FontAwesomeIcon', FontAwesomeIcon); + app.mount(el); +} + +function findImageWrapperById(targetId: string): HTMLElement | null { + return Array.from( + document.querySelectorAll('.image-input[data-json]') + ).find(el => { + try { + return String(JSON.parse(el.getAttribute('data-json')!).id) === String(targetId); + } catch { + return false; + } + }) ?? null; +} + +export function initPictureSizeButton(saveBtn: HTMLButtonElement): void { + const targetId = saveBtn.dataset.targetImage; + const widthInputId = saveBtn.dataset.widthInput; + const heightInputId = saveBtn.dataset.heightInput; + const url = saveBtn.dataset.url; + + if (!targetId || !widthInputId || !heightInputId || !url) return; + + const widthInput = document.getElementById(widthInputId) as HTMLInputElement | null; + const heightInput = document.getElementById(heightInputId) as HTMLInputElement | null; + + const applyDimensions = (width: string, height: string): void => { + const preview = document.querySelector(`#${targetId}-preview`); + if (preview && width && height) { + preview.style.width = `${width}px`; + preview.style.height = `${height}px`; + } + }; + + saveBtn.addEventListener('click', async (e: MouseEvent): Promise => { + e.preventDefault(); + + const width = widthInput?.value ?? ''; + const height = heightInput?.value ?? ''; + const csrf = document.querySelector('meta[name="csrf-token"]')?.content; + const pictoId = saveBtn.dataset.pictoId ?? null; + + if (!width || !height) return; + + // mounted component with new sizes + const applyAndRemount = (): void => { + const oldWrapper = findImageWrapperById(targetId); + if (!oldWrapper) return; + + const json = JSON.parse(oldWrapper.getAttribute('data-json')!); + json.width = Number(width); + json.height = Number(height); + json.helper = `Doit être ${width}px (largeur) par ${height}px (hauteur)`; + + const newWrapper = document.createElement('div'); + newWrapper.className = 'image-input'; + newWrapper.setAttribute('data-json', JSON.stringify(json)); + oldWrapper.replaceWith(newWrapper); + + remountImageInput(newWrapper, json); + applyDimensions(width, height); + }; + + // handle helper when no pictoId + if (!pictoId) { + applyAndRemount(); + return; + } + + // in edit request + if (!csrf) return; + + try { + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrf, + }, + body: JSON.stringify({ + picture_width: width, + picture_height: height, + picto_id: pictoId, + }), + }); + } catch (error) { + console.error('Erreur lors de la sauvegarde des dimensions :', error); + } finally { + applyAndRemount(); + } + }); + + + + + // when update dimension re upload img + const pictureInput = document.querySelector( + `#${targetId} input[name="picture"]` + ); + pictureInput?.addEventListener('change', (): void => { + applyDimensions(widthInput?.value ?? '', heightInput?.value ?? ''); + }); +} + diff --git a/resources/ts/bo/utilities/submit-form.ts b/resources/ts/bo/utilities/submit-form.ts new file mode 100644 index 00000000..aae5b090 --- /dev/null +++ b/resources/ts/bo/utilities/submit-form.ts @@ -0,0 +1,184 @@ +// Helpers + +import { initPictureSizeButton } from "./picto-picture-size"; + +function clearFormErrors(form: HTMLFormElement) { + form.querySelectorAll('.is-invalid').forEach(el => el.classList.remove('is-invalid')); + form.querySelectorAll('.invalid-feedback').forEach(el => el.remove()); +} + +function displayFormErrors(form: HTMLFormElement, errors: Record) { + clearFormErrors(form); + Object.entries(errors).forEach(([field, messages]) => { + const inputName = field.replace(/\.(\w+)/g, '[$1]'); + const input = form.querySelector(`[name="${inputName}"]`) as HTMLElement; + if (!input) return; + input.classList.add('is-invalid'); + const feedback = document.createElement('div'); + feedback.classList.add('invalid-feedback'); + feedback.textContent = messages[0]; + input.after(feedback); + }); +} + +function getFlashContainer(form: HTMLFormElement): HTMLElement { + let container = form.previousElementSibling as HTMLElement; + if (!container || !container.classList.contains('flash-container')) { + container = document.createElement('div'); + container.classList.add('flash-container'); + form.parentElement!.insertBefore(container, form); + } + return container; +} + +async function fetchFlash(container: HTMLElement) { + const res = await fetch('/bo/flash', { + headers: { 'Accept': 'application/json' } + }); + const data = await res.json(); + if (data.html) { + container.innerHTML = data.html; + } +} + +function showSuccess(form: HTMLFormElement, message: string) { + const container = getFlashContainer(form); + container.innerHTML = ` + `; + container.scrollIntoView({ behavior: 'smooth', block: 'center' }); + setTimeout(() => container.innerHTML = '', 5000); +} + +function showError(form: HTMLFormElement, message: string) { + const container = getFlashContainer(form); + container.innerHTML = ` + `; + container.scrollIntoView({ behavior: 'smooth', block: 'center' }); + setTimeout(() => container.innerHTML = '', 5000); +} + +export function showFlashGlobal(message: string, type: 'success' | 'danger' = 'success') { + let container = document.querySelector('.flash-container'); + if (!container) { + container = document.createElement('div'); + container.classList.add('flash-container'); + document.body.prepend(container); + } + const icon = type === 'success' ? 'fa-circle-check' : 'fa-circle-xmark'; + container.innerHTML = ` + `; + container.scrollIntoView({ behavior: 'smooth', block: 'center' }); + setTimeout(() => { if (container) container.innerHTML = ''; }, 5000); +} + + +export async function handleSubmit(form: HTMLFormElement, action: (form: HTMLFormElement) => Promise) { + clearFormErrors(form); + try { + const data = await action(form); + showSuccess(form, data.message ?? 'Enregistré avec succès.'); + } catch (e: any) { + if (e.errors) displayFormErrors(form, e.errors); + if (e.message && !e.errors) showError(form, e.message); + console.error(e); + const container = getFlashContainer(form); + container.scrollIntoView({ behavior: 'smooth', block: 'center' }); + await fetchFlash(container); + } +} + +export function initPictureSizeButtons(container: HTMLElement = document.body): void { + container.querySelectorAll('.submit-picture-size') + .forEach(initPictureSizeButton); +} + +// API + +export async function apiFetch(url: string, method: string, form: HTMLFormElement, override?: string) { + const token = (form.querySelector('input[name="_token"]') as HTMLInputElement)?.value; + const headers: Record = { + 'X-CSRF-TOKEN': token, + 'Accept': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + + }; + + const isDelete = method === 'DELETE'; + const body = isDelete ? undefined : new FormData(form); + if (!isDelete && override) body!.append('_method', override); + + const response = await fetch(url, { + method, + headers, + body + }); + + const data = await response.json(); + if (!response.ok) throw data; + return data; +} + + + +// Handlers + +document.addEventListener('submit', async (e) => { + const form = (e.target as HTMLElement).closest('.section-form'); + if (!form) return; + e.preventDefault(); + await handleSubmit(form, f => apiFetch(`/bo/sections/${f.dataset.id}`, 'POST', f, 'PATCH')); +}); + +document.addEventListener('submit', async (e) => { + const form = (e.target as HTMLElement).closest('.block-form'); + if (!form) return; + e.preventDefault(); + await handleSubmit(form, f => apiFetch(`/bo/blocks/${f.dataset.id}`, 'POST', f, 'PUT')); +}); + +document.addEventListener('submit', async (e) => { + const form = (e.target as HTMLElement).closest('.picto-form'); + if (!form) return; + e.preventDefault(); + await handleSubmit(form, async f => { + const data = f.dataset.id + ? await apiFetch(`/bo/pictos/${f.dataset.id}`, 'POST', f, 'PUT') + : await apiFetch('/bo/pictos', 'POST', f); + + const sectionId = (f.querySelector('input[name="section_id"]') as HTMLInputElement)?.value; + if (sectionId && !f.dataset.id) { + const listContainer = document.querySelector(`.picto-list[data-section="${sectionId}"]`); + const wrapper = document.querySelector(`.picto-form-wrapper[data-section="${sectionId}"]`); + const btnCreate = document.querySelector(`.btn-create-picto[data-section="${sectionId}"]`); + + if (listContainer) { + const res = await fetch(`/bo/pictos?section_id=${sectionId}`, { + headers: { 'X-Requested-With': 'XMLHttpRequest' } + }); + if (res.ok) listContainer.innerHTML = await res.text(); + } + + if (wrapper) wrapper.style.display = 'none'; + if (listContainer) listContainer.style.display = 'block'; + if (btnCreate) btnCreate.style.display = ''; + } + + return data; + }); +}); + +document.addEventListener('DOMContentLoaded', () => { + initPictureSizeButtons(); +}); \ No newline at end of file diff --git a/resources/ts/bo/utilities/toogle-block.ts b/resources/ts/bo/utilities/toogle-block.ts new file mode 100644 index 00000000..2930d3ae --- /dev/null +++ b/resources/ts/bo/utilities/toogle-block.ts @@ -0,0 +1,27 @@ +// for show or hide fields (block reduction) +document.querySelectorAll('[id$="-block-type"]').forEach((sel) => { + const prefix = sel.id.replace(/-block-type$/, ''); + const ctaDiv = document.getElementById(`${prefix}-fields-cta`); + const actDiv = document.getElementById(`${prefix}-fields-actu`); + + if (!ctaDiv || !actDiv) return; + + function toggle(): void { + const isCta = sel.value === 'cta'; + const isActu = sel.value === 'actu'; + const isBoth = sel.value === 'both'; + + ctaDiv!.classList.toggle('d-none', isActu); + actDiv!.classList.toggle('d-none', isCta); + + ctaDiv!.querySelectorAll('input, textarea, select').forEach(el => { + el.disabled = isActu; + }); + actDiv!.querySelectorAll('input, textarea, select').forEach(el => { + el.disabled = isCta; + }); + } + + sel.addEventListener('change', toggle); + toggle(); +}); diff --git a/resources/ts/bo/utilities/toogle-visibility.ts b/resources/ts/bo/utilities/toogle-visibility.ts new file mode 100644 index 00000000..2586651a --- /dev/null +++ b/resources/ts/bo/utilities/toogle-visibility.ts @@ -0,0 +1,88 @@ +type ToggleResponse = { + data: { + visibility: boolean; + }; +}; + +const csrfToken = document + .querySelector('meta[name="csrf-token"]') + ?.content; + +if (!csrfToken) { + throw new Error("CSRF token not found"); +} + +function showAlert(container: HTMLElement, message: string, type: 'success' | 'danger') { + const alert = document.createElement('div'); + alert.className = `alert alert-${type} alert-dismissible fade show`; + alert.innerHTML = ` + ${message} + + `; + container.prepend(alert); + setTimeout(() => alert.remove(), 3000); +} + +const toggles = document.querySelectorAll('.section-toggle'); + +toggles.forEach((toggle) => { + toggle.addEventListener('change', async function () { + const sectionId = this.dataset.id; + const targetSelector = this.dataset.target; + + if (!sectionId) return; + + const status = document.getElementById(`status-${sectionId}`); + const target = targetSelector + ? document.querySelector(targetSelector) + : null; + + if (target) { + target.style.display = this.checked ? '' : 'none'; + } + + this.disabled = true; + const flashContainer = document.querySelector(`#flash-${sectionId}`); + + try { + const response = await fetch(`/bo/sections/${sectionId}/toggle-visibility`, { + method: 'POST', + headers: { + 'X-CSRF-TOKEN': csrfToken, + 'Content-Type': 'application/json' + }, + }); + + if (!response.ok) { + throw new Error('Request failed'); + } + + const data: ToggleResponse = await response.json(); + + // sync with DB + this.checked = data.data.visibility; + + if (status) status.innerText = "OK"; + if (flashContainer) { + showAlert(flashContainer, 'Visibilité de la section est mise à jour.', 'success'); + } + } catch (error) { + this.checked = !this.checked; + if (target) { + target.style.display = this.checked ? '' : 'none'; + } + if (status) status.innerText = "KO"; + if (flashContainer) { + showAlert(flashContainer, 'Une erreur est survenue.', 'danger'); + } + console.error(error); + } finally { + this.disabled = false; + if (status) { + setTimeout(() => { + status.innerText = ""; + }, 2000); + } + } + }); +}); diff --git a/resources/ts/bo/utilities/vue-bo.ts b/resources/ts/bo/utilities/vue-bo.ts index 0ab2995b..aca83c08 100644 --- a/resources/ts/bo/utilities/vue-bo.ts +++ b/resources/ts/bo/utilities/vue-bo.ts @@ -2,19 +2,18 @@ import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; import type { App, Component } from "vue"; import { createApp } from "vue"; -document.addEventListener("DOMContentLoaded", () => { - const setupApp = (app: App) => { - app.config.globalProperties.window = window; - app.config.globalProperties.document = document; - app.component("FontAwesomeIcon", FontAwesomeIcon); - }; +const setupApp = (app: App) => { + app.config.globalProperties.window = window; + app.config.globalProperties.document = document; + app.component("FontAwesomeIcon", FontAwesomeIcon); +}; - // Register components - /// - const vues = import.meta.glob("./../components/*.vue") as Record< - string, - () => Promise<{ default: Component }> - >; +const vues = import.meta.glob("./../components/*.vue") as Record< + string, + () => Promise<{ default: Component }> +>; + +export const initVueComponents = (root: HTMLElement | Document = document) => { for (const vueCptName in vues) { const vueCptPromise = vues[vueCptName]; let htmlKey = vueCptName.split("/").pop(); @@ -29,21 +28,23 @@ document.addEventListener("DOMContentLoaded", () => { .replace(/^-/, "") .replace("-component", ""); - const el = document.getElementById(htmlKey), - elGroup = document.getElementsByClassName(htmlKey); - // * Init using ID - if (el) { - vueCptPromise().then((vueParsedFile) => { - const app = createApp(vueParsedFile.default, { - json: el.dataset.json ?? {}, + // * Init using ID (seulement sur document) + if (root === document) { + const el = document.getElementById(htmlKey); + if (el) { + vueCptPromise().then((vueParsedFile) => { + const app = createApp(vueParsedFile.default, { + json: el.dataset.json ?? {}, + }); + setupApp(app); + app.mount(el); }); - setupApp(app); - app.mount(el); - }); + } } - // * Init Using class + + // * Init using class + const elGroup = root.getElementsByClassName(htmlKey); for (const grpEl of elGroup) { - // * Hack to prevent loop weirdness vueCptPromise().then((vueParsedFile) => { setTimeout(() => { const app = createApp(vueParsedFile.default, { @@ -55,4 +56,8 @@ document.addEventListener("DOMContentLoaded", () => { }); } } +}; + +document.addEventListener("DOMContentLoaded", () => { + initVueComponents(); }); diff --git a/resources/views/bo/layouts/sidemenu.blade.php b/resources/views/bo/layouts/sidemenu.blade.php index aa542230..2f0c1a93 100644 --- a/resources/views/bo/layouts/sidemenu.blade.php +++ b/resources/views/bo/layouts/sidemenu.blade.php @@ -2,11 +2,12 @@ @auth('backend') @php $approachType = intval(request()->route()->parameter('approach_type') ?: ((isset($approachModel)) ? $approachModel->type->value() : 0)); @endphp @php $newsClubType = intval(request()->route()->parameter('news_club_type') ?: ((isset($newsClubModel)) ? $newsClubModel->type->value() : 0)); @endphp -