feat ✨ final commit for respect earth page

This commit is contained in:
Abdel LAMRANI 2026-06-04 14:20:18 +02:00
parent 797067520e
commit 00d9db93aa
72 changed files with 4210 additions and 467 deletions

View file

@ -0,0 +1,30 @@
<?php
namespace App\Enums\Block;
use App\Traits\Enums\BaseEnum;
enum BlockContentTypeEnum: string
{
use BaseEnum;
case Cta = 'cta';
case Actu = 'actu';
case Both = 'both';
private const LABELS = [
'Cta' => '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'));
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace App\Enums\Block;
use App\Traits\Enums\BaseEnum;
enum BlockPositionEnum: string
{
use BaseEnum;
case Gauche = 'gauche';
case Centre = 'centre';
case Droite = 'droite';
const LABELS = [
'Gauche' => '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',
};
}
}

View file

@ -0,0 +1,85 @@
<?php
namespace App\Enums;
use App\Enums\Block\BlockContentTypeEnum;
use App\Enums\Block\BlockPositionEnum;
use App\Traits\Enums\BaseEnum;
enum BlockTypeEnum: int
{
use BaseEnum;
case redirection = 1;
case picto = 2;
/**
* Optionnal labels definition.
*
* @phpstan-ignore-next-line
*/
private const LABELS = [
self::redirection->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]],
],
],
],
};
}
}

View file

@ -26,4 +26,5 @@ enum InputType: int
case video = 16;
case slider = 17;
case selectImageVideo = 18;
case searchBelongsTo = 19;
}

View file

@ -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<string>
*/
public static function getCommitmentsPages(): array
{
return [
self::respectTheEarth,
// self::forLivingTogether
];
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\BlockPage\UpdateBlockRequest;
use App\Models\Block;
use App\Services\StaticPage\BlockService;
use Illuminate\Http\Request;
class BlockController extends Controller
{
public function __construct(
private readonly BlockService $service
) {}
/**
* Update the specified resource in storage.
*/
public function update(UpdateBlockRequest $request, Block $block): \Illuminate\Http\JsonResponse
{
$this->service->update($block, $request->validated());
return response()->json([
'success' => true,
'message' => __('Bloc mis à jour avec succès.'),
]);
}
}

View file

@ -0,0 +1,199 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Pictos\StorePictoRequest;
use App\Http\Requests\Bo\UpdateChangeOrderRequest;
use App\Models\Picto;
use App\Services\Picto\PictoService;
use App\Traits\Controllers\ChangesModelOrder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Laravel\Octane\Exceptions\DdException;
use Illuminate\Support\Str;
class PictoController extends Controller
{
use ChangesModelOrder;
public function __construct(
private readonly PictoService $pictoService
) {
$this->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);
}
}

View file

@ -0,0 +1,79 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\SectionPages\UpdateSectionPageRequest;
use App\Models\SectionPage;
use App\Models\StaticPage;
use App\Services\StaticPage\SectionPageService;
use Illuminate\Http\JsonResponse;
class SectionPageController extends Controller
{
public function __construct(
private readonly SectionPageService $service
) {}
public function index(StaticPage $page): JsonResponse
{
return response()->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,
]);
}
}

View file

@ -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,
]);
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace App\Http\Controllers\Fo;
use App\Enums\StaticPageType;
use App\Http\Controllers\Controller;
use App\Models\StaticPage;
class CommitmentController extends Controller
{
/**
* Display the given breeding page.
* @return \Illuminate\Contracts\View\View
*/
public function getRestpectEarthPage(): \Illuminate\Contracts\View\View
{
$staticPageModel = StaticPage::getCachedStaticPage(StaticPageType::respectTheEarth);
$staticPageModel->load(['sections.blocks', 'sections.pictos']);
return view('fo.pages.commitments.respect', ['staticPageModel' => $staticPageModel]);
}
}

View file

@ -0,0 +1,191 @@
<?php
namespace App\Http\Requests\Bo\BlockPage;
use App\Enums\Block\BlockContentTypeEnum;
use App\Enums\BlockTypeEnum;
use App\Http\Requests\Bo\UpdateMiniCrudRequest;
use App\Traits\Requests\HasPicture;
use Illuminate\Support\Facades\Gate;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Validation\Rule;
class UpdateBlockRequest extends UpdateMiniCrudRequest
{
use HasPicture;
protected $modelName = 'block';
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return Gate::check('update', $this->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, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|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, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|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, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|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;
}
}

View file

@ -0,0 +1,90 @@
<?php
namespace App\Http\Requests\Bo\Pictos;
use App\Models\Picto;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Http\FormRequest;
use App\Rules\WysiwygLength;
use App\Traits\Requests\HasPicture;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;
class StorePictoRequest extends FormRequest
{
use HasPicture;
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return Gate::check('create', Picto::class);
}
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation(): void
{
$this->mergePicture('picture');
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|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)
);
}
}

View file

@ -0,0 +1,76 @@
<?php
namespace App\Http\Requests\Bo\SectionPages;
use App\Http\Requests\Bo\UpdateMiniCrudRequest;
use App\Traits\Requests\HasPicture;
use Illuminate\Support\Facades\Gate;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;
class UpdateSectionPageRequest extends UpdateMiniCrudRequest
{
use HasPicture;
protected $modelName = 'sectionPage';
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return Gate::check('update', $this->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, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|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)
);
}
}

View file

@ -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,

View file

@ -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;

55
app/Models/Block.php Normal file
View file

@ -0,0 +1,55 @@
<?php
namespace App\Models;
use App\Enums\BlockTypeEnum;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* Represents a content block belonging to a page section.
*
* @property int $id Primary key.
* @property int $section_id Foreign key referencing the parent section.
* @property BlockTypeEnum $type The type of the block.
* @property object|null $data JSON data associated with the block.
* @property int $order Display order within the section.
* @property \Illuminate\Support\Carbon $created_at Timestamp when the block was created.
* @property \Illuminate\Support\Carbon $updated_at Timestamp when the block was last updated.
*
* @property-read SectionPage $section The parent section this block belongs to.
*/
class Block extends Model
{
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'section_id',
'type',
'data',
'order',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'type' => BlockTypeEnum::class,
'data' => 'object',
];
/**
* Get the section that owns this block.
*
* @return BelongsTo<SectionPage, Block>
*/
public function section(): BelongsTo
{
return $this->belongsTo(SectionPage::class);
}
}

62
app/Models/Picto.php Normal file
View file

@ -0,0 +1,62 @@
<?php
namespace App\Models;
use App\Lib\Helpers\FileStorageHelper;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Picto extends Model
{
const MIN_PICTOS = 4;
const MAX_PICTOS = 6;
const MIN_WIDTH = 60;
const MIN_HEIGHT = 60;
protected $fillable = [
'title',
'description',
'picture_alt',
'picture_title',
'picture',
'picture_width',
'picture_height',
'section_id',
'order'
];
public function section(): BelongsTo
{
return $this->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');
});
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class SectionPage extends Model
{
protected $table = 'section_pages';
protected $fillable = [
'page_id',
'name',
'visibility',
'data',
];
protected $casts = [
'visibility' => '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');
}
}

View file

@ -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');
}
}

View file

@ -0,0 +1,70 @@
<?php
namespace App\Policies;
use App\Enums\Users\RoleEnum;
use App\Models\Block;
use App\Models\Customer;
use App\Models\User;
use App\Policies\Rules\UserStaticRules;
class BlockPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $authUser): bool
{
return UserStaticRules::atLeastRole($authUser, RoleEnum::admin);
}
/**
* Determine whether the user can view the model.
*/
public function view(Customer $customer, Block $blockPage): bool
{
return true;
}
/**
* Determine whether the user can create models.
*/
public function create(Customer $customer): bool
{
return false;
}
/**
* Determine whether the user can update the model.
*/
public function update(User $authUser, Block $blockPage): bool
{
// phpcs:enable
return UserStaticRules::atLeastRole($authUser, RoleEnum::admin);
}
/**
* Determine whether the user can delete the model.
*/
public function delete(Customer $customer, Block $blockPage): bool
{
return false;
}
/**
* Determine whether the user can restore the model.
*/
public function restore(Customer $customer, Block $blockPage): bool
{
return false;
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(Customer $customer, Block $blockPage): bool
{
return false;
}
}

View file

@ -0,0 +1,98 @@
<?php
namespace App\Policies;
use App\Enums\Users\RoleEnum;
use App\Models\Picto;
use App\Models\User;
use App\Policies\Rules\UserStaticRules;
class PictoPolicy
{
/**
* Determine whether the user can view any models.
*
* @return boolean
*/
public function viewAny(): bool
{
return UserStaticRules::everyOne();
}
/**
* Determine whether the user can view the model.
*
* @param \App\Models\User $authUser
* @return boolean
*/
public function view(User $authUser): bool
{
return UserStaticRules::atLeastRole($authUser, RoleEnum::admin);
}
/**
* Determine whether the user can create a new model.
*
* @param \App\Models\User $authUser
* @return boolean
*/
public function create(User $authUser): bool
{
return UserStaticRules::atLeastRole($authUser, RoleEnum::admin);
}
/**
* Determine whether the user can update the model.
*
* @param \App\Models\User $authUser
* @return boolean
*/
public function update(User $authUser): bool
{
return UserStaticRules::atLeastRole($authUser, RoleEnum::admin);
}
/**
* Determine whether the user can delete the model.
*
* @param \App\Models\User $authUser
* @return boolean
*/
public function delete(User $authUser, Picto $picto): bool
{
return UserStaticRules::atLeastRole($authUser, RoleEnum::admin);
}
/**
* Determine whether the user can duplicate the model.
*
* @param \App\Models\User $authUser
* @return boolean
*/
public function duplicate(User $authUser): bool
{
return UserStaticRules::atLeastRole($authUser, RoleEnum::admin);
}
/**
* Determine whether the user can publish/unpublish the model.
*
* @param \App\Models\User $authUser
* @return boolean
*/
public function changePublished(User $authUser): bool
{
return UserStaticRules::atLeastRole($authUser, RoleEnum::admin);
}
/**
* Determine whether the user can change the order of the model.
*
* @param \App\Models\User $authUser
* @return boolean
*/
public function changeOrder(User $authUser): bool
{
return UserStaticRules::atLeastRole($authUser, RoleEnum::admin);
}
}

View file

@ -0,0 +1,53 @@
<?php
namespace App\Policies;
use App\Enums\Users\RoleEnum;
use App\Models\Customer;
use App\Models\SectionPage;
use App\Models\User;
use App\Policies\Rules\UserStaticRules;
class SectionPagePolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $authUser): bool
{
return UserStaticRules::atLeastRole($authUser, RoleEnum::admin);
}
/**
* Determine whether the user can update the model.
*/
public function update(User $authUser, SectionPage $sectionPage): bool
{
// phpcs:enable
return UserStaticRules::atLeastRole($authUser, RoleEnum::admin);
}
/**
* Determine whether the user can delete the model.
*/
public function delete(Customer $customer, SectionPage $sectionPage): bool
{
return false;
}
/**
* Determine whether the user can restore the model.
*/
public function restore(Customer $customer, SectionPage $sectionPage): bool
{
return false;
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(Customer $customer, SectionPage $sectionPage): bool
{
return false;
}
}

View file

@ -137,6 +137,7 @@ class ViewServiceProvider extends ServiceProvider
['name' => '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;

View file

@ -0,0 +1,49 @@
<?php
namespace App\Services\Picto;
use App\Models\Picto;
use App\Models\SectionPage;
class PictoService
{
/**
* Create a new picto for the given section.
*
* Returns the created Picto on success, or an error message string
* when the section has reached its maximum number of pictos.
*
* @param array $data
* @return \App\Models\Picto|string
*/
public function store(array $data): Picto|string
{
$section = SectionPage::findOrFail($data['section_id']);
$sectionData = json_decode(json_encode($section->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;
}
}

View file

@ -0,0 +1,95 @@
<?php
namespace App\Services\StaticPage;
use App\Enums\Block\BlockContentTypeEnum;
use App\Enums\BlockTypeEnum;
use App\Models\Block;
class BlockService
{
/**
* Get the data object of a given block.
*
* @param Block $block The block to retrieve data from.
* @return object
*/
public function getBlockData(Block $block): object
{
return $block->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;
}
}

View file

@ -0,0 +1,144 @@
<?php
namespace App\Services\StaticPage;
use App\Lib\Helpers\FileStorageHelper;
use App\Models\SectionPage;
use App\Models\StaticPage;
use Illuminate\Database\Eloquent\Collection;
class SectionPageService
{
public function getByPage(StaticPage $page, bool $onlyVisible = false): Collection
{
$query = $page->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();
}
}

View file

@ -0,0 +1,83 @@
<?php
use App\Enums\InputType;
use App\Enums\ReductionEmplacementType;
use App\Enums\ReductionRedirectionType;
use App\Enums\StaticPageType;
use App\Lib\Helpers\FileStorageHelper;
use App\Models\StaticPage;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
$page = new StaticPage();
$picture = FileStorageHelper::storeFile(
$page,
new \SplFileInfo(\resource_path('assets/images/commitments/commitment-5/banner.jpg'))
);
$picto = FileStorageHelper::storeFile(
$page,
new \SplFileInfo(\resource_path('assets/images/commitments/navigation/picto-life.svg'))
);
$data = [
'picto_banner' => [
'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' => '<p>Face aux enjeux de notre époque, Le Gaulois répond présent.<br>Chaque jour, nous avançons pas à pas avec <strong>les équipes, les partenaires et les consommateurs</strong> 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 <strong>minimiser l’impact de notre activité sur l’environnement, veiller au bien-être animal et améliorer notre alimentation et notre qualité de vie</strong>.</p>'
]
];
$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' => '<p>Le Gaulois s\'engage à réduire son empreinte carbone : optimiser le transport, privilégier des fournisseurs locaux et réduire les emballages.</p>',
'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();
});
}
};

View file

@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('section_pages', function (Blueprint $table) {
$table->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');
}
};

View file

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('blocks', function (Blueprint $table) {
$table->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');
}
};

View file

@ -0,0 +1,290 @@
<?php
use App\Enums\BlockTypeEnum;
use App\Enums\InputType;
use App\Enums\StaticPageType;
use App\Lib\Helpers\FileStorageHelper;
use App\Models\Block;
use App\Models\StaticPage;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration
{
private function getSections(): array
{
return [
[
'name' => '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' => '<p>Chaque initiative joue un rôle crucial dans la <strong>lutte contre les gaz à effet de serre</strong>, 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 <strong>réchauffement climatique</strong>.</p>',
'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' => '<p>Chez Le Gaulois, pour assurer <strong>l\'apport protéique</strong> et assurer un <strong>équilibre nutritionnel de nos volailles</strong>, nous les alimentons avec du <strong>soja</strong>. C’est la <strong>légumineuse la plus riche en protéines et en acides aminés essentiels !</strong> Conscient que la culture du Soja importée est assujettie à de nombreux questionnements, notamment son impact sur la <strong>déforestation.</strong></p>',
'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' => ' <h6>garanti zéro déforestation</h6><p>conversion au plus tard d\'ici 2025.</p>',
'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' => '<p> 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. <br> &nbsp; </p><ul><li> soit intégrant de la matière recyclée </li><li> soit en étant recyclables </li></ul><p> D’ici fin 2025, nous ambitionnons de passer à près de <strong>95% d’emballages responsables.</strong></p>',
'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' => '<hgroup><strong>Le saviez-vous ?</strong> 100% des emballages cartons Le Gaulois sont déjà issus de <strong>cartons recyclés et certifiés FSC</strong>, garantissant une gestion responsable des forêts, et <strong>plus de 60% des barquettes</strong> de volaille Le Gaulois sont recyclables ou intégrant <strong>au moins 50% de matière recyclée.</strong></hgroup>',
'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();
}
}
}
};

View file

@ -0,0 +1,37 @@
<?php
use App\Models\Picto;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('pictos', function (Blueprint $table) {
$table->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');
}
};

View file

@ -0,0 +1,162 @@
<?php
use App\Enums\StaticPageType;
use App\Lib\Helpers\FileStorageHelper;
use App\Models\Picto;
use App\Models\StaticPage;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration
{
private function getPictos(): array
{
return [
'section 2' => [
[
'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();
}
}
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -12,6 +12,7 @@ return [
'edit' => 'mettre à jour',
'view' => 'aperçu',
'order_submodels' => 'ordonnancement des sous-élements',
'back' => 'retour'
],
'helpers' => [

View file

@ -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'
]
];

View file

@ -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'
],
];

View file

@ -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<Element>;
const elements = document.getElementsByClassName("confirmDeleteTS") as HTMLCollectionOf<Element>;
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<HTMLButtonElement>('button[type="submit"]');
submitBtn?.addEventListener("click", function (e) {
e.preventDefault();
e.stopPropagation();
const form = (e.target as HTMLElement).closest<HTMLFormElement>('.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", () => {
});
}
});

View file

@ -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";

View file

@ -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");

View file

@ -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<HTMLAnchorElement>(`.btn-create-picto[data-section="${sectionId}"]`);
}
// PICTO CREATE
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll<HTMLAnchorElement>('.btn-create-picto').forEach(btnCreate => {
const sectionId = btnCreate.dataset.section;
const pictoList = document.querySelector<HTMLDivElement>(`.picto-list[data-section="${sectionId}"]`);
const pictoForm = document.querySelector<HTMLDivElement>(`.picto-form-wrapper[data-section="${sectionId}"]`);
const btnBack = pictoForm?.querySelector<HTMLButtonElement>('.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<HTMLAnchorElement>('.btn-edit-picto');
if (!btn) return;
e.preventDefault();
const pictoId = btn.dataset.picto;
const sectionId = btn.dataset.section;
const wrapper = document.querySelector<HTMLDivElement>(`.picto-form-wrapper[data-section="${sectionId}"]`);
const list = document.querySelector<HTMLDivElement>(`.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<string>();
document.addEventListener('submit', async (e) => {
const form = (e.target as HTMLElement).closest<HTMLFormElement>('[data-change-order]');
if (!form) return;
e.preventDefault();
const sectionId = form.closest<HTMLDivElement>('.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<HTMLElement>('[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<HTMLElement>(`.picto-list[data-section="${sectionId}"] .flash-container`);
if (flashContainer) {
flashContainer.innerHTML = `
<div class="alert alert-success alert-dismissible fade show m-0 mb-3" role="alert">
<i class="fa-solid fa-circle-check"></i>
<strong>${data.message ?? 'Enregistré avec succès.'}</strong>
<button class="btn-close" data-bs-dismiss="alert" type="button" aria-label="Fermer"></button>
</div>`;
setTimeout(() => { flashContainer.innerHTML = ''; }, 5000);
}
await new Promise(resolve => setTimeout(resolve, 800));
const freshContainer = document.querySelector<HTMLDivElement>(`.picto-list[data-section="${sectionId}"]`);
if (!freshContainer) return;
freshContainer.querySelectorAll<HTMLElement>('[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<HTMLDivElement>('.picto-table-wrapper');
if (!tableWrapper) return;
tableWrapper.innerHTML = await res.text();
freshContainer.querySelectorAll<HTMLElement>('[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<HTMLElement>(`.picto-list[data-section="${sectionId}"] .flash-container`);
if (flashContainer) {
flashContainer.innerHTML = `
<div class="alert alert-danger alert-dismissible fade show m-0 mb-3" role="alert">
<i class="fa-solid fa-circle-xmark"></i>
<strong>${e.message ?? 'Une erreur est survenue.'}</strong>
<button class="btn-close" data-bs-dismiss="alert" type="button" aria-label="Fermer"></button>
</div>`;
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<HTMLAnchorElement>('#pictos-table thead a');
if (!link) return;
e.preventDefault();
const sectionId = link.closest<HTMLDivElement>('.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<HTMLDivElement>(`.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<HTMLElement>('[data-bs-toggle="tooltip"]').forEach(el => {
bootstrap.Tooltip.getInstance(el)?.dispose();
});
const tableWrapper = freshContainer.querySelector<HTMLDivElement>('.picto-table-wrapper');
if (!tableWrapper) return;
tableWrapper.innerHTML = await res.text();
// Afficher le bouton reset si un tri est actif
const resetBtn = freshContainer.querySelector<HTMLDivElement>('.picto-sort-reset');
if (resetBtn && sortCol) resetBtn.classList.remove('d-none');
freshContainer.querySelectorAll<HTMLElement>('[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<HTMLAnchorElement>('.picto-sort-reset a');
if (!resetLink) return;
e.preventDefault();
const sectionId = resetLink.closest<HTMLDivElement>('.picto-list')?.dataset.section;
if (!sectionId) return;
const freshContainer = document.querySelector<HTMLDivElement>(`.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<HTMLElement>('[data-bs-toggle="tooltip"]').forEach(el => {
bootstrap.Tooltip.getInstance(el)?.dispose();
});
const tableWrapper = freshContainer.querySelector<HTMLDivElement>('.picto-table-wrapper');
if (!tableWrapper) return;
tableWrapper.innerHTML = await res.text();
freshContainer.querySelector('.picto-sort-reset')?.classList.add('d-none');
freshContainer.querySelectorAll<HTMLElement>('[data-bs-toggle="tooltip"]').forEach(el => {
const tt = new bootstrap.Tooltip(el);
el.addEventListener('hidden.bs.tooltip', () => tt.dispose(), { once: true });
});
}
});

View file

@ -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<HTMLElement>('.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<HTMLImageElement>(`#${targetId}-preview`);
if (preview && width && height) {
preview.style.width = `${width}px`;
preview.style.height = `${height}px`;
}
};
saveBtn.addEventListener('click', async (e: MouseEvent): Promise<void> => {
e.preventDefault();
const width = widthInput?.value ?? '';
const height = heightInput?.value ?? '';
const csrf = document.querySelector<HTMLMetaElement>('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<HTMLInputElement>(
`#${targetId} input[name="picture"]`
);
pictureInput?.addEventListener('change', (): void => {
applyDimensions(widthInput?.value ?? '', heightInput?.value ?? '');
});
}

View file

@ -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<string, string[]>) {
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 = `
<div class="alert alert-success alert-dismissible fade show m-0 mb-3" role="alert">
<i class="fa-solid fa-circle-check"></i>
<strong>${message}</strong>
<button class="btn-close" data-bs-dismiss="alert" type="button" aria-label="Fermer"></button>
</div>`;
container.scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(() => container.innerHTML = '', 5000);
}
function showError(form: HTMLFormElement, message: string) {
const container = getFlashContainer(form);
container.innerHTML = `
<div class="alert alert-danger alert-dismissible fade show m-0 mb-3" role="alert">
<i class="fa-solid fa-circle-xmark"></i>
<strong>${message}</strong>
<button class="btn-close" data-bs-dismiss="alert" type="button" aria-label="Fermer"></button>
</div>`;
container.scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(() => container.innerHTML = '', 5000);
}
export function showFlashGlobal(message: string, type: 'success' | 'danger' = 'success') {
let container = document.querySelector<HTMLElement>('.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 = `
<div class="alert alert-${type} alert-dismissible fade show m-0 mb-3" role="alert">
<i class="fa-solid ${icon}"></i>
<strong>${message}</strong>
<button class="btn-close" data-bs-dismiss="alert" type="button" aria-label="Fermer"></button>
</div>`;
container.scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(() => { if (container) container.innerHTML = ''; }, 5000);
}
export async function handleSubmit(form: HTMLFormElement, action: (form: HTMLFormElement) => Promise<any>) {
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<HTMLButtonElement>('.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<string, string> = {
'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<HTMLFormElement>('.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<HTMLFormElement>('.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<HTMLFormElement>('.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<HTMLDivElement>(`.picto-list[data-section="${sectionId}"]`);
const wrapper = document.querySelector<HTMLDivElement>(`.picto-form-wrapper[data-section="${sectionId}"]`);
const btnCreate = document.querySelector<HTMLElement>(`.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();
});

View file

@ -0,0 +1,27 @@
// for show or hide fields (block reduction)
document.querySelectorAll<HTMLSelectElement>('[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<HTMLInputElement>('input, textarea, select').forEach(el => {
el.disabled = isActu;
});
actDiv!.querySelectorAll<HTMLInputElement>('input, textarea, select').forEach(el => {
el.disabled = isCta;
});
}
sel.addEventListener('change', toggle);
toggle();
});

View file

@ -0,0 +1,88 @@
type ToggleResponse = {
data: {
visibility: boolean;
};
};
const csrfToken = document
.querySelector<HTMLMetaElement>('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}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
container.prepend(alert);
setTimeout(() => alert.remove(), 3000);
}
const toggles = document.querySelectorAll<HTMLInputElement>('.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<HTMLElement>(targetSelector)
: null;
if (target) {
target.style.display = this.checked ? '' : 'none';
}
this.disabled = true;
const flashContainer = document.querySelector<HTMLElement>(`#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);
}
}
});
});

View file

@ -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<Element>) => {
app.config.globalProperties.window = window;
app.config.globalProperties.document = document;
app.component("FontAwesomeIcon", FontAwesomeIcon);
};
const setupApp = (app: App<Element>) => {
app.config.globalProperties.window = window;
app.config.globalProperties.document = document;
app.component("FontAwesomeIcon", FontAwesomeIcon);
};
// Register components
/// <reference types="vite/client" />
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();
});

View file

@ -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
<nav class="col-lg-4 col-xl-3 col-xxxl-2 d-lg-block bg-body-tertiary sidebar collapse overflow-x-visible" id="menuNavigation">
<nav class="col-lg-4 col-xl-3 col-xxxl-2 d-lg-block bg-body-tertiary sidebar collapse overflow-x-visible"
id="menuNavigation">
<div class="sidemenu py-2">
{{-- HOME --}}
<a data-bs-toggle="tooltip" href="{{ route('bo.dashboard') }}" title="{{ __('Accéder à la page d\'accueil du back-office') }}"
role="button" @class([
<a data-bs-toggle="tooltip" href="{{ route('bo.dashboard') }}"
title="{{ __('Accéder à la page d\'accueil du back-office') }}" role="button" @class([
'btn btn-primary w-100 text-start',
'fw-bold active' => request()->routeIs('bo.dashboard'),
])>
@ -67,7 +68,8 @@
</li>
{{-- MAIN MENU --}}
<li>
<a data-bs-toggle="tooltip" href="{{ route('bo.general_settings.edit', $mainMenuSettingModel) }}"
<a data-bs-toggle="tooltip"
href="{{ route('bo.general_settings.edit', $mainMenuSettingModel) }}"
title="{{ __('crud.helpers.update_model', ['model' => $mainMenuSettingModel->type->label()]) }}"
@class([
'dropdown-item px-3 py-2',
@ -96,7 +98,8 @@
</li>
{{-- HOT NEWS --}}
<li>
<a data-bs-toggle="tooltip" href="{{ route('bo.general_settings.edit', $hotNewsSettingModel) }}"
<a data-bs-toggle="tooltip"
href="{{ route('bo.general_settings.edit', $hotNewsSettingModel) }}"
title="{{ __('crud.helpers.update_model', ['model' => $hotNewsSettingModel->type->label()]) }}"
@class([
'dropdown-item px-3 py-2',
@ -297,7 +300,8 @@
<ul class="dropdown-menu">
{{-- RECRUITMENT PAGE --}}
<li>
<a data-bs-toggle="tooltip" href="{{ route('bo.static_pages.edit', $recruitmentStaticPageModel) }}"
<a data-bs-toggle="tooltip"
href="{{ route('bo.static_pages.edit', $recruitmentStaticPageModel) }}"
title="{{ __('crud.helpers.update_model', ['model' => $recruitmentStaticPageModel->type->label()]) }}"
@class([
'dropdown-item px-3 py-2',
@ -478,8 +482,8 @@
{{-- RECIPE PAGE --}}
@if (isset($recipeStaticPageModel))
<a data-bs-toggle="tooltip" href="{{ route('bo.static_pages.edit', $recipeStaticPageModel) }}"
title="{{ __('crud.helpers.update_model', ['model' => $recipeStaticPageModel->type->label()]) }}" role="button"
@class([
title="{{ __('crud.helpers.update_model', ['model' => $recipeStaticPageModel->type->label()]) }}"
role="button" @class([
'btn btn-primary w-100 text-start mt-1',
'fw-bold active' =>
url()->current() ===
@ -528,7 +532,8 @@
<ul class="dropdown-menu">
{{-- PRODUCTS PAGE --}}
<li>
<a data-bs-toggle="tooltip" href="{{ route('bo.static_pages.edit', $productsStaticPageModel) }}"
<a data-bs-toggle="tooltip"
href="{{ route('bo.static_pages.edit', $productsStaticPageModel) }}"
title="{{ __('crud.helpers.update_model', ['model' => $productsStaticPageModel->type->label()]) }}"
@class([
'dropdown-item px-3 py-2',
@ -566,7 +571,8 @@
{{-- RANGES --}}
<li>
<a data-bs-toggle="tooltip" href="{{ route('bo.ranges.index') }}"
title="{{ str(__('crud.actions.order_submodels'))->ucFirst() }}" @class([
title="{{ str(__('crud.actions.order_submodels'))->ucFirst() }}"
@class([
'dropdown-item px-3 py-2',
'fw-bold text-bg-secondary' =>
request()->routeIs('bo.ranges.*') &&
@ -599,7 +605,8 @@
{{-- ORDER RANGES/SUBRANGES --}}
@if ($firstRange && $firstRange->subranges->count() >= 2)
<li>
<a data-bs-toggle="tooltip" href="{{ route('bo.ranges.edit-subranges-order', $firstRange) }}"
<a data-bs-toggle="tooltip"
href="{{ route('bo.ranges.edit-subranges-order', $firstRange) }}"
title="{{ __('crud.helpers.update_model', ['model' => __('models.classes.subrange')]) }}"
@class([
'dropdown-item px-3 py-2',
@ -654,8 +661,10 @@
{{-- ORDER MEATS/SUBMEATS --}}
@if ($firstMeat && $firstMeat->submeats->count() >= 2)
<li>
<a data-bs-toggle="tooltip" href="{{ route('bo.meats.edit-submeats-order', $firstMeat) }}"
title="{{ str(__('crud.actions.order_submodels'))->ucFirst() }}" @class([
<a data-bs-toggle="tooltip"
href="{{ route('bo.meats.edit-submeats-order', $firstMeat) }}"
title="{{ str(__('crud.actions.order_submodels'))->ucFirst() }}"
@class([
'dropdown-item px-3 py-2',
'fw-bold text-bg-secondary' => request()->routeIs(
'bo.meats.edit-submeats-order'),
@ -705,7 +714,8 @@
<ul class="dropdown-menu">
{{-- PRODUCT PAGE --}}
<li>
<a data-bs-toggle="tooltip" href="{{ route('bo.static_pages.edit', $productStaticPageModel) }}"
<a data-bs-toggle="tooltip"
href="{{ route('bo.static_pages.edit', $productStaticPageModel) }}"
title="{{ __('crud.helpers.update_model', ['model' => $productStaticPageModel->type->label()]) }}"
@class([
'dropdown-item px-3 py-2',
@ -821,10 +831,11 @@
</ul>
</div>
{{-- BREEDING --}}
@if(isset($breedingPoultryStaticPageModel) || isset($breedingJobStaticPageModel) ||
isset($breedingFoodStaticPageModel) || isset($breedingHealthStaticPageModel) ||
isset($ourFarmsStaticPageModel)
)
@if (isset($breedingPoultryStaticPageModel) ||
isset($breedingJobStaticPageModel) ||
isset($breedingFoodStaticPageModel) ||
isset($breedingHealthStaticPageModel) ||
isset($ourFarmsStaticPageModel))
<div class="dropdown">
<button data-bs-toggle="dropdown" type="button" aria-expanded="false" @class([
'btn btn-primary text-start w-100 dropdown-toggle mt-1',
@ -875,8 +886,8 @@
$breedingHealthStaticPageModel->type->value()) ||
url()->current() ===
route(
'bo.static_pages.edit',
$ourFarmsStaticPageModel->type->value()) ||
'bo.static_pages.edit',
$ourFarmsStaticPageModel->type->value()) ||
// FIXME: uncomment when breeder map feature is ready
// || url()->current() === route('bo.general_settings.edit', \App\Enums\Settings\SettingsType::breederMap->value())
request()->routeIs('bo.news_breedings.*') ||
@ -948,7 +959,8 @@
title="{{ __('crud.helpers.update_model', ['model' => trans_choice('validation.custom.breeding_section_card', \INF)]) }}"
@class([
'dropdown-item px-3 py-2',
'fw-bold text-bg-secondary' => request()->routeIs('bo.static_page.sections*'),
'fw-bold text-bg-secondary' => request()->routeIs(
'bo.static_page.sections*'),
])>
<i @class([
'fa-solid fa-tag fa-fw',
@ -961,7 +973,7 @@
</div>
@endif
{{-- ACTUALITES --}}
@if(isset($newsStaticPageModel))
@if (isset($newsStaticPageModel))
<div class="dropdown">
<button data-bs-toggle="dropdown" type="button" aria-expanded="false" @class([
'btn btn-primary text-start w-100 dropdown-toggle mt-1',
@ -988,12 +1000,13 @@
<ul class="dropdown-menu">
{{-- ACTUS PAGE STATIC --}}
<li>
<a href="{{ route('bo.static_pages.edit', $newsStaticPageModel) }}" @class([
'dropdown-item px-3 py-2',
'fw-bold text-bg-secondary' =>
url()->current() ===
route('bo.static_pages.edit', $newsStaticPageModel->type->value()),
])>
<a href="{{ route('bo.static_pages.edit', $newsStaticPageModel) }}"
@class([
'dropdown-item px-3 py-2',
'fw-bold text-bg-secondary' =>
url()->current() ===
route('bo.static_pages.edit', $newsStaticPageModel->type->value()),
])>
<i @class([
'fa-solid fa-file fa-fw',
'fa-lg' =>
@ -1035,8 +1048,8 @@
{{-- RECIPE PAGE --}}
@if (isset($searchStaticPageModel))
<a data-bs-toggle="tooltip" href="{{ route('bo.static_pages.edit', $searchStaticPageModel) }}"
title="{{ __('crud.helpers.update_model', ['model' => $searchStaticPageModel->type->label()]) }}" role="button"
@class([
title="{{ __('crud.helpers.update_model', ['model' => $searchStaticPageModel->type->label()]) }}"
role="button" @class([
'btn btn-primary w-100 text-start mt-1',
'fw-bold active' =>
url()->current() ===
@ -1164,13 +1177,11 @@
<div class="dropdown">
<button data-bs-toggle="dropdown" type="button" aria-expanded="false" @class([
'btn btn-primary text-start w-100 dropdown-toggle mt-1',
'fw active' =>
request()->routeIs('bo.approaches.*'),
'fw active' => request()->routeIs('bo.approaches.*'),
])>
<i @class([
'fa-solid fa-people-arrows fa-fw',
'fa-lg' =>
request()->routeIs('bo.approaches.*'),
'fa-lg' => request()->routeIs('bo.approaches.*'),
])></i>
{{ __('Démarches') }}
</button>
@ -1178,12 +1189,13 @@
{{-- PAGE SORTIR-A-L-EXTERIEUR --}}
@if (isset($exitOutsideStaticPageModel))
<li>
<a href="{{ route('bo.static_pages.edit', $exitOutsideStaticPageModel) }}" @class([
'dropdown-item px-3 py-2',
'fw-bold text-bg-secondary' =>
url()->current() ===
route('bo.static_pages.edit', $exitOutsideStaticPageModel),
])>
<a href="{{ route('bo.static_pages.edit', $exitOutsideStaticPageModel) }}"
@class([
'dropdown-item px-3 py-2',
'fw-bold text-bg-secondary' =>
url()->current() ===
route('bo.static_pages.edit', $exitOutsideStaticPageModel),
])>
<i @class([
'fa-solid fa-file fa-fw',
'fa-lg' =>
@ -1197,13 +1209,13 @@
{{-- OUI-C-BON --}}
@if (isset($ouiCestBonStaticPageModel))
<li>
<a href="{{ route('bo.static_pages.edit', $ouiCestBonStaticPageModel ) }}" @class([
'dropdown-item px-3 py-2',
'fw-bold text-bg-secondary' =>
url()->current() ===
route('bo.static_pages.edit', $ouiCestBonStaticPageModel),
])>
<a href="{{ route('bo.static_pages.edit', $ouiCestBonStaticPageModel) }}"
@class([
'dropdown-item px-3 py-2',
'fw-bold text-bg-secondary' =>
url()->current() ===
route('bo.static_pages.edit', $ouiCestBonStaticPageModel),
])>
<i @class([
'fa-solid fa-file fa-fw fa-fw',
'fa-lg' =>
@ -1219,7 +1231,8 @@
<li>
<a href="{{ route('bo.sections.approaches.index') }}" @class([
'dropdown-item px-3 py-2',
'fw-bold text-bg-secondary' => request()->routeIs('bo.sections.approaches.*'),
'fw-bold text-bg-secondary' => request()->routeIs(
'bo.sections.approaches.*'),
])>
<i @class([
'fa-solid fa-tags fa-fw',
@ -1231,7 +1244,8 @@
<li>
<a href="{{ route('bo.categories.approaches.index') }}" @class([
'dropdown-item px-3 py-2',
'fw-bold text-bg-secondary' => request()->routeIs('bo.categories.approaches.*'),
'fw-bold text-bg-secondary' => request()->routeIs(
'bo.categories.approaches.*'),
])>
<i @class([
'fa-solid fa-tags fa-fw',
@ -1242,11 +1256,65 @@
</li>
</ul>
</div>
{{-- COMMITMENTS --}}
<div class="dropdown">
<button data-bs-toggle="dropdown" type="button" aria-expanded="false" @class([
'btn btn-primary text-start w-100 dropdown-toggle mt-1',
'fw active' => request()->routeIs('bo.static_pages.*') ||
request()->routeIs('bo.static_page.commitmments.sections.*'),
])>
<i @class([
'fa-solid fa-handshake fa-fw',
'fa-lg' => request()->routeIs('bo.static_pages.*'),
])></i>
{{ __('Nos engagement') }}
</button>
<ul class="dropdown-menu">
{{-- PAGE RESPECTER LA TERRE --}}
@if (isset($respecttheEarthStaticPageModel))
<li>
<a href="{{ route('bo.static_pages.edit', $respecttheEarthStaticPageModel) }}"
@class([
'dropdown-item px-3 py-2',
'fw-bold text-bg-secondary' =>
url()->current() ===
route('bo.static_pages.edit', $respecttheEarthStaticPageModel),
])>
<i @class([
'fa-solid fa-file fa-fw',
'fa-lg' =>
url()->current() ===
route('bo.static_pages.edit', $respecttheEarthStaticPageModel),
])></i>
{{ str(__('Page Respecter la terre'))->ucFirst() }}
</a>
</li>
@endif
<li>
<hr class="dropdown-divider">
</li>
<li>
<a data-bs-toggle="tooltip" href="{{ route('bo.static_page.commitmments.sections.edit') }}"
title="{{ __('crud.helpers.update_model', ['model' => trans_choice('validation.custom.common_section_part', \INF)]) }}"
@class([
'dropdown-item px-3 py-2',
'fw-bold text-bg-secondary' => request()->routeIs(
'bo.static_page.*'),
])>
<i @class([
'fa-solid fa-tag fa-fw',
'fa-lg' => request()->routeIs('bo.static_page.*'),
])></i>
{{ str(trans_choice('validation.custom.common_section_part', \INF))->ucFirst() }}
</a>
</li>
</ul>
</div>
{{-- TDF PAGE --}}
@if (isset($tdfStaticPageModel))
<a data-bs-toggle="tooltip" href="{{ route('bo.static_pages.edit', $tdfStaticPageModel) }}"
title="{{ __('crud.helpers.update_model', ['model' => $tdfStaticPageModel->type->label()]) }}" role="button"
@class([
title="{{ __('crud.helpers.update_model', ['model' => $tdfStaticPageModel->type->label()]) }}"
role="button" @class([
'btn btn-primary w-100 text-start mt-1',
'fw-bold active' =>
url()->current() ===
@ -1263,10 +1331,10 @@
@endif
<hr>
{{-- CONTACT --}}
@if(isset($contactStaticPageModel))
@if (isset($contactStaticPageModel))
<a data-bs-toggle="tooltip" href="{{ route('bo.static_pages.edit', $contactStaticPageModel) }}"
title="{{ __('crud.helpers.update_model', ['model' => $contactStaticPageModel->type->label()]) }}" role="button"
@class([
title="{{ __('crud.helpers.update_model', ['model' => $contactStaticPageModel->type->label()]) }}"
role="button" @class([
'btn btn-primary w-100 text-start',
'fw-bold active' =>
url()->current() ===
@ -1282,10 +1350,10 @@
</a>
@endif
{{-- LEGALS CGU --}}
@if(isset($legalsStaticPageModel))
@if (isset($legalsStaticPageModel))
<a data-bs-toggle="tooltip" href="{{ route('bo.static_pages.edit', $legalsStaticPageModel) }}"
title="{{ __('crud.helpers.update_model', ['model' => $legalsStaticPageModel->type->label()]) }}" role="button"
@class([
title="{{ __('crud.helpers.update_model', ['model' => $legalsStaticPageModel->type->label()]) }}"
role="button" @class([
'btn btn-primary w-100 text-start mt-1',
'fw-bold active' =>
url()->current() ===
@ -1301,10 +1369,10 @@
</a>
@endif
{{-- CONFIDENTIALITY --}}
@if(isset($confidentialityStaticPageModel))
@if (isset($confidentialityStaticPageModel))
<a data-bs-toggle="tooltip" href="{{ route('bo.static_pages.edit', $confidentialityStaticPageModel) }}"
title="{{ __('crud.helpers.update_model', ['model' => $confidentialityStaticPageModel->type->label()]) }}" role="button"
@class([
title="{{ __('crud.helpers.update_model', ['model' => $confidentialityStaticPageModel->type->label()]) }}"
role="button" @class([
'btn btn-primary w-100 text-start mt-1',
'fw-bold active' =>
url()->current() ===
@ -1324,10 +1392,10 @@
</a>
@endif
{{-- TERMS AND CONDITIONS --}}
@if(isset($sellingStaticPageModel))
@if (isset($sellingStaticPageModel))
<a data-bs-toggle="tooltip" href="{{ route('bo.static_pages.edit', $sellingStaticPageModel) }}"
title="{{ __('crud.helpers.update_model', ['model' => $sellingStaticPageModel->type->label()]) }}" role="button"
@class([
title="{{ __('crud.helpers.update_model', ['model' => $sellingStaticPageModel->type->label()]) }}"
role="button" @class([
'btn btn-primary w-100 text-start mt-1',
'fw-bold active' =>
url()->current() ===
@ -1343,10 +1411,10 @@
</a>
@endif
{{-- ACCESSIBILITY --}}
@if(isset($accessibilityStaticPageModel))
@if (isset($accessibilityStaticPageModel))
<a data-bs-toggle="tooltip" href="{{ route('bo.static_pages.edit', $accessibilityStaticPageModel) }}"
title="{{ __('crud.helpers.update_model', ['model' => $accessibilityStaticPageModel->type->label()]) }}" role="button"
@class([
title="{{ __('crud.helpers.update_model', ['model' => $accessibilityStaticPageModel->type->label()]) }}"
role="button" @class([
'btn btn-primary w-100 text-start mt-1',
'fw-bold active' =>
url()->current() ===
@ -1368,22 +1436,18 @@
{{-- SITEMAP --}}
@if (isset($sitemapStaticPageModel))
<a data-bs-toggle="tooltip" href="{{ route('bo.static_pages.edit', $sitemapStaticPageModel) }}"
title="{{ __('crud.helpers.update_model', ['model' => $sitemapStaticPageModel->type->label()]) }}" role="button"
@class([
title="{{ __('crud.helpers.update_model', ['model' => $sitemapStaticPageModel->type->label()]) }}"
role="button" @class([
'btn btn-primary w-100 text-start mt-1',
'fw-bold active' =>
url()->current() ===
route(
'bo.static_pages.edit',
$sitemapStaticPageModel->type->value()),
route('bo.static_pages.edit', $sitemapStaticPageModel->type->value()),
])>
<i @class([
'fa-solid fa-file fa-fw',
'fa-lg' =>
url()->current() ===
route(
'bo.static_pages.edit',
$sitemapStaticPageModel->type->value()),
route('bo.static_pages.edit', $sitemapStaticPageModel->type->value()),
])></i>
{{ $sitemapStaticPageModel->type->label() }}
</a>
@ -1391,22 +1455,18 @@
{{-- RULES --}}
@if (isset($rulesStaticPageModel))
<a data-bs-toggle="tooltip" href="{{ route('bo.static_pages.edit', $rulesStaticPageModel) }}"
title="{{ __('crud.helpers.update_model', ['model' => $rulesStaticPageModel->type->label()]) }}" role="button"
@class([
title="{{ __('crud.helpers.update_model', ['model' => $rulesStaticPageModel->type->label()]) }}"
role="button" @class([
'btn btn-primary w-100 text-start mt-1',
'fw-bold active' =>
url()->current() ===
route(
'bo.static_pages.edit',
$rulesStaticPageModel->type->value()),
route('bo.static_pages.edit', $rulesStaticPageModel->type->value()),
])>
<i @class([
'fa-solid fa-file fa-fw',
'fa-lg' =>
url()->current() ===
route(
'bo.static_pages.edit',
$rulesStaticPageModel->type->value()),
route('bo.static_pages.edit', $rulesStaticPageModel->type->value()),
])></i>
{{ $rulesStaticPageModel->type->label() }}
</a>

View file

@ -1,31 +1,49 @@
<td class="text-center align-middle">
@can('changeOrder', $model)
<div class="d-flex justify-content-center align-items-center">
<form @class(['invisible' => $loop->first and $models->onFirstPage()])
<form data-change-order @class([
'invisible' =>
$loop->first and
(method_exists($models, 'onFirstPage')
? $models->onFirstPage()
: $loop->first),
])
action="{{ route('bo.' . $routeName . '.change-order', [str($routeName)->singular()->value => $model, 'direction' => 'up']) }}"
method="POST">
@csrf
@method('PATCH')
<button data-bs-toggle="tooltip" type="submit" title="{{ __('crud.other.up') }}" @class([
'btn btn-sm btn-outline-secondary',
'disabled' => $loop->first and $models->onFirstPage(),
'disabled' =>
$loop->first and
(method_exists($models, 'onFirstPage')
? $models->onFirstPage()
: $loop->first),
])>
<i class="fas fa-arrow-up"></i>
</button>
</form>
<form @class([
<form data-change-order @class([
'invisible' =>
$loop->last and $models->currentPage() === $models->lastPage(),
$loop->last and
(method_exists($models, 'lastPage')
? $models->currentPage() === $models->lastPage()
: $loop->last),
])
action="{{ route('bo.' . $routeName . '.change-order', [str($routeName)->singular()->value => $model, 'direction' => 'down']) }}"
method="POST">
@csrf
@method('PATCH')
<button data-bs-toggle="tooltip" type="submit" title="{{ __('crud.other.down') }}" @class([
'btn btn-sm btn-outline-secondary ms-1',
'disabled' =>
$loop->last and $models->currentPage() === $models->lastPage(),
])>
<button data-bs-toggle="tooltip" type="submit" title="{{ __('crud.other.down') }}"
@class([
'btn btn-sm btn-outline-secondary ms-1',
'disabled' =>
$loop->last and
(method_exists($models, 'lastPage')
? $models->currentPage() === $models->lastPage()
: $loop->last),
])>
<i class="fas fa-arrow-down"></i>
</button>
</form>

View file

@ -124,7 +124,7 @@
{{-- PICTURE_ALT --}}
<div class="col-12 col-md-6 mb-3">
@include('bo.partials.inputs.varchar', [
'id' => str($input['label'])->slug() . '-alt',
'id' => ($idPrefix ?? '') . '-' . str($input['label'])->slug() . '-alt',
'label' => __('crud.minicrud.picture_alt_label'),
'hasWordCounter' => true,
'name' => 'data[' . $k . '][alt]',
@ -146,7 +146,7 @@
{{-- PICTURE_TITLE --}}
<div class="col-12 col-md-6 mb-3">
@include('bo.partials.inputs.varchar', [
'id' => str($input['label'])->slug() . '-title',
'id' => ($idPrefix ?? '') . '-' . str($input['label'])->slug() . '-title',
'label' => __('crud.minicrud.picture_title_label'),
'hasWordCounter' => true,
'name' => 'data[' . $k . '][title]',
@ -168,7 +168,7 @@
{{-- PICTURE --}}
<div class="col-12 mb-3">
@include('bo.partials.inputs.varchar-picture', [
'id' => str($input['label'])->slug(),
'id' => ($idPrefix ?? '') . '-' . str($input['label'])->slug(),
'name' => "data[{$k}][value]",
'width' => $input['dimensions']['width'],
'height' => $input['dimensions']['height'],
@ -253,7 +253,7 @@
<div class="row mb-3">
<div class="col-12">
@include('bo.partials.inputs.media-selector', [
'id' => Str::camel($k),
'id' => ($idPrefix ?? '') . Str::studly($k),
'name' => $k,
'value' => old("data.{$k}.value", $input['value'] ?? null),
'required' => isset($input['required']) ? !empty($input['required']) : true,

View file

@ -78,6 +78,12 @@
title="{{ __('crud.other.sort_delete') }}">
<i class="fa fa-eraser"></i>
</a>
@elseif (isset($ajaxSort))
<a class="picto-sort-reset btn btn-sm btn-danger" data-bs-toggle="tooltip"
href="{{ request()->fullUrlWithQuery(['sort_col' => null, 'sort_way' => null, 'rst' => true, 'search' => null]) }}"
title="{{ __('crud.other.sort_delete') }}">
<i class="fa fa-eraser"></i>
</a>
@else
<span data-bs-toggle="tooltip" title="{{ __('crud.other.sort_arrow') }}">
<i class="fas fa-question-circle"></i>

View file

@ -0,0 +1,24 @@
@php
$condition = trans_choice('models.classes.picto', 1);
@endphp
<div class="d-flex justify-content-between flex-md-nowrap align-items-center flex-wrap pb-3">
<div class="d-flex align-items-start flex-row">
</div>
</div>
<div class="flash-container"></div>
<form class="picto-form" action="{{ route('bo.pictos.store') }}" method="POST" enctype="multipart/form-data">
@csrf
<input type="hidden" name="section_id" value="{{ $section->id }}">
@include('bo.pages.pictos.form')
@include('bo.partials.script-button-clone')
<div class="row mt-3">
<div class="col text-center">
<button class="btn btn-primary" id="formSubmit" data-bs-toggle="tooltip" type="submit"
title="{{ __('crud.helpers.save_model', ['model' => trans_choice('models.classes.sectionApproach', 1)]) }}">
{{ str(__('crud.actions.save'))->ucfirst() }}
</button>
</div>
</div>
</form>

View file

@ -0,0 +1,25 @@
@php
$condition = trans_choice('models.classes.picto', 1);
@endphp
<div class="d-flex justify-content-between flex-md-nowrap align-items-center flex-wrap pb-3">
<div class="d-flex align-items-start flex-row">
</div>
</div>
<div class="flash-container"></div>
<form class="picto-form" action="{{ route('bo.pictos.update', $pictoModel) }}" method="POST" enctype="multipart/form-data" data-id="{{ $pictoModel->id }}">
@csrf
@method('PUT')
<input type="hidden" name="section_id" value="{{ $section->id }}">
@include('bo.pages.pictos.form')
@include('bo.partials.script-button-clone')
<div class="row mt-3">
<div class="col text-center">
<button class="btn btn-primary" id="formSubmit" data-bs-toggle="tooltip" type="submit"
title="{{ __('crud.helpers.save_model', ['model' => trans_choice('models.classes.sectionApproach', 1)]) }}">
{{ str(__('crud.actions.save'))->ucfirst() }}
</button>
</div>
</div>
</form>

View file

@ -0,0 +1,176 @@
@php
$inter = __('validation.custom.of_the.male');
$model = __(trans_choice('models.classes.picto', 1));
$idPrefix = $idPrefix ?? 'picto';
@endphp
<div class="row">
{{-- TITLE --}}
<div class="col-12 col-lg-6 mb-3">
<fieldset class="h-100 border p-3">
<legend>{{ str(__('validation.attributes.title'))->ucfirst() }}</legend>
<div class="row">
<div class="col-12">
@include('bo.partials.inputs.varchar', [
'id' => $idPrefix . '-pictoName',
'label' => str(
__(':field :inter:model', [
'field' => __('validation.attributes.title'),
'inter' => $inter,
'model' => $model,
]))->ucfirst(),
'hasWordCounter' => true,
'name' => 'title',
'required' => true,
'model' => $pictoModel ?? null,
'helper' => __('validation.between.string', [
'attribute' => __(':field :inter:model', [
'field' => __('validation.attributes.title'),
'inter' => $inter,
'model' => $model,
]),
'min' => 3,
'max' => 255,
]),
'placeholder' => __('Un beau titre'),
])
</div>
</div>
</fieldset>
</div>
{{-- CONTENT --}}
<div class="col-12 mb-3">
<fieldset class="border p-3">
<legend>{{ str(__('validation.attributes.description'))->ucfirst() }}</legend>
<div class="row mb-3">
<div class="col-12">
@include('bo.partials.inputs.text-html', [
'id' => $idPrefix . '-pictoContent',
'label' => __(':field :inter:model', [
'field' => str(__('validation.attributes.description'))->ucfirst(),
'inter' => __('validation.custom.of_the.male'),
'model' => $model,
]),
'name' => 'description',
'editorConfig' => \App\Enums\CkEditorConfigName::basic->label(),
'model' => $pictoModel ?? null,
'required' => true,
'helper' => __('crud.minicrud.text_warning'),
])
</div>
</div>
</fieldset>
</div>
{{-- PICTURE --}}
<div class="col-12 pb-3">
<fieldset class="h-100 border p-3">
<legend>{{ str(__('validation.attributes.image'))->ucfirst() }}</legend>
<div class="row mb-3">
{{-- PICTURE_ALT --}}
<div class="col-12 col-lg-6 m-lg-0 mb-3">
@include('bo.partials.inputs.varchar', [
'id' => $idPrefix . '-pictoPictureAlt',
'label' => __(':field1 :inter1:field2 :inter2:model', [
'field1' => str(__('validation.custom.attribute_alt'))->ucfirst(),
'inter1' => __('validation.custom.of_the.vowel'),
'field2' => __('validation.attributes.image'),
'inter2' => $inter,
'model' => $model,
]),
'hasWordCounter' => true,
'name' => 'picture_alt',
'required' => true,
'model' => $pictoModel ?? null,
'helper' => __('crud.minicrud.picture_alt_helper'),
'placeholder' => __(':field1 :inter1:field2 :inter2:model', [
'field1' => str(__('validation.custom.attribute_alt'))->ucfirst(),
'inter1' => __('validation.custom.of_the.vowel'),
'field2' => __('validation.attributes.image'),
'inter2' => $inter,
'model' => $model,
]),
])
</div>
{{-- PICTURE_TITLE --}}
<div class="col-12 col-lg-6 mb-3">
@include('bo.partials.inputs.varchar', [
'id' => $idPrefix . '-pictoPictureTitle',
'label' => __(':field1 :inter1:field2 :inter2:model', [
'field1' => str(__('validation.custom.attribute_title'))->ucfirst(),
'inter1' => __('validation.custom.of_the.vowel'),
'field2' => __('validation.attributes.image'),
'inter2' => $inter,
'model' => $model,
]),
'hasWordCounter' => true,
'name' => 'picture_title',
'required' => true,
'model' => $pictoModel ?? null,
'helper' => __('crud.minicrud.picture_title_helper'),
'placeholder' => __(':field1 :inter1:field2 :inter2:model', [
'field1' => str(__('validation.custom.attribute_title'))->ucfirst(),
'inter1' => __('validation.custom.of_the.vowel'),
'field2' => __('validation.attributes.image'),
'inter2' => $inter,
'model' => $model,
]),
])
</div>
<div class="row">
<div class="col-lg-6">
<div class="row">
<div class="col-6 col-lg-4">
@include('bo.partials.inputs.int-number', [
'id' => $idPrefix . '-pictoWidth',
'name' => 'picture_width',
'model' => $pictoModel ?? null,
// 'label' => str(__('validation.custom.width'))->ucFirst(),
'helper' => __('Largeur'),
])
</div>
<div class="col-6 col-lg-4">
@include('bo.partials.inputs.int-number', [
'id' => $idPrefix . '-pictoHeight',
'name' => 'picture_height',
'model' => $pictoModel ?? null,
// 'label' => str(__('validation.custom.height'))->ucFirst(),
'helper' => __('Hauteur'),
])
</div>
<div class="col-6 col-lg-4">
<button class="btn btn-primary submit-picture-size"
id="{{ $idPrefix }}-submitPictureSize"
data-url="{{ route('bo.pictos.savePictureSize') }}"
data-target-image="{{ $idPrefix }}-pictoPicture"
data-width-input="{{ $idPrefix }}-pictoWidth"
data-height-input="{{ $idPrefix }}-pictoHeight" data-bs-toggle="tooltip"
data-picto-id="{{ $pictoModel?->id }}"
type="button" title="{{ __('save size') }}">
{{ str(__('sauvegarder taille'))->ucfirst() }}
</button>
</div>
</div>
</div>
{{-- PICTURE --}}
<div class="col-lg-6">
<div class="row">
<div class="col-lg-12">
@include('bo.partials.inputs.varchar-picture', [
'id' => $idPrefix . '-pictoPicture',
'name' => 'picture',
'width' => $pictoModel?->picture_width ?? App\Models\Picto::MIN_WIDTH,
'height' => $pictoModel?->picture_height ?? App\Models\Picto::MIN_HEIGHT,
'model' => $pictoModel ?? null,
'required' => true,
'preview' => true,
])
</div>
</div>
</div>
</div>
</div>
</fieldset>
</div>
<div class="col-12 text-center">
<p><b>{{ __('* Champs obligatoires') }}</b></p>
</div>
</div>

View file

@ -0,0 +1,76 @@
<table id="pictos-table" class="table-hover table-fix-action m-0 table">
@if (count($pictos) > 0)
<thead>
@include('bo.modules.table-col-sorter', [
'cols' => [
'name' => str(__('validation.attributes.title'))->ucfirst(),
'description' => str(__('validation.attributes.description'))->ucfirst(),
'picture' => str(__('validation.custom.picture'))->ucfirst(),
'updated_at' => str(__('validation.attributes.updated_at'))->ucfirst(),
'order' => str(__('validation.custom.order'))->ucfirst(),
],
'ajaxSort' => true,
])
</thead>
<tbody>
@foreach ($pictos as $picto)
<tr class="border-bottom">
<td class="text-center align-middle">{{ $picto->title }}</td>
<td class="text-center align-middle">{!! clean($picto->description) !!}</td>
<td class="text-center align-middle">
<img class="image-preview" src="{{ asset($picto->picture) }}" title="{{ $picto->picture_title }}"
alt="{{ $picto->picture_alt }}">
</td>
<td class="text-center align-middle">
<span class="badge text-bg-secondary">{{ $picto->updated_at->isoFormat('LLLL') }}</span>
</td>
@include('bo.modules.change-model-order', [
'routeName' => 'pictos',
'model' => $picto,
'models' => $pictos,
])
<td class="text-end align-middle">
@canAny(['duplicate', 'update', 'delete'], $picto)
<form class="btn-group confirmDeleteTS" action="{{ route('bo.pictos.destroy', $picto) }}"
method="POST" novalidate data-picto="{{ $picto->id }}"
data-section="{{ $picto->section_id }}">
@can('update', $picto)
<a class="btn btn-sm btn-primary btn-edit-picto" data-bs-toggle="tooltip" href="#"
data-picto="{{ $picto->id }}" data-section="{{ $picto->section_id }}"
title="{{ str(__('crud.actions.update'))->ucfirst() }}">
<i class="fa-solid fa-pencil"></i>
</a>
@endcan
{{-- @can('duplicate', $picto)
<a class="btn btn-sm btn-secondary" data-bs-toggle="tooltip"
href="{{ route('bo.pictos.duplicate', $picto) }}"
title="{{ str(__('crud.actions.duplicate'))->ucfirst() }}">
<i class="fa-solid fa-copy"></i>
</a>
@endcan --}}
@can('delete', $picto)
@csrf
<button class="btn btn-sm btn-danger" data-bs-toggle="tooltip" type="submit"
title="{{ str(__('crud.actions.delete'))->ucfirst() }}">
<i class="fa-solid fa-trash"></i>
</button>
@endcan
</form>
@else
<span class="text-danger" data-bs-toggle="tooltip" title="{{ __('list.right') }}">
<i class="fa-solid fa-ban"></i>
</span>
@endcan
</td>
</tr>
@endforeach
</tbody>
@else
<tr>
<td class="border-0">
{{ trans_choice('crud.other.no_model_found', 2, ['model' => trans_choice('models.classes.picto', 2)]) }}
</td>
</tr>
@endif
</table>

View file

@ -0,0 +1,28 @@
@php
$currentCount = $section->pictos->count();
@endphp
@can('create', \App\Models\Picto::class)
<a class="btn btn-primary float-right btn-create-picto"
data-section="{{ $section->id }}" href="#" @disabled($currentCount >= $maxPictos)>
<i class="fa-solid fa-plus"></i>
</a>
@endcan
<div class="picto-list table-responsive mb-3" data-section="{{ $section->id }}">
<div class="flash-container"></div>
<div class="picto-table-wrapper">
@include('bo.pages.pictos.index-list', ['pictos' => $section->pictos])
</div>
</div>
<div class="picto-form-wrapper data-section" data-section="{{ $section->id }}" style="display:none;">
<button type="button" class="btn btn-primary btn-back-picto"
data-section="{{ $section->id }}">
<i class="fa-solid fa-arrow-left"></i>
</button>
@include('bo.pages.pictos.create', [
'pictoModel' => new \App\Models\Picto(),
'section' => $section,
'idPrefix' => 'picto-' . $section->id,
])
</div>

View file

@ -0,0 +1,35 @@
@extends('bo.layout', ['brParam' => (object) $commonSection])
@section('title', __('Édition section commune d\'une :model', ['model' => trans_choice('models.classes.static_page', 1)]))
@section('description', __('Édition de la séction commune dans les :model déjà existant', ['model' => trans_choice('models.classes.static_page', 2)]))
@section('breadcrumb', request()->route()->getName())
@section('content')
<div class="d-flex justify-content-between flex-md-nowrap align-items-center flex-wrap pb-3">
<div class="d-flex align-items-start flex-row">
@include('modules.breadcrumbs.breadcrumb-body', ['brParam' => (object) $commonSection])
</div>
<div class="mb-md-0 mb-2">
<div class="btn-group">
<button class="btn btn-primary" id="formSubmitClone" data-bs-toggle="tooltip" type="submit"
title="{{ __('crud.helpers.save_model', ['model' => trans_choice('models.classes.static_page', 1)]) }}">
{{ str(__('crud.actions.save'))->ucfirst() }}
</button>
</div>
</div>
</div>
<form action="{{ route('bo.static_page.commitmments.sections.update') }}" method="POST">
@csrf
@method('PATCH')
@include('bo.pages.static_pages.common_section.form')
@include('bo.partials.script-button-clone')
<div class="row">
<div class="col text-center">
<button class="btn btn-primary" id="formSubmit" data-bs-toggle="tooltip" type="submit"
title="{{ __('crud.helpers.save_model', ['model' => trans_choice('models.classes.news_breeding', 1)]) }}">
{{ str(__('crud.actions.save'))->ucfirst() }}
</button>
</div>
</div>
</form>
@endsection

View file

@ -32,4 +32,35 @@
</div>
</div>
</form>
{{-- Sections & block --}}
@if($isCommitmentPage)
@include('bo.pages.static_pages.sections.form_section', [
'sections' => $staticPageModel->sections
])
@endif
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function () {
const params = new URLSearchParams(window.location.search);
const sectionId = params.get('section');
const tab = params.get('tab') ?? 't3';
if (!sectionId) return;
const collapseEl = document.getElementById('collapse-' + sectionId);
if (!collapseEl) return;
// Ouvrir l'accordéon
const bsCollapse = new bootstrap.Collapse(collapseEl, { toggle: false });
bsCollapse.show();
// Activer le tab une fois l'accordéon ouvert
collapseEl.addEventListener('shown.bs.collapse', function () {
const tabTrigger = document.querySelector(`[data-bs-target="#s${sectionId}-${tab}"]`);
if (tabTrigger) bootstrap.Tab.getOrCreateInstance(tabTrigger).show();
}, { once: true });
});
</script>
@endpush
@endsection

View file

@ -121,7 +121,8 @@
!$staticPageModel->type->equals(StaticPageType::selling) &&
!$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))
{{-- SUBTITLE --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="h-100 border p-3">

View file

@ -0,0 +1,139 @@
@use('\App\Enums\StaticPageType', 'StaticPageType')
<div class="row mt-4">
<div class="accordion" id="mainAccordion">
@foreach ($sections as $index => $section)
@php
$collapseId = 'collapse-' . $section->id;
$toggleId = 'toggle-' . $section->id;
$isVisible = $section->visibility ?? true;
$sectionData = json_decode(json_encode($section->data), true) ?? [];
$hasPictos = !empty($sectionData['has_pictos']);
$hasBlocks = $section->blocks->isNotEmpty();
$maxPictos = $sectionData['max_pictos'] ?? 5;
@endphp
<div class="accordion-item border rounded-3 mb-3 shadow-sm">
{{-- Header --}}
<div class="accordion-header d-flex align-items-center px-3 py-2">
<button class="accordion-button collapsed flex-grow-1 border-0 bg-transparent shadow-none px-0 py-1"
type="button" data-bs-toggle="collapse" data-bs-target="#{{ $collapseId }}"
aria-expanded="false" aria-controls="{{ $collapseId }}">
<span class="btn btn-outline-primary ms-2">{{ $section->name }}</span>
</button>
<div class="d-flex align-items-center gap-2 ms-3 flex-shrink-0">
<div class="section-flash" id="flash-{{ $section->id }}"></div>
<small class="toggle-label text-muted">Visible</small>
<div class="form-check form-switch mb-0">
<input class="form-check-input section-toggle" type="checkbox" role="switch"
id="{{ $toggleId }}" data-target="#{{ $collapseId }}"
data-id="{{ $section->id }}" {{ $isVisible ? 'checked' : '' }}>
</div>
</div>
</div>
{{-- Body + Tabs --}}
<div id="{{ $collapseId }}" class="accordion-collapse collapse" data-bs-parent="#mainAccordion">
<div class="accordion-body">
<ul class="nav nav-tabs mb-3" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" data-bs-toggle="tab"
data-bs-target="#s{{ $section->id }}-t1" type="button" role="tab">
<i class="bi bi-eye me-1"></i> Générale
</button>
</li>
@if ($hasBlocks)
<li class="nav-item" role="presentation">
<button class="nav-link" data-bs-toggle="tab"
data-bs-target="#s{{ $section->id }}-t2" type="button" role="tab">
<i class="bi bi-bar-chart me-1"></i> Redirection
</button>
</li>
@endif
@if ($hasPictos)
<li class="nav-item" role="presentation">
<button class="nav-link" data-bs-toggle="tab"
data-bs-target="#s{{ $section->id }}-t3" type="button" role="tab">
<i class="bi bi-gear me-1"></i> Pictogrammes
</button>
</li>
@endif
</ul>
<div class="tab-content mx-4 my-4">
{{-- Tab --}}
<div class="tab-pane fade show active" id="s{{ $section->id }}-t1" role="tabpanel">
<form class="section-form" data-id="{{ $section->id }}" enctype="multipart/form-data">
@csrf
@method('PUT')
<input type="hidden" name="form_section" value="section_{{ $section->id }}">
@include('bo.modules.minicrud-form-inputs', [
'data' => $sectionData,
'model' => $staticPageModel,
'idPrefix' => 's' . $section->id,
])
<div class="row">
<div class="col text-center">
<button class="btn btn-primary" type="submit">
{{ str(__('crud.actions.save'))->ucfirst() }}
</button>
</div>
</div>
</form>
</div>
{{-- Tab Redirection --}}
@if ($hasBlocks)
<div class="tab-pane fade" id="s{{ $section->id }}-t2" role="tabpanel">
@foreach ($section->blocks as $blockIndex => $block)
<form class="block-form" data-id="{{ $block->id }}" enctype="multipart/form-data">
@csrf
@method('PUT')
<input type="hidden" name="form_block" value="block_{{ $block->id }}">
@include('bo.partials.blocks.redirection-block-form', [
'data' => json_decode(json_encode($block->data), true),
'model' => $staticPageModel,
'idPrefix' => 's' . $section->id . '-b' . $block->id,
])
<div class="row mt-4">
<div class="col text-center">
<button class="btn btn-primary" type="submit">
{{ str(__('crud.actions.save'))->ucfirst() }}
</button>
</div>
</div>
</form>
@if (!$loop->last)
<hr class="my-4">
@endif
@endforeach
</div>
@endif
{{-- Tab Pictogrammes --}}
@if ($hasPictos)
<div class="tab-pane fade" id="s{{ $section->id }}-t3" role="tabpanel">
@include('bo.pages.pictos.index', [
'section' => $section,
'idPrefix' => 's' . $section->id,
'maxPictos' => $maxPictos,
])
</div>
@endif
</div>
</div>
</div>
</div>
@endforeach
</div>
</div>

View file

@ -0,0 +1,140 @@
@php
use \App\Enums\Block\BlockPositionEnum;
use \App\Enums\Block\BlockContentTypeEnum;
@endphp
{{-- Titre --}}
<div class="mb-3">
<fieldset class="border p-3">
<legend>{{ __('Titre') }}</legend>
@include('bo.partials.inputs.varchar', [
'id' => $idPrefix . '-title',
'label' => __('Titre'),
'hasWordCounter' => true,
'name' => 'data[title][value]',
'value' => old('data.title.value', $data['title']['value'] ?? null),
'required' => false,
'model' => $model ?? null,
'helper' => __('validation.between.string', ['attribute' => __('Titre'), 'min' => 3, 'max' => 150]),
])
@include('modules.input-error', ['inputName' => 'data.title.value'])
</fieldset>
</div>
{{-- Position --}}
<div class="mb-3">
<fieldset class="border p-3">
<legend>{{ __('Position') }} *</legend>
<select class="form-select" name="data[position][value]" required>
<option value="" {{ ($data['position']['value'] ?? '') === '' ? 'selected' : '' }}>
{{ __('-- Choisissez une position --') }}
</option>
@foreach (BlockPositionEnum::cases() as $case)
<option value="{{ $case->value }}"
{{ ($data['position']['value'] ?? '') === $case->value ? 'selected' : '' }}>
{{ __($case->label()) }}
</option>
@endforeach
</select>
<small class="form-text text-muted">{{ __('Choisissez la position du bloc sur la page.') }}</small>
@include('modules.input-error', ['inputName' => 'data.position.value'])
</fieldset>
</div>
{{-- Type de contenu --}}
<div class="mb-3">
<fieldset class="border p-3">
<legend>{{ __('Type de contenu') }} *</legend>
<select class="form-select" name="data[block_type][value]" id="{{ $idPrefix }}-block-type" required>
@foreach (BlockContentTypeEnum::cases() as $case)
<option value="{{ $case->value }}"
{{ ($data['block_type']['value'] ?? '') === $case->value ? 'selected' : '' }}>
{{ __($case->label()) }}
</option>
@endforeach
</select>
<small
class="form-text text-muted">{{ __('Sélectionnez le type de contenu à afficher dans ce bloc.') }}</small>
@include('modules.input-error', ['inputName' => 'data.block_type.value'])
</fieldset>
</div>
{{-- Champs CTA --}}
<div id="{{ $idPrefix }}-fields-cta" class="row">
@php
$ctaFields = [
'cta_url' => $data['cta_url'] ?? ['label' => 'URL du bouton', 'value' => null, 'required' => false],
'cta_label' => $data['cta_label'] ?? ['label' => 'Nom du CTA', 'value' => null, 'required' => false],
];
@endphp
@foreach ($ctaFields as $k => $input)
<div class="minicrud-input col-12 col-md-6 mb-3">
<fieldset class="h-100 border p-3">
<legend>{{ $input['label'] }} {{ !empty($input['required']) ? '*' : '' }}</legend>
<div class="row">
<div class="col-12 mb-3">
@include('bo.partials.inputs.varchar', [
'id' => $idPrefix . '-' . str($input['label'])->slug(),
'label' => $input['label'],
'hasWordCounter' => true,
'name' => 'data[' . $k . '][value]',
'value' => old("data.{$k}.value", $input['value'] ?? null),
'required' => !empty($input['required']),
'model' => $model ?? null,
'helper' =>
$k === 'cta_url'
? __('validation.url', ['attribute' => $input['label']])
: __('validation.between.string', [
'attribute' => $input['label'],
'min' => 3,
'max' => 100,
]),
])
@include('modules.input-error', ['inputName' => "data.{$k}.value"])
</div>
</div>
</fieldset>
</div>
@endforeach
</div>
{{-- Champs Actu --}}
<div id="{{ $idPrefix }}-fields-actu" class="row d-none">
<div class="col-12 mb-3">
<fieldset class="h-100 border p-3">
<legend>{{ str(trans_choice('models.classes.news', 1))->ucFirst() }}</legend>
<div class="row">
<div class="col-12">
@include('bo.partials.inputs.search-belongs-to-dropdown', [
'id' => $idPrefix . '-article-id',
'label' => __(':model associé', [
'model' => str(trans_choice('models.classes.news', 1))->ucFirst(),
]),
'name' => 'data[article_id][value]',
'relation' => null,
'model' => $model ?? null,
'targetClass' => '\App\Models\News',
'modelListPaginate' => \App\Models\News::where('published',1)->paginate(10),
'routeName' => 'bo.news.json-paginate',
'fieldName' => 'title',
'multiple' => false,
'required' => true,
'disabled' => false,
'selectedIds' =>
isset($data['article_id']['value']) && $data['article_id']['value']
? [$data['article_id']['value']]
: [],
'placeholder' => __('Rechercher un :model', [
'model' => trans_choice('models.classes.news', 1),
]),
'helper' => __(
'Sélectionner un :model qui sera associé parmi la liste (un seul et unique :model doit être choisi).',
['model' => trans_choice('models.classes.news', 1)]),
])
</div>
</div>
</fieldset>
</div>
</div>

View file

@ -1,17 +1,24 @@
<div id="{{ sprintf("%sSection", $id)}}" class="row">
@php
$value = $value ?? $model->data[$name]['value'] ?? null;
@endphp
<div id="{{ sprintf('%sSection', $id) }}" class="row">
{{-- BUTTONS SELECTOR --}}
<div class="col-12 col-md-6 mb-3">
<div class="input-group justify-content-center align-items-center h-100">
<input class="btn-check" id="mediaSelectorVideo" name="mediaSelector" type="radio" autocomplete="off"
@checked(isset($model->data[$name]['value']['video']))>
<label class="btn btn-secondary rounded-start" for="mediaSelectorVideo">{{ __('Vidéo') }}</label>
<input class="btn-check" id="mediaSelectorImage" name="mediaSelector" type="radio" autocomplete="off"
@checked(isset($model->data[$name]['value']['picture']))>
<label class="btn btn-secondary" for="mediaSelectorImage">{{ __('Image') }}</label>
<input class="btn-check" id="{{ $id }}SelectorVideo" name="mediaSelector_{{ $id }}"
type="radio" autocomplete="off" @checked(isset($value['video']))>
<label class="btn btn-secondary rounded-start"
for="{{ $id }}SelectorVideo">{{ __('Vidéo') }}</label>
<input class="btn-check" id="{{ $id }}SelectorImage" name="mediaSelector_{{ $id }}"
type="radio" autocomplete="off" @checked(isset($value['picture']))>
<label class="btn btn-secondary" for="{{ $id }}SelectorImage">{{ __('Image') }}</label>
@if (!$required)
<input class="btn-check" id="mediaSelectorEmpty" name="mediaSelector" type="radio" autocomplete="off"
@checked(!$required && empty($model->data[$name]['value']['video']) && empty($model->data[$name]['value']['picture']))>
<label class="btn btn-secondary" for="mediaSelectorEmpty">{{ __('Rien') }}</label>
<input class="btn-check" id="{{ $id }}SelectorEmpty" name="mediaSelector_{{ $id }}"
type="radio" autocomplete="off"
@checked(empty($value['video']) && empty($value['picture']))>
<label class="btn btn-secondary" for="{{ $id }}SelectorEmpty">{{ __('Rien') }}</label>
@endif
</div>
</div>
@ -22,77 +29,83 @@
{{-- VIDEO --}}
<div @class([
sprintf('%sVideo', $id) . ' col-12 col-md-6 mb-3',
'd-none' => empty($model->data[$name]['value']['video']),
'd-none' => empty($value['video']),
])>
{{-- VIDEO ID --}}
@include('bo.partials.inputs.varchar', [
'id' => sprintf("%sVideoId", $id),
'id' => sprintf('%sVideoId', $id),
'label' => Str::ucFirst(__('validation.custom.video_id')),
'hasWordCounter' => true,
'name' => "data[$name][value][video][id]",
'required' => true,
'disabled' => empty($model->data[$name]['value']['video']),
'value' => $model->data[$name]['value']['video']['id'] ?? null,
'disabled' => empty($value['video']),
'value' => $value['video']['id'] ?? null,
'helper' => Str::ucFirst(__('validation.custom.video_id')),
'placeholder' => Str::ucFirst(__('validation.custom.video_id')),
])
</div>
{{-- VIDEO_ALT --}}
<div @class([
sprintf('%sVideo', $id) . ' col-12 col-md-6 mb-3',
'd-none' => empty($model->data[$name]['value']['video']),
'd-none' => empty($value['video']),
])>
@include('bo.partials.inputs.varchar', [
'id' => sprintf("%sVideoAlt", $id),
'id' => sprintf('%sVideoAlt', $id),
'label' => Str::ucFirst(__('validation.custom.video_alt')),
'hasWordCounter' => true,
'name' => "data[$name][value][video][alt]",
'required' => true,
'model' => $model ?? null,
'disabled' => empty($model->data[$name]['value']['video']['alt']),
'value' => $model->data[$name]['value']['video']['alt'] ?? '',
'helper' => __('crud.minicrud.video_alt_helper') . ' ' . __('validation.between.string', [
'attribute' => Str::ucFirst(__('validation.custom.video_alt')),
'min' => 6,
'max' => 255
]),
'disabled' => empty($value['video']['alt']),
'value' => $value['video']['alt'] ?? '',
'helper' =>
__('crud.minicrud.video_alt_helper') .
' ' .
__('validation.between.string', [
'attribute' => Str::ucFirst(__('validation.custom.video_alt')),
'min' => 6,
'max' => 255,
]),
'placeholder' => Str::ucFirst(__('validation.custom.video_alt')),
])
</div>
{{-- VIDEO_TITLE --}}
<div @class([
sprintf('%sVideo', $id) . ' col-12 col-md-6 mb-3',
'd-none' => empty($model->data[$name]['value']['video']),
'd-none' => empty($value['video']),
])>
@include('bo.partials.inputs.varchar', [
'id' => sprintf("%sVideoTitle", $id),
'id' => sprintf('%sVideoTitle', $id),
'label' => Str::ucFirst(__('validation.custom.video_title')),
'hasWordCounter' => true,
'name' => "data[$name][value][video][title]",
'required' => true,
'model' => $model ?? null,
'disabled' => empty($model->data[$name]['value']['video']['title']),
'value' => $model->data[$name]['value']['video']['title'] ?? '',
'helper' => __('crud.minicrud.video_title_helper') . ' ' . __('validation.between.string', [
'attribute' => Str::ucFirst(__('validation.custom.video_title')),
'min' => 6,
'max' => 255
]),
'disabled' => empty($value['video']['title']),
'value' => $value['video']['title'] ?? '',
'helper' =>
__('crud.minicrud.video_title_helper') .
' ' .
__('validation.between.string', [
'attribute' => Str::ucFirst(__('validation.custom.video_title')),
'min' => 6,
'max' => 255,
]),
'placeholder' => Str::ucFirst(__('validation.custom.video_title')),
])
</div>
{{-- PICTURE --}}
<div @class([
sprintf('%sPicture', $id) . ' col-12 col-md-6 mb-3',
'd-none' => empty($model->data[$name]['value']['picture']),
'd-none' => empty($value['picture']),
])>
@include('bo.partials.inputs.varchar-picture', [
'id' => sprintf("%sPicture", $id),
'id' => sprintf('%sPicture', $id),
'name' => "data[$name][value][picture][path]",
'width' => $width,
'height' => $height,
'model' => $model ?? null,
'value' => $model->data[$name]['value']['picture']['path'] ?? null,
'value' => $value['picture']['path'] ?? null,
'helper' => __('crud.minicrud.dimensions_must_be', [
'width' => $width,
'height' => $height,
@ -101,54 +114,61 @@
'preview' => false,
])
</div>
{{-- PICTURE_ALT --}}
<div @class([
sprintf('%sPicture', $id) . ' col-12 col-md-6 mb-3',
'd-none' => empty($model->data[$name]['value']['picture']),
'd-none' => empty($value['picture']),
])>
@include('bo.partials.inputs.varchar', [
'id' => sprintf("%sPictureAlt", $id),
'id' => sprintf('%sPictureAlt', $id),
'label' => Str::ucFirst(__('validation.custom.picture_alt')),
'hasWordCounter' => true,
'name' => "data[$name][value][picture][alt]",
'required' => true,
'model' => $model ?? null,
'disabled' => empty($model->data[$name]['value']['picture']['alt']),
'value' => $model->data[$name]['value']['picture']['alt'] ?? '',
'helper' => __('crud.minicrud.picture_alt_helper') . ' ' . __('validation.between.string', [
'attribute' => Str::ucFirst(__('validation.custom.picture_alt')),
'min' => 6,
'max' => 255
]),
'disabled' => empty($value['picture']['alt']),
'value' => $value['picture']['alt'] ?? '',
'helper' =>
__('crud.minicrud.picture_alt_helper') .
' ' .
__('validation.between.string', [
'attribute' => Str::ucFirst(__('validation.custom.picture_alt')),
'min' => 6,
'max' => 255,
]),
'placeholder' => Str::ucFirst(__('validation.custom.picture_alt')),
])
</div>
{{-- PICTURE_TITLE --}}
<div @class([
sprintf('%sPicture', $id) . ' col-12 col-md-6 mb-3',
'd-none' => empty($model->data[$name]['value']['picture']),
'd-none' => empty($value['picture']),
])>
@include('bo.partials.inputs.varchar', [
'id' => sprintf("%sPictureTitle", $id),
'id' => sprintf('%sPictureTitle', $id),
'label' => Str::ucFirst(__('validation.custom.picture_title')),
'hasWordCounter' => true,
'name' => "data[$name][value][picture][title]",
'required' => true,
'model' => $model ?? null,
'disabled' => empty($model->data[$name]['value']['picture']['title']),
'value' => $model->data[$name]['value']['picture']['title'] ?? '',
'helper' => __('crud.minicrud.picture_title_helper') . ' ' . __('validation.between.string', [
'attribute' => Str::ucFirst(__('validation.custom.picture_title')),
'min' => 6,
'max' => 255
]),
'disabled' => empty($value['picture']['title']),
'value' => $value['picture']['title'] ?? '',
'helper' =>
__('crud.minicrud.picture_title_helper') .
' ' .
__('validation.between.string', [
'attribute' => Str::ucFirst(__('validation.custom.picture_title')),
'min' => 6,
'max' => 255,
]),
'placeholder' => Str::ucFirst(__('validation.custom.picture_title')),
])
</div>
{{-- NOTHING SELECTED --}}
<div @class([
sprintf('%sNone', $id) . ' col-12 col-md-6',
'd-none' => !empty($model->data[$name]['value']['picture']) || !empty($model->data[$name]['value']['video']),
'd-none' => !empty($value['picture']) || !empty($value['video']),
])>
<p class="text-center text-body-secondary m-0">{{ __('Aucun média n\'est sélectionné') }}</p>
</div>
@ -156,7 +176,7 @@
@push('scripts')
<script nonce="{{ $nonce }}">
document.addEventListener("DOMContentLoaded", function () {
document.addEventListener("DOMContentLoaded", function() {
initMediaSelector("{{ $id }}");
});
</script>

View file

@ -2,9 +2,16 @@
<b>{{ $label }} {{ $required ? '*' : '' }}</b>
</label>
@php
$modelSelected = old($name)
? $targetClass::find(old($name, []))
: $model->{(isset($relation)) ? $relation : $name};
$modelSelected = null;
if (old($name)) {
$modelSelected = $targetClass::find(old($name, []));
} elseif (isset($relation) && $relation) {
$modelSelected = $model->{$relation};
} elseif (isset($selectedIds) && $selectedIds) {
$modelSelected = $targetClass::find($selectedIds);
}
$data = [
'id' => $id,
'name' => $name,

View file

@ -14,13 +14,13 @@
</div>
@endif
<div class="d-flex align-items-center">
@if (isset($badge) && !empty($picto))
<img class="img-fluid my-3 me-2" id="header-banner-picto" src="{{ $picto }}" title="{{ $picto_title }}"
alt="{{ $picto_alt }}">
@if (isset($badge) && !empty($picto_baner))
<img class="img-fluid my-3 me-2" id="header-banner-picto" src="{{ asset($picto_baner['value']) }}" title="{{ $picto_baner['title'] }}"
alt="{{ $picto_baner['alt'] }}">
@endif
<h1 class="my-3">{{ $staticPageModel->title }}</h1>
</div>
<p>{!! clean($staticPageModel->subtitle) !!}</p>
<p>{!! clean($staticPageModel->content) !!}</p>
</hgroup>
</div>
<div class="d-none d-xl-block col-6 col-xxl-4">

View file

@ -1,20 +1,26 @@
@php
$commitmentId = 5;
$staticPageModel = new stdClass();
$staticPageModel->seo_title = str('respecter la terre')->ucFirst();
$staticPageModel->seo_description =
'Optimiser le transport, privilégier des fournisseurs locaux et réduire les emballages... quelques actions pour agir ensemble sur notre empreinte carbone !';
$staticPageModel->title = str('respecter la terre')->upper();
$staticPageModel->subtitle =
'Le Gaulois s’engage à réduire son empreinte carbone : optimiser le transport, privilégier des fournisseurs locaux et réduire les emballages.';
$staticPageModel->picture = Vite::asset("resources/assets/images/commitments/commitment-{$commitmentId}/banner.jpg");
$staticPageModel->picture_alt = 'Forêt éclairé par la lumière du soleil.';
$staticPageModel->picture_title = "Bannière de la page engagement n°{$commitmentId}";
$badge = new stdClass();
$badge->label = str("engagement n°{$commitmentId}")->upper();
$badge->color = '#006633';
$section_2_model = $staticPageModel->sections[0];
$section_2 = $section_2_model->data;
$section_2_pictos = $section_2_model->pictos;
$reduction_2 = $section_2_model->blocks[0]->data;
$section_3_model = $staticPageModel->sections[1];
$section_3 = $section_3_model->data;
$reduction_3 = $section_3_model->blocks[0]->data;
$section_4_model = $staticPageModel->sections[2];
$section_4 = $section_4_model->data;
$section_4_pictos = $section_4_model->pictos;
$reduction_4 = $section_4_model->blocks[0]->data;
@endphp
@extends('fo.layout', ['brParam' => $staticPageModel])
@section('title', str($staticPageModel->seo_title)->ucFirst())
@ -27,198 +33,74 @@
@include('fo.partials.commitments.navigation')
{{-- HEADER --}}
@include('fo.layouts.header-banner', [
'picto' => Vite::asset('resources/assets/images/commitments/navigation/picto-life.svg'),
'picto_alt' => __('Pictogramme d’un serrage de main.'),
'picto_title' => __('Pictogramme d’un serrage de main.'),
'picto' => asset($staticPageModel->picture),
'picto_alt' => __($staticPageModel->picture_alt),
'picto_title' => __($staticPageModel->picture_title),
'picto_baner' => $staticPageModel->data['picto_banner'],
])
{{-- PARAGRAPH WITH VISUAL AND COMPLEX ITEMS --}}
@php
// Item 1
$item1 = new stdClass();
$item1->title = str(__('colisage optimisé'))->upper();
$item1->subtitle = __('avec des cartons pleins');
$item1->picto = 'resources/assets/images/commitments/pictograms/picto-open-box.svg';
$item1->picto_alt = __('Pictogramme d’un carton ouvert pour optimiser les colisages');
$item1->picto_title = __('Pictogramme d’un colisage optimisé');
// Item 2
$item2 = new stdClass();
$item2->title = str(__('fournisseurs locaux'))->upper();
$item2->subtitle = __('privilégiés');
$item2->picto = 'resources/assets/images/commitments/pictograms/picto-local-french-suppliers.svg';
$item2->picto_alt = __(
'Pictogramme de la carte de France avec un emplacement de coq, illustrant les fournisseurs locaux privilégiés par Le Gaulois',
);
$item2->picto_title = __('Pictogramme de fournisseurs français locaux privilégiés');
// Item 3
$item3 = new stdClass();
$item3->title = str(__('réduction des distances'))->upper();
$item3->subtitle = __('de transport des animaux');
$item3->picto = 'resources/assets/images/commitments/pictograms/picto-terminal.svg';
$item3->picto_alt = __(
'Pictogramme d’une borne de distance kilométrique peu élevé pour réduire les distances de transport des volailles Le Gaulois',
);
$item3->picto_title = __('Pictogramme de réduction des distance de transport des volailles');
// Item 4
$item4 = new stdClass();
$item4->title = str(__('des boucles de transport vertueuses'))->upper();
$item4->subtitle = __('des produits');
$item4->picto = 'resources/assets/images/commitments/pictograms/picto-map.svg';
$item4->picto_alt = __('Pictogramme d’une carte avec des boucles de transport plus vertueuses pour les produits Le Gaulois');
$item4->picto_title = __('Pictogramme d’une boucle de transport vertueux');
// Item 5
$item5 = new stdClass();
$item5->title = str(__('amélioration de la flotte'))->upper();
$item5->subtitle = __('dédiée au ramassage');
$item5->picto = 'resources/assets/images/commitments/pictograms/picto-truck.svg';
$item5->picto_alt = __('Pictogramme d’un camion plus écologique pour la flotte dédiée au ramassage des volailles Le Gaulois');
$item5->picto_title = __('Pictogramme d’une flotte de transport dédié au ramassage plus écologique');
// Item 6
$item6 = new stdClass();
$item6->sustitle = str(__('des poulaillers à'))->ucFirst();
$item6->title = str(__('basse consommation'))->upper();
$item6->picto = 'resources/assets/images/commitments/pictograms/picto-henhouse.svg';
$item6->picto_alt = __('Poulaillers d’élevage à basse consommation');
$item6->picto_title = __('Pictogramme d’un poulailler à basse consommation pour les volailles Le Gaulois');
// All
$collectionItems = collect([$item1, $item2, $item3, $item4, $item5, $item6]);
@endphp
<section class="line-border-large bg-light">
@if ($section_2_model->visibility)
@include('fo.partials.commitments.paragraph-with-visual-complex-flex-items', [
'title' => __('priorité à la réduction de notre empreinte carbone'),
'text' => nl2br(
__(
'Chaque initiative joue un rôle crucial dans la :strongStartlutte contre les gaz à effet de serre:strongEnd, 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 :strongStartréchauffement climatique:strongEnd.',
[
'strongStart' => '<strong>',
'strongEnd' => '</strong>',
])),
'collection' => $collectionItems,
'picture' => Vite::asset('resources/assets/images/commitments/common/footprint.jpg'),
'picture_alt' => __('Vue du dessus d’une empreinte humaine de pied avec de l’eau et entouré d’une forêt verdoyante'),
'picture_title' => __('Empreinte carbone de l’activité humaine sur la Terre'),
'picto' => Vite::asset('resources/assets/images/commitments/pictograms/picto-human-footprint.svg'),
'picto_alt' => __(
'Pictogramme d’une empreinte digitale humaine, symbôle de l’engagement de Le Gaulois à réduire son empreinte carbone sur la terre.'),
'picto_title' => __('Pictogramme d’une empreinte de doigt'),
'title' => __($section_2->title->value),
'text' => nl2br(__($section_2->description->value)),
'collection' => $section_2_pictos,
'picture' => asset($section_2->media->value->picture->path),
'picture_alt' => $section_2->media->value->picture->alt,
'picture_title' => $section_2->media->value->picture->title,
'picto' => asset($section_2->media_picto->value),
'picto_alt' => __($section_2->media_picto->alt),
'picto_title' => __($section_2->media_picto->title),
'reduction' => $reduction_2,
])
</section>
@endif
{{-- PARAGRAPH WITH PICTURE --}}
@include('fo.partials.commitments.paragraph-with-visual', [
'title' => __('stop à la déforestation, halte au soja non responsable'),
'text' => nl2br(
__(
'Chez Le Gaulois, pour assurer :strongStartl’apport protéique:strongEnd et assurer un :strongStartéquilibre nutritionnel de nos volailles:strongEnd, nous les alimentons avec du :strongStartsoja:strongEnd. C’est la :strongStartlégumineuse la plus riche en protéines et en acides aminés essentiels !:strongEnd Conscient que la culture du Soja importée est assujettie à de nombreux questionnements, notamment son impact sur la :strongStartdéforestation.:strongEnd',
[
'strongStart' => '<strong>',
'strongEnd' => '</strong>',
])),
'background_color' => 'light',
'picture' => Vite::asset('resources/assets/images/commitments/common/soja.jpg'),
'picture_alt' => __('Graines de soja importées mais garanties zéro déforesation d’ici 2025'),
'picture_title' => __('Soja responsable et engagé contre la déforestation'),
'picto' => Vite::asset('resources/assets/images/commitments/pictograms/picto-cereals.svg'),
'picto_alt' => __('Céréales cultivées en France pour nourrir les volailles Le Gaulois'),
'picto_title' => __('Pictogramme de céréales pour l’alimentation des volailles Le Gaulois'),
'card_sustitle' => str('Le Gaulois s’engage')->upper(),
'card_title' => '100% de soja',
'card_subtitle' => 'garanti zéro déforestation',
'card_text' => 'conversion au plus tard d’ici 2025.',
'card_picto' => Vite::asset('resources/assets/images/commitments/pictograms/picto-target.svg'),
'card_picto_alt' => __('Objectif 100% de soja, garanti zéro déforestation'),
'card_picto_title' => __('Pictogramme d’une cible pour l’engagement de Le Gaulois'),
])
@if ($staticPageModel->sections[1]->visibility)
@include('fo.partials.commitments.paragraph-with-visual', [
'title' => __($section_3->title->value),
'text' => __($section_3->description->value),
'background_color' => 'light',
'picture' => asset($section_3->media->value->picture->path),
'picture_alt' => $section_3->media->value->picture->alt,
'picture_title' => $section_3->media->value->picture->title,
'picto' => asset($section_3->media_picto->value),
'picto_alt' => __($section_3->media_picto->alt),
'picto_title' => __($section_3->media_picto->title),
'card_title' => __($section_3->encare_beige_title->value),
'card_text' => __($section_3->encare_beige_description->value),
'reduction' => $reduction_3,
])
@endif
{{-- PARAGRAPH WITH PICTURE --}}
@include('fo.partials.commitments.paragraph-with-visual', [
'title' => __('réduire l’impact de nos emballages sur l’environnement'),
'text' => nl2br(
__(
'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.') .
'</br>' .
__(
'<ul class="mb-3 p-0">Depuis fin 2023, près de :strongStart80% de nos emballages sont responsables:strongEnd :<li class="ms-4">soit intégrant de la matière recyclée</li><li class="ms-4">soit en étant recyclables</li></ul>',
['strongStart' => '<strong>', 'strongEnd' => '</strong>']) .
__(
'<p>D’ici fin 2025, nous ambitionnons de passer à près de :strongStart95% d’emballages responsables.:strongEnd</p>',
[
'strongStart' => '<strong>',
'strongEnd' => '</strong>',
]) .
'</br>' .
__(
':divStart:image:hgroupStart:strongStartLe saviez-vous ?:strongEnd 100% des emballages cartons Le Gaulois sont déjà issus de :strongStartcartons recyclés et certifiés FSC:strongEnd, garantissant une gestion responsable des forêts, et :strongStartplus de 60% des barquettes:strongEnd de volaille Le Gaulois sont recyclables ou intégrant :strongStartau moins 50% de matière recyclée.:hgroupEnd:divEnd:strongEnd',
[
'image' =>
'<img src="' .
Vite::asset('resources/assets/images/commitments/pictograms/picto-question.svg') .
'" class="me-2" alt="Pictogramme d’une ampoule pour informer sur un fait marquant à retenir" title="Pictogramme d’une ampoule">',
'divStart' => '<div class="card-text-part d-flex justify-content-start align-items-center">',
'divEnd' => '</div>',
'hgroupStart' => '<hgroup>',
'hgroupEnd' => '</hgroup>',
'strongStart' => '<strong>',
'strongEnd' => '</strong>',
])),
'background_color' => 'light',
'picture' => Vite::asset('resources/assets/images/commitments/common/box.jpg'),
'picture_alt' => __('Colis en carton avec une estampille de recyclabilité'),
'picture_title' => __('Emballages cartons issus de cartons recyclés ou certifiés FSC'),
'picto' => Vite::asset('resources/assets/images/commitments/pictograms/picto-recycle.svg'),
'picto_alt' => __('Pictogramme de recyclabilité sur les emballages et la gestion des déchets'),
'picto_title' => __('Pictogramme sur les emballages et la gestion des déchets'),
'reversed' => true,
])
{{-- COMPLEX FLEX ITEMS --}}
@php
// Item 1
$complexItem1 = new stdClass();
$complexItem1->title = str(__('réduction des emballages'))->upper();
$complexItem1->text = __('épaisseurs, dimensions et poids');
$complexItem1->picto = 'resources/assets/images/commitments/pictograms/picto-reduction-packaging.svg';
$complexItem1->picto_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',
);
$complexItem1->picto_title = __('Pictogramme sur la réduction du poids des emballages');
// Item 2
$complexItem2 = new stdClass();
$complexItem2->title = str(__('100% des emballages carton'))->upper();
$complexItem2->text = __('issus de cartons recyclés ou certifiés FSC Mixte');
$complexItem2->picto = 'resources/assets/images/commitments/pictograms/picto-recycled-cardboard-packaging.svg';
$complexItem2->picto_alt = __('Pictogramme d’un carton issus de cartons recyclés ou certifiés FSC Mixte');
$complexItem2->picto_title = __('Pictogramme d’un emballage carton recyclé ou certifié FSC');
// Item 3
$complexItem3 = new stdClass();
$complexItem3->sustext = str(__('Mise au point d’une'))->ucFirst();
$complexItem3->title = str(__('filière de recyclabilié'))->upper();
$complexItem3->text = __('des barquettes de volaille Le Gaulois');
$complexItem3->picto = 'resources/assets/images/commitments/pictograms/picto-recycling-industry.svg';
$complexItem3->picto_alt = __('Pictogramme d’une filière de recyclabilité en France sur les barquettes de volailles Le Gaulois');
$complexItem3->picto_title = __('Pictogramme d’une filière de recyclabilité des barquettes');
// Item 4
$complexItem4 = new stdClass();
$complexItem4->title = str(__('moins de plastique'))->upper();
$complexItem4->text = str(__('minimiser notre empreinte carbone et agir sur le changement climatique'));
$complexItem4->picto = 'resources/assets/images/commitments/pictograms/picto-plastic-decrease.svg';
$complexItem4->picto_alt = __('Pictogramme d’un sac plastique avec l’estampille de recyclabilité');
$complexItem4->picto_title = __('Pictogramme sur la diminution de plastique');
// All
$collectionComplexItems = collect([$complexItem1, $complexItem2, $complexItem3, $complexItem4]);
@endphp
@include('fo.partials.commitments.complex-flex-items', [
'background_color' => 'quint',
'collection' => $collectionComplexItems,
])
@if ($section_4_model->visibility)
@include('fo.partials.commitments.paragraph-with-visual', [
'title' => __($section_4->title->value),
'text' => __($section_4->description->value),
'encart' => $section_4->encare_beige->value,
'background_color' => 'light',
'picture' => asset($section_4->media->value->picture->path),
'picture_alt' => __($section_4->media->value->picture->alt),
'picture_title' => __($section_4->media->value->picture->title),
'picto' => asset($section_4->media_picto->value),
'picto_alt' => __($section_4->media_picto->alt),
'picto_title' => __($section_4->media_picto->title),
'reversed' => true,
'reduction' => $reduction_4,
])
{{-- COMPLEX FLEX ITEMS --}}
@include('fo.partials.commitments.complex-flex-items', [
'background_color' => 'quint',
'collection' => $section_4_pictos,
])
@endif
{{-- OTHER COMMITMENTS --}}
@include('fo.partials.commitments.other-commitments', [
'number' => $commitmentId,
'title' => 'Découvrez plus d’engagements Le Gaulois',
'text' => nl2br(
__('Face aux enjeux de notre époque, Le Gaulois répond présent.') .
'</br>' .
__(
'Chaque jour, nous avançons pas à pas avec :strongStartles équipes, les partenaires et les consommateurs:strongEnd 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 :strongStartminimiser l’impact de notre activité sur l’environnement, veiller au bien-être animal et améliorer notre alimentation et notre qualité de vie:strongEnd.',
[
'strongStart' => '<strong>',
'strongEnd' => '</strong>',
])),
'title' => $staticPageModel->data['common_section']['title'],
'text' => $staticPageModel->data['common_section']['content'],
])
@endsection

View file

@ -0,0 +1,48 @@
@php
use App\Enums\Block\BlockContentTypeEnum;
$pos = \App\Enums\Block\BlockPositionEnum::from($position)->toBootstrap();
$hasCta = in_array($blockType, [BlockContentTypeEnum::Cta->value, BlockContentTypeEnum::Both->value]);
$hasActu = in_array($blockType, [BlockContentTypeEnum::Actu->value, BlockContentTypeEnum::Both->value]);
@endphp
<div class="row justify-content-{{ $pos }}">
@if ($hasActu && $news)
<div class="col-12 {{ $hasCta ? 'col-md-4' : 'col-md-6' }} order-1">
<a href="{{ route('fo.news.show', $news->slug) }}" class="text-decoration-none text-dark">
<div class="card bg-white shadow-lg border-0">
<div class="row g-0">
<div class="col-4">
<img src="{{ asset($news->picture) }}" class="img-fluid rounded-start h-100 object-fit-cover"
alt="{{ $news->picture_alt }}" title="{{ $news->picture_title }}">
</div>
<div class="col-8">
<div class="card-body py-2">
<div class="d-flex flex-column flex-md-row align-items-md-center gap-1 gap-md-2 mb-1">
<span class="badge text-uppercase align-self-start"
style="background-color: {{ $news->newsCategory->color }}">
{!! clean($news->newsCategory->title) !!}
</span>
<small class="text-muted ps-md-1"
style="padding-left: 5px;">{{ \Carbon\Carbon::parse($news->updated_at)->format('d/m/Y') }}</small>
</div>
<p class="card-text fw-bold text-uppercase mb-0" style="font-size: 0.85rem;">
{{ $news->title }}
</p>
</div>
</div>
</div>
</div>
</a>
</div>
@endif
@if ($hasCta && $ctaUrl && $ctaLabel)
<div class="col-12 col-md-auto order-2 order-md-1 d-flex align-items-center mt-3 mt-md-0">
<a href="{{ $ctaUrl }}" class="btn btn-secondary btn-lg">{{ $ctaLabel }}</a>
</div>
@endif
</div>

View file

@ -20,8 +20,8 @@
<div class="col-6 col-lg-4 card mb-3 border-0 bg-transparent">
<div class="row g-0">
<div class="col-12 d-flex justify-content-center align-items-center">
<img class="img-fluid" src="{{ Vite::asset($item->picto) }}" title="{{ $item->picto_title }}"
alt="{{ $item->picto_alt }}">
<img class="img-fluid" src="{{ asset($item->picture) }}" title="{{ $item->picture_title }}"
alt="{{ $item->picture_alt }}">
</div>
<div class="col-12">
<div class="card-body text-center">
@ -29,8 +29,8 @@
<p class="card-text m-0">{{ $item->sustext }}</p>
@endif
<h5 class="card-title m-0">{{ $item->title }}</h5>
@if (isset($item->text))
<p class="card-text">{!! clean($item->text) !!}</p>
@if (isset($item->description))
<p class="card-text">{!! clean($item->description) !!}</p>
@endif
</div>
</div>

View file

@ -1,3 +1,11 @@
@php
$blockType = $reduction->block_type->value;
$position = $reduction->position->value;
$ctaUrl = $reduction->cta_url->value ?? null;
$ctaLabel = $reduction->cta_label->value ?? null;
$articleId = $reduction->article_id->value ?? null;
$reductionNews = $articleId ? \App\Models\News::find($articleId) : null;
@endphp
<section class="position-relative bg-white-wood-big py-5" id="commitments-paragraph-with-visual-complex-flex-items">
<div class="container">
<div class="row">
@ -7,20 +15,21 @@
])>
<div class="position-relative pt-md-0 mx-auto my-5">
@if (isset($picto) && !empty($picto))
<img class="img-fluid position-absolute pictogram-reversed z-1" src="{{ $picto }}" title="{{ $picto_title }}"
alt="{{ $picto_alt }}">
<img class="img-fluid position-absolute pictogram-reversed z-1" src="{{ $picto }}"
title="{{ $picto_title }}" alt="{{ $picto_alt }}">
@endif
<div class="d-flex justify-content-start align-items-start bottom-left-shadow">
<div class="line-border-small rotate-right w-100 m-lg-0 mx-auto h-fit bg-white p-2">
<img class="img-fluid line-border-small w-100" src="{{ $picture }}" title="{{ $picture_title }}"
alt="{{ $picture_alt }}">
<img class="img-fluid line-border-small w-100" src="{{ $picture }}"
title="{{ $picture_title }}" alt="{{ $picture_alt }}">
</div>
</div>
@if (isset($card_title) && !empty($card_title))
<div class="custom-card bottom-left-shadow mt-md-0 z-1 mx-auto mt-5 w-fit">
<img class="position-absolute z-2" src="{{ $card_picto }}" title="{{ $card_picto_title }}"
alt="{{ $card_picto_alt }}">
<p class="position-absolute text-light bg-secondary rotate-left h6 z-1 start-0 m-0 p-2 ps-5">
<p
class="position-absolute text-light bg-secondary rotate-left h6 z-1 start-0 m-0 p-2 ps-5">
{{ $card_sustitle }}</p>
<div class="bg-tertiary line-border-small p-4 text-center">
<h2>{{ $card_title }}</h2>
@ -36,22 +45,30 @@
<div class="col-12 col-md-6 d-flex justify-content-center align-items-center">
<div class="row d-flex justify-content-center align-items-start">
@foreach ($collection as $item)
<div class="col-6 col-md-4 d-flex justify-content-center align-items-center flex-column p-3 text-center">
<div
class="col-6 col-md-4 d-flex justify-content-center align-items-center flex-column p-3 text-center">
<div class="pictogram-content d-flex justify-content-center align-items-center">
<img class="img-fluid" src="{{ Vite::asset($item->picto) }}" title="{{ $item->picto_title }}"
alt="{{ $item->picto_alt }}">
<img class="img-fluid" src="{{ asset($item->picture) }}"
title="{{ $item->picture_title }}" alt="{{ $item->picture_alt }}">
</div>
@if (isset($item->sustitle) && !empty($item->sustitle))
<p class="m-0">{{ $item->sustitle }}</p>
@endif
<p class="h6 m-0">{{ $item->title }}</p>
@if (isset($item->subtitle) && !empty($item->subtitle))
<p class="m-0">{{ $item->subtitle }}</p>
@if (isset($item->description) && !empty($item->description))
<p class="m-0">{{ $item->description }}</p>
@endif
</div>
@endforeach
</div>
</div>
</div>
@include('fo.partials.block.reduction-card', [
'news' => $reductionNews,
'ctaUrl' => $ctaUrl ?? null,
'ctaLabel' => $ctaLabel ?? null,
'position' => $position,
'blockType' => $blockType,
])
</div>
</section>

View file

@ -20,7 +20,12 @@
(isset($reversedMobile) && $reversedMobile),
])>
<h3>{{ $title }}</h3>
<p class="mt-3">{!! $text !!}</p>
<p class="mt-3">{!! clean($text) !!}</p>
@if (isset($encart) && !empty($encart))
<div class="card-text-part d-flex justify-content-start align-items-center">
{!! $encart !!}
</div>
@endif
@if (isset($subtext) && !empty($subtext))
<p>{{ $subtext }}</p>
@endif
@ -28,7 +33,8 @@
@include('fo.partials.news-card-tiny', ['model' => $newsModel])
@endif
@if (isset($btnLabel) && !empty($btnLabel))
<a class="btn btn-{{ $btn_color }}" href="{{ $btnLink }}" title="{{ __('Accéder à la page') }}"
<a class="btn btn-{{ $btn_color }}" href="{{ $btnLink }}"
title="{{ __('Accéder à la page') }}"
aria-label="{{ __('Accéder à la page') }}">{{ $btnLabel }}</a>
@endif
</div>
@ -42,10 +48,11 @@
'ratio ratio-16x9' => isset($video_id),
])>
@if (isset($picto) && !empty($picto))
<img src="{{ $picto }}" title="{{ $picto_title }}" alt="{{ $picto_alt }}" @class([
'img-fluid position-absolute z-1',
isset($reversed) && $reversed ? 'pictogram-reversed' : 'pictogram',
])>
<img src="{{ $picto }}" title="{{ $picto_title }}" alt="{{ $picto_alt }}"
@class([
'img-fluid position-absolute z-1',
isset($reversed) && $reversed ? 'pictogram-reversed' : 'pictogram',
])>
@endif
<div class="bottom-left-shadow">
<div @class([
@ -69,8 +76,9 @@
<div class="position-relative w-100 h-100">
@if (isset($video_id) && \App\Rules\IsValidYoutubeId::validates($video_id))
@if (isset($isVideoModal) && isset($video_id) && \App\Rules\IsValidYoutubeId::validates($video_id))
<div class="position-absolute z-1 w-100 h-100 start-0 top-0 bg-transparent" data-bs-toggle="modal"
data-bs-target="#mecenat-chirurgie-video-modal" role="button"></div>
<div class="position-absolute z-1 w-100 h-100 start-0 top-0 bg-transparent"
data-bs-toggle="modal" data-bs-target="#mecenat-chirurgie-video-modal"
role="button"></div>
@endif
<div class="video-inlaid w-100 h-100" type="youtube"
href="https://www.youtube.com/embed/{{ $video_id }}?autoplay=0&loop=1">
@ -84,26 +92,36 @@
</div>
@if (!isset($video_id))
<img class="img-fluid line-border-small w-100" src="{{ $picture }}" title="{{ $picture_title }}"
alt="{{ $picture_alt }}">
<img class="img-fluid line-border-small w-100" src="{{ $picture }}"
title="{{ $picture_title }}" alt="{{ $picture_alt }}">
@endif
</div>
</div>
@if (isset($card_title) && !empty($card_title))
<div class="custom-card bottom-left-shadow mt-md-0 z-1 mx-auto mt-5 w-fit">
<img class="position-absolute z-2" src="{{ $card_picto }}" title="{{ $card_picto_title }}"
alt="{{ $card_picto_alt }}">
<p class="position-absolute text-light bg-secondary rotate-left h6 z-1 start-0 m-0 p-2 ps-5">
{{ $card_sustitle }}</p>
<div class="bg-tertiary line-border-small p-4 text-center">
<h2>{{ $card_title }}</h2>
<p class="h6 m-0">{{ $card_subtitle }}</p>
<p class="m-0">{{ $card_text }}</p>
<p class="m-0">{!! clean($card_text) !!}</p>
</div>
</div>
@endif
</div>
</div>
</div>
@php
$blockType = $reduction->block_type->value;
$position = $reduction->position->value;
$ctaUrl = $reduction->cta_url->value ?? null;
$ctaLabel = $reduction->cta_label->value ?? null;
$articleId = $reduction->article_id->value ?? null;
$reductionNews = $articleId ? \App\Models\News::find($articleId) : null;
@endphp
@include('fo.partials.block.reduction-card', [
'news' => $reductionNews,
'ctaUrl' => $ctaUrl ?? null,
'ctaLabel' => $ctaLabel ?? null,
'position' => $position,
'blockType' => $blockType,
])
</div>
</section>

View file

@ -28,6 +28,7 @@
window.__SYSTEM._routes['fo.news.index'] = '{{ route('fo.news.index') }}';
window.__SYSTEM._routes['bo.sections.approaches.json-paginate'] = '{{ route('bo.sections.approaches.json-paginate') }}';
window.__SYSTEM._routes['bo.categories.approaches.json-paginate'] = '{{ route('bo.categories.approaches.json-paginate') }}';
window.__SYSTEM._routes['bo.news.json-paginate'] = '{{ route('bo.news.json-paginate') }}';
// FO
window.__SYSTEM._routes['fo.home.products'] = '{{ route('fo.home.products', ['legend_slug' => 'LEGENDSLUG']) }}';

View file

@ -23,6 +23,7 @@ use App\Models\NewsCategory;
use App\Models\NewsClub;
use App\Models\NewsCorporate;
use App\Models\Offer;
use App\Models\Picto;
use App\Models\Product;
use App\Models\ProductType;
use App\Models\Range;
@ -835,3 +836,8 @@ Breadcrumbs::for('bo.sell_items.duplicate', function (Generator $trail) {
$trail->parent('bo.sell_items.index');
$trail->push(Str::ucfirst(trans('crud.actions.duplicate')));
});
// * PICTOS
Breadcrumbs::for('bo.pictos.edit', function (Generator $trail, Picto $picto) {
$trail->push(Str::ucfirst(trans('crud.actions.edit')), route('bo.pictos.edit', $picto));
});

View file

@ -2,6 +2,7 @@
use App\Http\Controllers\Bo\ActivityLogController;
use App\Http\Controllers\Bo\ApproachController;
use App\Http\Controllers\Bo\BlockController;
use App\Http\Controllers\Bo\DemarcheSlideController;
use App\Http\Controllers\Bo\BrandAdvantageController;
use App\Http\Controllers\Bo\CategoryApproachesController;
@ -21,6 +22,7 @@ use App\Http\Controllers\Bo\NewsClubController;
use App\Http\Controllers\Bo\NewsController;
use App\Http\Controllers\Bo\NewsCorporateController;
use App\Http\Controllers\Bo\OfferController;
use App\Http\Controllers\Bo\PictoController;
use App\Http\Controllers\Bo\ProductController;
use App\Http\Controllers\Bo\ProductTypeController;
use App\Http\Controllers\Bo\ProfessionController;
@ -37,6 +39,8 @@ use App\Http\Controllers\Bo\UserController;
use App\Http\Controllers\Bo\VideoController;
use App\Http\Controllers\Controller;
use App\Http\Controllers\Bo\SectionApproachController;
use App\Http\Controllers\Bo\SectionPageController;
use App\Models\SectionPage;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
@ -93,10 +97,16 @@ Route::middleware(['throttle:web', 'admin-secure'])->prefix('bo')->name('bo.')->
->withoutMiddleware('htmlsanitize')
->middleware('htmlsanitize:content,data');
Route::get('/pages/common_section', [StaticPageController::class, 'getCommonSection'])->name('static_page.sections.edit');
Route::patch('/pages/common_section/update', [StaticPageController::class, 'updateCommonSection'])
->name('static_page.sections.update')
->withoutMiddleware('htmlsanitize');
// COMMUN SECTION FOR COMMITMENTS PAGES
Route::get('/pages/engagements/common_section', [StaticPageController::class, 'getCommitmentsCommonSection'])->name('static_page.commitmments.sections.edit');
Route::patch('/pages/engagements/common_section/update', [StaticPageController::class, 'updateCommitmentsCommonSection'])
->name('static_page.commitmments.sections.update')
->withoutMiddleware('htmlsanitize');
// * MENU NODE DATA
Route::resource('menu_node_data', MenuNodeDataController::class)
// * Nasty hack to prevent unwanted modifications form singular lib
@ -508,6 +518,8 @@ Route::middleware(['throttle:web', 'admin-secure'])->prefix('bo')->name('bo.')->
NewsController::class,
'duplicate'
])->name('news.duplicate');
Route::get('/news/json/paginate', [NewsController::class, 'jsonPaginateModels'])
->name('news.json-paginate');
// * NEWS CATEGORIES.
Route::resource('news_categories', NewsCategoryController::class);
@ -602,6 +614,36 @@ Route::middleware(['throttle:web', 'admin-secure'])->prefix('bo')->name('bo.')->
Route::get('/activity_logs/user/{user:id}', [ActivityLogController::class, 'index'])
->name('activity_logs.user');
});
// SECTION PAGES
Route::patch('/sections/{sectionPage}', [SectionPageController::class, 'update'])
->name('sections.update')
->withoutMiddleware('htmlsanitize')
->middleware('htmlsanitize:content,data');
Route::post('/sections/{section}/toggle-visibility', [SectionPageController::class, 'toggleVisibility'])
->name('sections.toggleVisibility');
// BLOCKS
Route::put('/blocks/{block}', [BlockController::class, 'update'])
->withoutMiddleware('htmlsanitize')
->middleware('htmlsanitize:content,data')
->name('blocks.update');
// PICTOS
Route::resource('pictos', PictoController::class)
->withoutMiddleware('htmlsanitize')
->middleware('htmlsanitize:content');
Route::post('/pictos/save-picture-size', [PictoController::class, 'savePictureSize'])->name('pictos.savePictureSize');
Route::patch('/pictos/{picto}/change-order/{direction}', [
PictoController::class,
'changeOrder'
])->where('direction', 'up|down')->name('pictos.change-order');
Route::get('/flash', function () {
return response()->json([
'html' => view('modules.flash-messages')->render()
]);
});
});
// * Others routes
Route::post('theme/set', [Controller::class, 'setTheme'])->name('theme.set');

View file

@ -2,6 +2,7 @@
use App\Http\Controllers\Fo\BreedingController;
use App\Http\Controllers\Fo\BreederController;
use App\Http\Controllers\Fo\CommitmentController;
use App\Http\Controllers\Fo\ConsentController;
use App\Http\Controllers\Fo\ContactController;
use App\Http\Controllers\Fo\ExportController;
@ -129,7 +130,7 @@ Route::middleware(['throttle:web'])->name('fo.')->group(function () {
Route::view('/elever-durablement', 'fo.pages.commitments.raise')->name('raise');
Route::view('/bien-nourrir-les-francais', 'fo.pages.commitments.eat')->name('eat');
Route::view('/favoriser-le-mieux-vivre-ensemble', 'fo.pages.commitments.life')->name('life');
Route::view('/respecter-la-terre', 'fo.pages.commitments.respect')->name('respect');
Route::get('/respecter-la-terre', [CommitmentController::class, 'getRestpectEarthPage'])->name('respect');
});
// * BREEDING