feat: ✨ implement carousel CRUD + add fields to parity section + set fields as nullable on parity and formation sections

This commit is contained in:
Corentin Huet 2026-03-12 10:13:33 +01:00
parent 17b35bb20c
commit 5d77c5d158
28 changed files with 1752 additions and 74 deletions

View file

@ -0,0 +1,222 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Carousels\StoreCarouselRequest;
use App\Http\Requests\Bo\Carousels\UpdateCarouselRequest;
use App\Lib\Helpers\FileStorageHelper;
use App\Models\Carousel;
use App\Models\Section;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class CarouselController extends Controller
{
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(Carousel::class);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View
*/
public function index(Request $request): \Illuminate\Contracts\View\View
{
/** @var \Illuminate\Database\Eloquent\Builder $carouselModels */
$carouselModels = Carousel::query()->with('section');
$sectionModels = Section::query()->whereHas('carousels')->get();
$selectedSection = intval($request->section);
$carouselModels = $carouselModels->when($selectedSection, function ($query) use ($selectedSection) {
$query->where('section_id', $selectedSection);
});
/** @var string $search Search field */
$search = $request->input('search');
if ($search) {
$this->searchQuery(
$carouselModels,
$search,
null,
'title',
);
}
$searchFields = \implode(', ', [
Str::ucFirst(trans('validation.attributes.title')),
]);
/** Sort columns with a query */
$this->sortQuery($carouselModels);
/** Custom pagination */
$carouselModels = $this->paginate($carouselModels);
return view('bo.pages.carousels.index', compact(
'carouselModels',
'sectionModels',
'selectedSection',
'search',
'searchFields'
));
}
/**
* Show the form for creating a new resource.
*
* @param \App\Models\Carousel $carousel
* @return \Illuminate\Contracts\View\View
*/
public function create(Carousel $carousel): \Illuminate\Contracts\View\View
{
$sectionModels = Section::all();
return view('bo.pages.carousels.create', [
'carouselModel' => $carousel,
'sectionModels' => $sectionModels
]);
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Bo\Carousels\StoreCarouselRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(StoreCarouselRequest $request): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request) {
$carousel = new Carousel();
$carousel->fill($request->validated());
if (!Arr::exists($request->validated(), 'media')) {
$carousel->media = [];
}
// Set the new pictures files in the pictures array.
collect($request->only(['media.pictures.*.path']))
->each(
function (mixed $value, string $attribute) use ($carousel) {
foreach ($value['pictures']['*']['path'] as $index => $file) {
if ($file instanceof \Illuminate\Http\UploadedFile) {
$carousel->setAttribute(
"{$attribute}->pictures->{$index}->path",
FileStorageHelper::storeFile($carousel, $file, true)
);
}
}
}
);
if ($carousel->saveOrFail()) {
return redirect()->route('bo.carousels.edit', $carousel)
->with('success', trans('crud.messages.has_been_created', [
'model' => Str::ucfirst(trans_choice('models.classes.carousel', 1))
]));
}
return redirect()->back()
->with('error', trans('crud.messages.cannot_be_created', [
'model' => Str::ucfirst(trans_choice('models.classes.carousel', 1))
]));
});
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\Carousel $carousel
* @return \Illuminate\Contracts\View\View
*/
public function edit(Carousel $carousel): \Illuminate\Contracts\View\View
{
$sectionModels = Section::all();
return view('bo.pages.carousels.edit', [
'carouselModel' => $carousel,
'sectionModels' => $sectionModels
]);
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\Bo\Carousels\UpdateCarouselRequest $request
* @param \App\Models\Carousel $carousel
* @return \Illuminate\Http\RedirectResponse
*/
public function update(UpdateCarouselRequest $request, Carousel $carousel): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request, $carousel) {
$carousel->fill($request->validated());
if (!Arr::exists($request->validated(), 'media')) {
$carousel->media = [];
}
// Set the new pictures files in the pictures array.
collect($request->only(['media.pictures.*.path']))
->each(
function (mixed $value, string $attribute) use ($carousel) {
foreach ($value['pictures']['*']['path'] as $index => $file) {
if ($file instanceof \Illuminate\Http\UploadedFile) {
$carousel->setAttribute(
"{$attribute}->pictures->{$index}->path",
FileStorageHelper::storeFile($carousel, $file, true)
);
}
}
}
);
if ($carousel->saveOrFail()) {
return redirect()->route('bo.carousels.edit', $carousel)
->with('success', trans('crud.messages.has_been_updated', [
'model' => Str::ucfirst(trans_choice('models.classes.carousel', 1))
]));
}
return redirect()->route('bo.carousels.edit', $carousel)
->with('error', trans('crud.messages.cannot_be_updated', [
'model' => Str::ucfirst(trans_choice('models.classes.carousel', 1))
]));
});
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Carousel $carousel
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(Carousel $carousel): \Illuminate\Http\RedirectResponse
{
if ($carousel->deleteOrFail()) {
return redirect()->route('bo.carousels.index')
->with('success', trans('crud.messages.has_been_deleted', [
'model' => Str::ucfirst(trans_choice('models.classes.carousel', 1))
]));
}
return redirect()->back()
->with('error', trans('crud.messages.cannot_be_deleted', [
'model' => Str::ucfirst(trans_choice('models.classes.carousel', 1))
]));
}
/**
* Duplicate the specified resource.
*
* @param \App\Models\Carousel $carousel
* @return \Illuminate\Contracts\View\View
*/
public function duplicate(Carousel $carousel): \Illuminate\Contracts\View\View
{
return $this->create($carousel->replicate());
}
}

View file

@ -11,6 +11,7 @@ use App\Http\Controllers\Controller;
use App\Http\Requests\Fo\ListenFormRequest;
use App\Mail\SendConfirmationMail;
use App\Mail\SendContactMail;
use App\Models\Carousel;
use App\Models\HistoricalMilestone;
use App\Models\JobVideo;
use App\Models\Network;
@ -266,15 +267,16 @@ class StaticPageController extends Controller
*/
public function hire(): \Illuminate\Contracts\View\View
{
$staticPageModel = StaticPage::query()->where('type', StaticPageType::we_hire)->first();
$sectionModels = Section::query()->where('page_id', $staticPageModel->id)->get();
$videoModels = JobVideo::query()->inRandomOrder()->simplePaginate(4);
$testimonialModels = Testimonial::query()->where('type', TypeEnum::hire)->get();
$compagnyLifeSection = $sectionModels->where('section', PageSectionEnum::hire_life)->first();
$formationSection = $sectionModels->where('section', PageSectionEnum::hire_formation)->first();
$testimonialSection = $sectionModels->where('section', PageSectionEnum::hire_grow)->first();
$paritySection = $sectionModels->where('section', PageSectionEnum::hire_parity)->first();
$findSection = $sectionModels->where('section', PageSectionEnum::hire_find)->first();
$staticPageModel = StaticPage::query()->where('type', StaticPageType::we_hire)->first();
$sectionModels = Section::query()->where('page_id', $staticPageModel->id)->get();
$videoModels = JobVideo::query()->inRandomOrder()->simplePaginate(4);
$testimonialModels = Testimonial::query()->where('type', TypeEnum::hire)->get();
$compagnyLifeSection = $sectionModels->where('section', PageSectionEnum::hire_life)->first();
$formationSection = $sectionModels->where('section', PageSectionEnum::hire_formation)->first();
$testimonialSection = $sectionModels->where('section', PageSectionEnum::hire_grow)->first();
$paritySection = $sectionModels->where('section', PageSectionEnum::hire_parity)->first();
$findSection = $sectionModels->where('section', PageSectionEnum::hire_find)->first();
$carouselSectionParity = Carousel::query()->where('section_id', PageSectionEnum::hire_parity)->where('published', true)->get();
$navLinks = Sticky::query()->where('type', StaticPageType::we_hire)->get();
return view('fo.pages.hire', \compact(
@ -287,6 +289,7 @@ class StaticPageController extends Controller
'testimonialSection',
'paritySection',
'findSection',
'carouselSectionParity',
'navLinks'
));
}

View file

@ -0,0 +1,127 @@
<?php
namespace App\Http\Requests\Bo\Carousels;
use App\Models\Section;
use App\Rules\IsMaxChildrenReached;
use App\Rules\WysiwygLength;
use App\Traits\Requests\HasPicture;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Arr;
use Illuminate\Validation\Rule;
class StoreCarouselRequest extends FormRequest
{
use HasPicture;
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return auth('backend')->check();
}
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation(): void
{
if ($this->input('media.pictures')) {
foreach ($this->input('media.pictures') as $index => $pictureArray) {
$this->mergePicture("media.pictures.$index.path");
}
}
$this->merge([
'published' => $this->boolean('published'),
]);
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
$rules = [
'title' => 'required|string|between:3,255',
'section_id' => [
'required',
Rule::exists('sections', 'id'),
new IsMaxChildrenReached(new Section(), 'carousels', 5)
],
'media' => 'required|array',
'media.pictures.*.alt' => 'sometimes|string|between:3,255',
'media.pictures.*.title' => 'sometimes|string|between:3,255',
'media.pictures.*.legend' => [
'sometimes',
'string',
new WysiwygLength(3, 255),
],
'published' => 'required|boolean',
];
if ($this->input('media')) {
if (Arr::exists($this->input('media'), 'pictures')) {
foreach ($this->input('media')['pictures'] as $pictureIndex => $pictureArray) {
$rules = array_merge(
$rules,
[
"media.pictures.$pictureIndex.alt" => 'required|string|between:3,255',
"media.pictures.$pictureIndex.title" => 'required|string|between:3,255',
"media.pictures.$pictureIndex.legend" => [
'sometimes',
'string',
new WysiwygLength(3, 255),
],
],
$this->pictureRules("media.pictures.$pictureIndex.path", 500, 500, 500, 500, false),
);
}
} //end if
} //end if
return $rules;
}
/**
* Get custom attributes for validator errors.
*
* @return array
*/
public function attributes(): array
{
$attributes = [
'title' => trans('validation.attributes.title'),
];
if ($this->input('media') && array_key_exists('pictures', $this->input('media'))) {
foreach ($this->input('media.pictures') as $index => $pictureArray) {
$attributes = array_merge(
$attributes,
[
"media.pictures.$index.alt" => trans(':field :inter:model', [
'field' => trans('validation.custom.alt_attribute'),
'inter' => trans('validation.custom.of_the.vowel'),
'model' => sprintf('%s n°%d', trans('validation.custom.picture'), $index + 1),
]),
"media.pictures.$index.title" => trans(':field :inter:model', [
'field' => trans('validation.custom.title_attribute'),
'inter' => trans('validation.custom.of_the.vowel'),
'model' => sprintf('%s n°%d', trans('validation.custom.picture'), $index + 1),
]),
"media.pictures.$index.legend" => trans(':field :inter:model', [
'field' => trans('validation.custom.legend'),
'inter' => trans('validation.custom.of_the.vowel'),
'model' => sprintf('%s n°%d', trans('validation.custom.picture'), $index + 1),
]),
],
);
} //end foreach
} //end if
return $attributes;
}
}

View file

@ -0,0 +1,7 @@
<?php
namespace App\Http\Requests\Bo\Carousels;
class UpdateCarouselRequest extends StoreCarouselRequest
{
}

117
app/Models/Carousel.php Normal file
View file

@ -0,0 +1,117 @@
<?php
namespace App\Models;
use App\Lib\Helpers\FileStorageHelper;
use App\Lib\Helpers\ToolboxHelper;
use App\Traits\Models\ActivityLog;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property-read int $id Id.
* @property string $title Title.
* @property array $media Medias of the carousel.
* @property-read \Carbon\Carbon $created_at Created date.
* @property-read \Carbon\Carbon $updated_at Updated date.
*
* @property-read \App\Models\ActivityLog $activityLogs Activities logs One-to-many relationship.
*/
class Carousel extends Model
{
use ActivityLog;
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'section_id',
'title',
'media',
'order',
'published',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'media' => 'array',
'published' => 'boolean',
];
/**
* Perform any actions required after the model boots.
*
* @return void
*/
protected static function booted(): void
{
static::creating(function (self $model) {
$otherCarousels = self::query()
->whereRelation('section', 'id', $model->section->id)
->count();
if ($otherCarousels > 4) {
throw new \RuntimeException('Creating more than 5 carousels is not allowed');
}
self::setOrder($model);
});
static::updated(function (self $model) {
FileStorageHelper::removeOldFiles(
data_get(ToolboxHelper::getOriginal($model, 'media'), 'pictures.*.path', ['']),
data_get($model->media, 'pictures.*.path', [''])
);
});
static::deleted(function (self $model) {
FileStorageHelper::removeOldFiles(
data_get($model->media, 'pictures.*.path', ['']),
['']
);
self::setOrder($model, true);
});
}
// * METHODS
/**
* Set order after the last element of the list.
*
* @param self $model
* @param boolean $isDeletion
* @return void
*/
private static function setOrder(self $model, bool $isDeletion = false): void
{
$maxOrder = \intval(self::query()->where('section_id', $model->section_id)->max('order'));
if ($isDeletion) {
$request = Carousel::query()
->where('section_id', $model->section_id)
->whereNot('id', $model->id)
->whereBetween('order', [$model->order, $maxOrder]);
$request->decrement('order', 1);
return;
}
$model->order = $maxOrder + 1;
}
// * RELATIONSHIPS
/**
* Get the sections that owns the Carousel
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function section(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(Section::class);
}
}

View file

@ -486,4 +486,14 @@ class Section extends Model
{
return $this->belongsTo(StaticPage::class);
}
/**
* Get the sections that own the carousel.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function carousels(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Carousel::class);
}
}

View file

@ -0,0 +1,98 @@
<?php
namespace App\Policies;
use App\Enums\Users\RoleEnum;
use App\Models\User;
use App\Policies\Rules\UserStaticRules;
class CarouselPolicy
{
/**
* Determine whether the user can view any models.
*
* @param \App\Models\User $authUser
* @return boolean
*/
public function viewAny(User $authUser): bool
{
return UserStaticRules::atLeastRole($authUser, RoleEnum::admin);
}
/**
* 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 models.
*
* @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): 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,59 @@
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Database\Eloquent\Model;
/**
* Check if the max count of children element is reached.
*
* @package App\Rules
*/
class IsMaxChildrenReached implements ValidationRule
{
/**
* Construct method.
*
* @param Model $parentModel The parent Model.
* @param string $relationName The name of the relation (used in whereRelation()).
* @param integer $max The max count of children allowed.
* @param integer|null $idToIgnore The id of the children model to ignore.
* @return void
*/
public function __construct(
private Model $parentModel,
private string $relationName,
private int $max,
private int|null $idToIgnore = null
) {
}
/**
* Run the validation rule.
*
* @param string $attribute
* @param mixed $value
* @param \Closure(string):\Illuminate\Translation\PotentiallyTranslatedString $fail
* @return void
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceBeforeLastUsed
* @phpcs:disable Squiz.Commenting.FunctionComment.IncorrectTypeHint
*/
public function validate(string $attribute, mixed $value, \Closure $fail): void
{
$children = $this->parentModel->where('id', $value)->first()->{$this->relationName};
if (!is_null($this->idToIgnore)) {
$children = $children->where('id', '!==', $this->idToIgnore);
}
if ($children->count() >= $this->max) {
$parentName = str($this->parentModel->getTable())->singular();
$childrenName = str($this->parentModel->{$this->relationName}()->getModel()->getTable())->singular();
$fail(trans('crud.messages.rule_max_reached', [
'parent' => str(trans_choice('models.classes.' . $parentName, 1))->title(),
'max' => $this->max,
'children' => str(trans_choice('models.classes.' . $childrenName, 1))->title()->plural()
]));
}
}
}

View file

@ -0,0 +1,242 @@
<?php
use App\Enums\Sections\PageSectionEnum;
use App\Models\Section;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Collection;
return new class extends Migration
{
private function registerMacros(): void
{
Collection::macro('insertAfter', function ($target, $value, $new_key = null) {
if (is_int($target)) {
$this->items = array_merge(array_slice($this->items, 0, $target + 1), [$value], array_slice($this->items, $target + 1));
return;
}
foreach ($this->items as $key => $old) {
$new[$key] = $old;
if ($key === $target && is_null($new_key)) {
$new[] = $value;
} elseif ($key === $target) {
$new[$new_key] = $value;
}
}
$this->items = $new;
});
Collection::macro('insertBefore', function ($target, $value, $new_key = null) {
if (is_int($target)) {
$this->items = array_merge(array_slice($this->items, 0, $target), [$value], array_slice($this->items, $target));
return;
}
foreach ($this->items as $key => $old) {
if ($key === $target && is_null($new_key)) {
$new[] = $value;
} elseif ($key === $target) {
$new[$new_key] = $value;
}
$new[$key] = $old;
}
$this->items = $new;
});
Collection::macro('replaceItem', function ($target, $value, $new_key = null) {
$new = [];
foreach ($this->items as $key => $old) {
if ($key === $target) {
$new[$new_key ?? $key] = $value;
} else {
$new[$key] = $old;
}
}
$this->items = $new;
});
}
/**
* Run the migrations.
*/
public function up(): void
{
$this->registerMacros();
$hireParitySection = Section::query()->where('section', PageSectionEnum::hire_parity)->first();
if ($hireParitySection) {
$data = collect($hireParitySection->data);
$contentImage = collect(Section::emptyPictureBloc(
[],
'',
'',
'',
1400,
788,
false,
'Image n°1'
))->first();
$data->replaceItem(
"bloc_image_n1",
$contentImage
);
$contentTitle2 = collect(Section::emptyStringBloc(
[],
'Titre de la section n°2',
'L\'humain au cœur de nos préoccupations',
true
))->first();
$data->insertBefore(
"bloc_contenu_n2_content",
$contentTitle2,
"bloc_title_n2"
);
$data->forget("bloc_image_n2");
$contentTitle3 = collect(Section::emptyStringBloc(
[],
'Titre de la section n°3',
'Ensemble, on passe de bons moments',
true
))->first();
$data->insertBefore(
"bloc_contenu_n3_content",
$contentTitle3,
"bloc_title_n3"
);
$contentTitle4 = collect(Section::emptyStringBloc(
[],
'Titre de la section n°4',
'Évènements conviviaux organisés par les sites :',
true
))->first();
$data->insertAfter(
"bloc_contenu_n3_content",
$contentTitle4,
"bloc_title_n4"
);
$contentDesc = collect(Section::emptyWysiwygBloc(
[],
'Contenu n°4',
clean(\fake()->randomHtml()),
))->first();
$data->insertAfter(
"bloc_title_n4",
$contentDesc,
"bloc_contenu_n4_content"
);
$hireParitySection->data = $data->all();
$hireParitySection->saveQuietly();
}
$hireFormationSection = Section::query()->where('section', PageSectionEnum::hire_formation)->first();
if ($hireFormationSection) {
$data = collect($hireFormationSection->data);
$contentImage = collect(Section::emptyPictureBloc(
[],
'',
'',
'',
1400,
788,
false,
'Image n°1'
))->first();
$data->replaceItem(
"bloc_image_n1",
$contentImage
);
$hireFormationSection->data = $data->all();
$hireFormationSection->saveQuietly();
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
$this->registerMacros();
$hireParitySection = Section::query()->where('section', PageSectionEnum::hire_parity)->first();
if ($hireParitySection) {
$data = collect($hireParitySection->data)->except(
"bloc_title_n2",
"bloc_title_n3",
"bloc_title_n4",
"bloc_contenu_n4_content",
);
$contentImage = collect(Section::emptyPictureBloc(
[],
'storage/modelfiles/sections/25/09/19/c2/20241013-103605-68cd8a6285dac.jpg',
'Mobilisation de l\'ensemble de nos équipes lors de la campagne Octobre Rose 2023',
'Mobilisation de l\'ensemble de nos équipes lors de la campagne Octobre Rose 2023',
1400,
788,
true,
'Image n°1'
))->first();
$data->replaceItem(
"bloc_image_n1",
$contentImage
);
$contentImage2 = collect(Section::emptyPictureBloc(
[],
'storage/modelfiles/sections/25/09/19/c1/dsc01088-68cd8c5dc85bd.jpg',
'Animations sommeils : Gerer le sommeil quand on est en horaires décalés',
'Animations sommeils : Gerer le sommeil quand on est en horaires décalés',
1400,
788,
true,
'Image n°2'
))->first();
$data->insertBefore(
"bloc_contenu_n2_content",
$contentImage2,
"bloc_image_n2"
);
$hireParitySection->data = $data->all();
$hireParitySection->saveQuietly();
}
$hireFormationSection = Section::query()->where('section', PageSectionEnum::hire_formation)->first();
if ($hireFormationSection) {
$data = collect($hireFormationSection->data);
$contentImage = collect(Section::emptyPictureBloc(
[],
'',
'',
'',
1400,
788,
true,
'Image n°1'
))->first();
$data->replaceItem(
"bloc_image_n1",
$contentImage
);
$hireFormationSection->data = $data->all();
$hireFormationSection->saveQuietly();
}
}
};

View file

@ -0,0 +1,32 @@
<?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('carousels', function (Blueprint $table) {
$table->id();
$table->foreignId('section_id')->constrained('sections')->comment('Id of the section associated');
$table->string('title')->comment('Title of the carousel');
$table->json('media')->comment('0-5 images + alt/title/legend');
$table->integer('order')->comment('Position of the carousels');
$table->boolean('published')->comment('Is the carousel published ?');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('carousels');
}
};

View file

@ -247,7 +247,7 @@ class SectionSeeder extends Seeder
new \SplFileInfo(\resource_path(
'../database/factories/assets/sections/default-picture-1400x788.png'
))
), fake()->sentence(), fake()->sentence(), 1400, 788, true, 'Bannière');
), fake()->sentence(), fake()->sentence(), 1400, 788, false, 'Bannière');
$data = array_merge(
$data,
$section->emptyWysiwygBloc([], "Contenu n°1", clean(\fake()->randomHtml()))
@ -257,21 +257,31 @@ class SectionSeeder extends Seeder
new \SplFileInfo(\resource_path(
'../database/factories/assets/sections/default-picture-1400x788.png'
))
), fake()->sentence(), fake()->sentence(), 1400, 788, true, 'Image n°1'));
), fake()->sentence(), fake()->sentence(), 1400, 788, false, 'Image n°1'));
$data = array_merge(
$data,
$section->emptyStringBloc([], "Titre de la section n°2", \fake()->text(50))
);
$data = array_merge(
$data,
$section->emptyWysiwygBloc([], "Contenu n°2", clean(\fake()->randomHtml()))
);
$data = array_merge($data, $section->emptyPictureBloc([], FileStorageHelper::storeFile(
new Section(),
new \SplFileInfo(\resource_path(
'../database/factories/assets/sections/default-picture-1400x788.png'
))
), fake()->sentence(), fake()->sentence(), 1400, 788, true, 'Image n°2'));
$data = array_merge(
$data,
$section->emptyStringBloc([], "Titre de la section n°3", \fake()->text(50))
);
$data = array_merge(
$data,
$section->emptyWysiwygBloc([], "Contenu n°3", clean(\fake()->randomHtml()))
);
$data = array_merge(
$data,
$section->emptyStringBloc([], "Titre de la section n°4", \fake()->text(50))
);
$data = array_merge(
$data,
$section->emptyWysiwygBloc([], "Contenu n°4", clean(\fake()->randomHtml()))
);
$data = array_merge($data, $section->emptyButtonBooleanBloc([], 'On Bouge Ensemble'));
return $data;

View file

@ -42,6 +42,7 @@ return [
'right' => 'Vous n\'avez pas les droits pour accéder à cette partie !',
'theme_updated' => 'Le thème a été mis à jour !',
'user_unpublished' => 'Vous ne pouvez pas désactiver votre propre compte !',
'rule_max_reached' => 'L\'élement :parent ne peut pas avoir plus de :max :children',
],
'minicrud' => [
'visibility_label' => 'Visibilité',

View file

@ -17,6 +17,7 @@ return [
'network' => 'réseau|réseaux',
'aisle' => 'rayon',
'brand' => 'marque',
'carousel' => 'carrousel|carrousels'
'sticky' => 'menu d\'ancre|menu d\'ancres'
]
];

View file

@ -29,3 +29,4 @@
// * PARTIALS
@import "./partials/slider";
@import "./partials/testimonial-card";
@import "./partials/carousel";

View file

@ -0,0 +1,31 @@
@use "sass:color";
.swiper-carousels {
.swiper-carousels-pagination {
bottom: 2rem;
left: 5%;
@include media-breakpoint-up(lg) {
top: auto;
bottom: .5rem;
left: 0;
width: 100%;
}
}
.swiper-pagination-bullet {
width: 3rem;
max-width: 10rem;
height: .5rem;
background-color: color.adjust($color-dark, $alpha: -0.8);
&:hover {
cursor: pointer;
}
}
.swiper-pagination-bullet-active {
background-color: $color-primary;
}
}

View file

@ -0,0 +1,306 @@
<template>
<div
:class="`list-item-component list-item-${inputParameters.name}`"
ref="listItem"
>
<table
v-if="itemParameters.list.length > 0"
class="table text-center table-sm table-hover table-bordered m-0"
ref="tableElement"
>
<thead>
<tr>
<th
v-for="(column, columnIndex) of itemParameters.columns"
:key="columnIndex"
scope="col"
>
{{ column[1] }}
</th>
<th
scope="col"
v-if="buttonsDisplayParameters.remove"
class="table-action"
>
<!-- Laisser vide -->
</th>
</tr>
</thead>
<tbody class="table-group-divider">
<tr
v-for="(item, itemIndex) of itemParameters.list"
:key="itemIndex"
>
<td
class="align-top"
v-for="(column, columnIndex) of itemParameters.columns"
:key="columnIndex"
>
<div
v-if="itemParameters.type[columnIndex] == 'boolean'"
class="form-check form-switch d-flex justify-content-center align-items-center p-0"
>
<input
class="form-check-input m-0"
type="checkbox"
role="switch"
:name="`${inputParameters.name}[${String(itemIndex)}][${column[0]}]`"
:checked="Boolean(item[column[0] as keyof typeof item])"
>
</div>
<div
v-else-if="itemParameters.type[columnIndex] == 'wysiwyg'"
:class="(getErrorMessage(itemIndex, column) !== null) ? 'is-invalid' : ''"
>
<CkEditorComponent
:name="`${inputParameters.name}[${String(itemIndex)}][${column[0]}]`"
:placeholder="inputParameters.placeholder[columnIndex]"
:dbtext="String(item[column[0] as keyof typeof item] ?? '')"
:required="true"
:errors="allErrors"
:editor-config="wysiwygConfig"
/>
</div>
<div
v-else-if="itemParameters.type[columnIndex] == 'picture'"
:class="(getErrorMessage(itemIndex, column) !== null) ? 'is-invalid' : ''"
>
<ImageInputComponent
:id="'picture-input-' + id + itemIndex"
:required="true"
:show-labels="true"
:name="`${inputParameters.name}[${String(itemIndex)}][${column[0]}]`"
:helper="trans.methods.__('crud.minicrud.dimensions_must_be', {
widthpx: (imageSizes[0] ?? String(500)) + 'px',
heightpx: (imageSizes[1] ?? String(500)) + 'px',
})"
:width="imageSizes[0] ?? 500"
:height="imageSizes[1] ?? 500"
:value="item[column[0] as keyof typeof item] ? String(item[column[0] as keyof typeof item]) : undefined"
:preview="false"
:browse-event="false"
/>
</div>
<input
v-else
:class="['form-control form-control-sm', (getErrorMessage(itemIndex, column) !== null) ? 'is-invalid' : '']"
:name="`${inputParameters.name}[${String(itemIndex)}][${column[0]}]`"
:type="itemParameters.type[columnIndex]"
v-model="item[column[0] as keyof typeof item]"
:placeholder="inputParameters.placeholder[columnIndex]"
:min="itemParameters.type[columnIndex] == 'number' ? '0' : undefined"
>
<small class="text-danger m-0">
{{ getErrorMessage(itemIndex, column) }}
</small>
</td>
<td
v-if="buttonsDisplayParameters.remove"
class="align-bottom"
>
<button
@click.prevent="removeLine(itemIndex)"
class="btn btn-sm btn-danger"
:title="trans.methods.__('Supprimer la ligne')"
data-bs-toggle="tooltip"
>
<i class="fa-solid fa-trash" />
</button>
</td>
</tr>
</tbody>
</table>
<div v-else>
<p>{{ trans.methods.__("Aucun élément renseigné(s).") }}</p>
</div>
<div
v-if="buttonsDisplayParameters.add"
class="text-center mt-1"
>
<button
@click.prevent="addLine"
class="btn btn-sm btn-primary"
type="button"
:title="trans.methods.__('Ajouter une ligne')"
data-bs-toggle="tooltip"
>
<i class="fa-solid fa-plus" />
</button>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, defineOptions, onMounted, reactive, ref, useAttrs } from "vue";
import { Tooltips } from "./../../modules/bsTooltips";
import parseLaravelErrorMessages from "./../../modules/parseLaravelErrorMessages";
import trans from "./../../modules/trans";
import CkEditorComponent from "./CkEditorComponent.vue";
import ImageInputComponent from "./ImageInputComponent.vue";
defineOptions({
name: "ListItemComponent"
});
// * ATTRIBUTES
const attrs = useAttrs();
// * REFS
const listItem = ref<HTMLDivElement|null>(null);
const tableElement = ref<HTMLTableElement|null>(null);
// * DATA
const tooltips = ref<Tooltips | null>(null);
const allErrors = ref<LaravelValidationErrors>({});
const wysiwygConfig = ref<string>();
const id = ref<string>();
/** Input parameters. */
const inputParameters = reactive<{
name: string,
placeholder: Array<string>,
}>({
name: "",
placeholder: [""],
});
/** Item parameters. */
const itemParameters = reactive<{
list: Array<Object>,
columns: Array<string[]>,
limits: Array<number>,
type: Array<string>,
}>({
list: [{}],
columns: [[""]],
limits: [0, 0],
type: [""],
});
/** Image Sizes. */
let imageSizes = [] as Array<number|null>;
/** Buttons parameters. */
const buttonsDisplayParameters = reactive<{
add: boolean,
remove: boolean,
}>({
add: true,
remove: true,
});
// * COMPUTED
const isUsedWithProps = computed<boolean>(() => attrs.json === undefined);
// * LIFECYCLE
onMounted(() => {
const json = String(attrs.json ?? "{}"),
data = JSON.parse(json);
if (!isUsedWithProps.value) {
/** Input parameters. */
inputParameters.name = data.inputName;
inputParameters.placeholder = data.inputPlaceholder;
/** Item parameters. */
itemParameters.list = data.itemList;
itemParameters.columns = data.itemColumns;
itemParameters.limits = data.itemLimit;
itemParameters.type = data.itemType;
imageSizes = data.imageSizes;
/** Global parameters */
allErrors.value = data.errors;
id.value = data.id ?? "";
/** Ckeditor specific parameter */
wysiwygConfig.value = String(data.wysiwygConfig);
}
setDefaultLines();
checkLengthList();
initTooltips();
});
// * METHODS
/**
* Set default line(s)
* according to the minimum limit defined.
*/
function setDefaultLines(): void {
if (itemParameters.list.length < 1) {
for (let index = 0; index < itemParameters.limits[0]; index++) {
addLine();
}
}
}
/**
* Add a new line to the <table>,
* add a new element to the array,
* show remove line button.
*/
function addLine(): void {
itemParameters.list.push({});
if (buttonsDisplayParameters.remove == false) {
buttonsDisplayParameters.remove = true;
}
checkLengthList();
tooltips.value?.closeBootstrapTooltip();
initTooltips();
}
/**
* Remove a specific line from the <table>,
* remove a specific element to the array,
* show adding line button.
*/
function removeLine(itemIndex: number): void {
itemParameters.list.splice(itemIndex, 1);
if (buttonsDisplayParameters.add == false) {
buttonsDisplayParameters.add = true;
}
checkLengthList();
tooltips.value?.closeBootstrapTooltip();
initTooltips();
}
/**
* Check the length of the list,
* update variables accordingly.
*/
function checkLengthList(): void {
/** Length minimum */
if (itemParameters.list.length <= itemParameters.limits[0]) {
buttonsDisplayParameters.remove = false;
}
/** Length maximum */
if (itemParameters.list.length >= itemParameters.limits[1]) {
buttonsDisplayParameters.add = false;
}
}
/** Return error message or null. */
function getErrorMessage(itemIndex: number, column: string[]): string|null {
return parseLaravelErrorMessages.methods.parseValidationInput(
`${inputParameters.name}.${String(itemIndex)}.${column[0]}`,
allErrors.value
);
}
/** Initialise all tooltips in the component. */
function initTooltips(): void {
setTimeout(() => {
tooltips.value = Tooltips.make({
type: "dom",
elements: listItem.value!.querySelectorAll("[data-bs-toggle=\"tooltip\"]")
});
}, 500);
}
</script>
<style lang="scss">
.list-item-component .table-action {
width: 2rem;
}
.list-item-component .table {
table-layout: fixed;
}
</style>

View file

@ -346,6 +346,42 @@ document.addEventListener("DOMContentLoaded", function () {
},
},
},
"swiper-carousels": {
modules: [Pagination],
grabCursor: true,
slidesPerView: 1,
spaceBetween: 15,
centeredSlides: true,
initialSlide: 0,
speed: 750,
pagination: {
el: ".swiper-carousels-pagination",
clickable: true,
renderBullet: function (index: number, className: string) {
return "<button class=\"btn btn-tertiary rounded-0 border-0 w-100 mx-2 p-0 " + className + "\" title=\"Aller à la slide " + (index + 1) + "\"></button>";
},
},
breakpoints: {
576: {
slidesPerView: 1.5,
},
768: {
slidesPerView: 2,
},
992: {
slidesPerView: 2.5,
},
},
on: {
beforeInit(swiper: Swiper) {
const slideCount = swiper.el.querySelectorAll(".swiper-slide").length;
if (slideCount !== 2) {
swiper.params.initialSlide = Math.floor(slideCount / 2);
}
}
}
},
};
const swiperVol = document.getElementsByClassName("swiper") as HTMLCollectionOf<HTMLElement>;

View file

@ -109,7 +109,7 @@
{{-- WE HIRE --}}
<li class="nav-item">
<div class="dropdown">
<a class="nav-link @if (url()->current() === route('bo.static_pages.edit', $hireStaticPageModel->type->value()) || $isActiveHire || request()->routeIs('bo.job_video.*') || request()->query('search') === $respectSlides->label() || request()->routeIs('bo.testimonials.*')) active @endif dropdown-toggle" data-bs-toggle="dropdown" type="button"
<a class="nav-link @if (url()->current() === route('bo.static_pages.edit', $hireStaticPageModel->type->value()) || $isActiveHire || request()->routeIs('bo.job_video.*') || request()->query('search') === $respectSlides->label() || request()->routeIs('bo.testimonials.*') || request()->routeIs('bo.carousels.*')) active @endif dropdown-toggle" data-bs-toggle="dropdown" type="button"
aria-expanded="false">
{{ $hireStaticPageModel->type->label() }}
</a>
@ -152,6 +152,15 @@
</a>
</li>
@endcan
@can('viewAny', \App\Models\Carousel::class)
<li class="nav-item">
<a class="nav-link @if (request()->routeIs('bo.carousels.*')) active @endif" data-bs-toggle="tooltip"
href="{{ route('bo.carousels.index') }}"
title="{{ __('crud.helpers.list_all', ['model' => trans_choice('models.classes.carousel', \INF)]) }}">
{{ Str::ucfirst(trans_choice('models.classes.carousel', \INF)) }}
</a>
</li>
@endcan
@can('viewAny', \App\Models\Testimonial::class)
<li class="nav-item">
<a class="nav-link @if (request()->routeIs('bo.testimonials.*')) active @endif" data-bs-toggle="tooltip"

View file

@ -0,0 +1,36 @@
@extends('bo.layout', ['brParam' => $carouselModel])
@section('title', __('Création d\'un :model', ['model' => trans_choice('models.classes.carousel', 1)]))
@section('description', __('Création d\'un tout nouveau :model', ['model' => trans_choice('models.classes.carousel', 1)]))
@section('breadcrumb', request()->route()->getName())
@section('content')
<div class="d-flex justify-content-between flex-md-nowrap align-items-center flex-wrap pb-3">
<div class="d-flex align-items-start flex-row">
<a class="btn btn-primary text-decoration-none m-0" data-bs-toggle="tooltip" href="{{ route('bo.carousels.index') }}"
title="{{ __('crud.helpers.list_all', ['model' => trans_choice('models.classes.carousel', 1)]) }}">
<i class="fa-solid fa-arrow-left"></i>
</a>
@include('modules.breadcrumbs.breadcrumb-body', ['brParam' => $carouselModel])
</div>
<div class="mb-md-0 mb-2">
<button class="btn btn-primary" id="formSubmitClone" data-bs-toggle="tooltip" type="submit"
title="{{ __('crud.helpers.save_model', ['model' => trans_choice('models.classes.carousel', 1)]) }}">
{{ Str::ucfirst(__('crud.actions.save')) }}
</button>
</div>
</div>
<form id="create-or-edit-form" action="{{ route('bo.carousels.store') }}" method="POST" enctype="multipart/form-data">
@csrf
@include('bo.pages.carousels.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.carousel', 1)]) }}">
{{ Str::ucfirst(__('crud.actions.save')) }}
</button>
</div>
</div>
</form>
@endsection

View file

@ -0,0 +1,52 @@
@extends('bo.layout', ['brParam' => $carouselModel])
@section('title', __('Édition d\'un :model', ['model' => trans_choice('models.classes.carousel', 1)]))
@section('description', __('Édition d\'un :model déjà existant', ['model' => trans_choice('models.classes.carousel', 1)]))
@section('breadcrumb', request()->route()->getName())
@section('content')
<div class="d-flex justify-content-between flex-md-nowrap align-items-center flex-wrap pb-3">
<div class="d-flex align-items-start flex-row">
<a class="btn btn-primary text-decoration-none m-0" data-bs-toggle="tooltip" href="{{ route('bo.carousels.index') }}"
title="{{ __('crud.helpers.list_all', ['model' => trans_choice('models.classes.carousel', '*')]) }}">
<i class="fa-solid fa-arrow-left"></i>
</a>
@include('modules.breadcrumbs.breadcrumb-body', ['brParam' => $carouselModel])
</div>
<div class="mb-md-0 mb-2">
<form class="btn-group confirmDeleteTS" action="{{ route('bo.carousels.destroy', $carouselModel) }}" method="POST" novalidate>
@can('duplicate', $carouselModel)
<a class="btn btn-secondary" data-bs-toggle="tooltip" href="{{ route('bo.carousels.duplicate', ['carousel' => $carouselModel]) }}"
title="{{ Str::ucfirst(__('crud.actions.duplicate')) }}">
<i class="fa-solid fa-copy"></i>
</a>
@endcan
<button class="btn btn-primary" id="formSubmitClone" data-bs-toggle="tooltip" type="submit"
title="{{ __('crud.helpers.save_model', ['model' => trans_choice('models.classes.carousel', 1)]) }}">
{{ Str::ucfirst(__('crud.actions.save')) }}
</button>
@can('delete', $carouselModel)
@csrf
@method('DELETE')
<button class="btn btn-danger" data-bs-toggle="tooltip" type="submit" title="{{ Str::ucfirst(__('crud.actions.delete')) }}">
<i class="fa-solid fa-trash"></i>
</button>
@endcan
</form>
</div>
</div>
<form action="{{ route('bo.carousels.update', $carouselModel) }}" method="POST" enctype="multipart/form-data">
@csrf
@method('PUT')
@include('bo.pages.carousels.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.carousel', 1)]) }}">
{{ Str::ucfirst(__('crud.actions.save')) }}
</button>
</div>
</div>
</form>
@endsection

View file

@ -0,0 +1,103 @@
<div class="row">
{{-- TITLE --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="h-100 border p-3">
<legend>{{ Str::ucfirst(__('validation.attributes.title')) }}</legend>
<div class="row">
<div class="col-12">
@include('bo.partials.inputs.varchar', [
'id' => 'carouselTitle',
'label' => __(':field :inter:model', [
'field' => Str::ucfirst(__('validation.attributes.title')),
'inter' => __('validation.custom.of_the.male'),
'model' => trans_choice('models.classes.carousel', 1),
]),
'hasWordCounter' => true,
'name' => 'title',
'required' => true,
'model' => $carouselModel ?? null,
'helper' => __('validation.between.string', [
'attribute' => __(':field :inter:model', [
'field' => __('validation.attributes.title'),
'inter' => __('validation.custom.of_the.male'),
'model' => trans_choice('models.classes.carousel', 1),
]),
'min' => 3,
'max' => 100,
]),
'placeholder' => __('Un nom clair et concis'),
])
</div>
</div>
</fieldset>
</div>
{{-- SECTION_ID --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="h-100 border p-3">
<legend>{{ Str::ucfirst(trans_choice('models.classes.section', 1)) }}</legend>
<div class="row">
<div class="col-12">
@include('bo.partials.inputs.bigint-foreign-id', [
'id' => 'carouselSection',
'label' => __(':field du :model', [
'field' => Str::ucFirst(trans_choice('models.classes.section', '1')),
'model' => trans_choice('models.classes.carousel', 1),
]),
'name' => 'section_id',
'required' => true,
'model' => $carouselModel ?? null,
'associatedModels' => $sectionModels ?? null,
'foreignLabel' => 'h2_title',
'helper' => __('crud.minicrud.enum_select', [
'enum' => trans_choice('models.classes.section', '1'),
]),
])
</div>
</div>
</fieldset>
</div>
{{-- MEDIA --}}
<div class="col-12 mb-3">
<fieldset class="h-100 border p-3">
<legend>{{ str(__('validation.custom.media'))->ucFirst()->plural() }}&nbsp;*</legend>
<div class="row">
<div class="col-12">
@include('bo.partials.inputs.text-list-items', [
'id' => 'carouselMediaPictures',
'name' => 'media[pictures]',
'label' => __('Liste d\':field', ['field' => str(__('validation.attributes.image'))->plural()]),
'model' => $carouselModel ?? null,
'value' => $carouselModel->media['pictures'] ?? [],
'itemColumns' => [['path', 'Image'], ['alt', 'Texte alternatif'], ['title', 'Titre'], ['legend', 'Légende']],
'itemType' => ['picture', 'text', 'text', 'wysiwyg'],
'inputPlaceholder' => ['', 'Un texte clair et concis', 'Un texte clair et concis', 'Un texte clair et concis'],
'itemLimit' => [1, 5],
'imageSizes' => [500, 500],
'required' => true,
'helper' => __('Insérez les images liées à cet évènement ainsi que leur texte alternatif, titre et légende.'),
])
</div>
</div>
</fieldset>
</div>
{{-- PUBLISHED --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="h-100 border p-3">
<legend>{{ Str::ucfirst(__('crud.minicrud.visibility_label')) }}</legend>
<div class="row">
<div class="col-12">
@include('bo.partials.inputs.tinyint-boolean', [
'id' => 'carouselPublished',
'name' => 'published',
'model' => $carouselModel ?? null,
'label' => __('crud.minicrud.visibility_label'),
'helper' => __('crud.minicrud.visibility_helper'),
])
</div>
</div>
</fieldset>
</div>
<div class="col-12 text-center">
<p><b>{{ __('validation.custom.required_fields') }}</b></p>
</div>
</div>

View file

@ -0,0 +1,71 @@
<table class="table-hover table-fix-action m-0 table">
@if (count($carouselModels) > 0)
<thead>
@include('bo.modules.table-col-sorter', [
'cols' => [
'title' => Str::ucfirst(__('validation.attributes.title')),
'section_id' => Str::ucfirst(trans_choice('models.classes.section', 1)),
'published' => Str::ucfirst(__('validation.custom.activated')),
'updated_at' => Str::ucfirst(__('validation.attributes.updated_at')),
],
])
</thead>
<tbody>
@foreach ($carouselModels as $carouselModel)
<tr class="border-bottom">
<td class="text-center align-middle">{{ $carouselModel->title }}</td>
<td class="text-center">
<a class="btn btn-sm btn-primary" class="text-center align-middle"
href="{{ route('bo.sections.edit', $carouselModel->section) }}"
@cannot('edit', $carouselModel->section) disabled @endcannot>
{{ $carouselModel->section->h2_title }}</a>
</td>
@include('bo.modules.change-published-status', [
'routeName' => 'carousels',
'model' => $carouselModel,
])
<td class="text-center align-middle">
<span class="badge bg-secondary">{{ $carouselModel->updated_at->isoFormat('LLLL') }}</span>
</td>
<td class="text-end align-middle">
@canAny(['delete', 'duplicate', 'update'], $carouselModel)
<form class="btn-group confirmDeleteTS" action="{{ route('bo.carousels.destroy', $carouselModel) }}" method="POST" novalidate>
@can('duplicate', $carouselModel)
<a class="btn btn-sm btn-secondary" data-bs-toggle="tooltip"
href="{{ route('bo.carousels.duplicate', ['carousel' => $carouselModel]) }}"
title="{{ Str::ucfirst(__('crud.actions.duplicate')) }}">
<i class="fa-solid fa-copy"></i>
</a>
@endcan
@can('update', $carouselModel)
<a class="btn btn-sm btn-primary" data-bs-toggle="tooltip"
href="{{ route('bo.carousels.edit', ['carousel' => $carouselModel]) }}"
title="{{ Str::ucfirst(__('crud.actions.update')) }}">
<i class="fa-solid fa-pencil"></i>
</a>
@endcan
@can('delete', $carouselModel)
@csrf
@method('DELETE')
<button class="btn btn-sm btn-danger" data-bs-toggle="tooltip" type="submit"
title="{{ Str::ucfirst(__('crud.actions.delete')) }}">
<i class="fa-solid fa-trash"></i>
</button>
@endcan
</form>
@else
<span class="text-danger" data-bs-toggle="tooltip" title="{{ __('list.right') }}">
<i class="fa-solid fa-ban"></i>
</span>
@endcan
</td>
</tr>
@endforeach
</tbody>
@else
<tr>
<td class="border-0">{{ trans_choice('crud.other.no_model_found', 1, ['model' => trans_choice('models.classes.carousel', 1)]) }}
</td>
</tr>
@endif
</table>

View file

@ -0,0 +1,42 @@
@extends('bo.layout')
@section('title', Str::ucfirst(trans_choice('models.classes.carousel', 1)))
@section('description', __('Liste de tous les :model', ['model' => trans_choice('models.classes.carousel', '*')]))
@section('content')
<div class="d-flex justify-content-between flex-md-nowrap align-items-center flex-wrap">
@include('modules.breadcrumbs.breadcrumb-body')
<form id="carouselSection" action="{{ route('bo.carousels.index') }}">
<div class="input-group justify-content-end">
<select class="form-select w-fit" id="selectCarouselSection" name="section">
<option value="" @if ($selectedSection === null) selected @endif>
{{ __('Tous les carrousels') }}
</option>
@foreach ($sectionModels as $sectionModel)
<option value="{{ $sectionModel->id }}" @if ($selectedSection === $sectionModel->id) selected @endif>
{{ $sectionModel->h2_title }}
</option>
@endforeach
</select>
@push('scripts')
<script nonce="{{ $nonce }}">
document.getElementById('selectCarouselSection').addEventListener('change', function(e) {
document.getElementById('carouselSection').submit();
});
</script>
@endpush
@can('create', \App\Models\Carousel::class)
<a class="btn btn-primary float-right" data-bs-toggle="tooltip" href="{{ route('bo.carousels.create') }}"
title="{{ __('crud.helpers.create_model', ['model' => trans_choice('models.classes.carousel', 1)]) }}">
<i class="fa-solid fa-plus"></i>
</a>
@endcan
</div>
</form>
</div>
@include('bo.modules.search-bar')
<div class="table mb-3">
@include('bo.pages.carousels.index-list')
</div>
{{ $carouselModels->links() }}
@endsection

View file

@ -1,31 +1,21 @@
<div class="container p-0 m-0">
<div class="row">
<div class="col">
<label for="{{ $name }}" class="col-form-label">
<b>{{ $label }} {{ $required ? '*' : '' }}</b>
</label>
</div>
</div>
<div class="row">
<div class="col">
@php
$data = [
'itemList' => old($name, $model->{$name} ?? []) ?? [],
'itemColumns' => $itemColumns,
'itemType' => $itemType,
'itemLimit' => $itemLimit ?? [],
'inputName' => $name,
'inputPlaceholder' => $inputPlaceholder,
'header' => $header ?? false,
'errors' => $errors->getBag('default')->getMessages() ?? []
];
@endphp
<div class="list-item" data-json='@json($data)'></div>
@include('modules.input-error', [
'inputName' => $name,
'helperId' => "{$id}Help",
'helper' => $helper,
])
</div>
</div>
</div>
@php
$data = [
'id' => uniqId(),
'itemList' => old($name, $model->{$name} ?? ($value ?? [])) ?? [],
'itemColumns' => $itemColumns,
'itemType' => $itemType,
'itemLimit' => $itemLimit ?? [],
'imageSizes' => $imageSizes ?? [],
'inputName' => $name,
'inputPlaceholder' => $inputPlaceholder,
'wysiwygConfig' => $wysiwygConfig ?? 'basic',
'header' => $header ?? false,
'errors' => $errors->getBag('default')->getMessages() ?? [],
];
@endphp
<div class="list-item" data-json='@json($data)'></div>
@include('modules.input-error', [
'inputName' => $name,
'helperId' => "{$id}Help",
'helper' => $helper,
])

View file

@ -174,11 +174,14 @@
'wysiwyg' => true,
])
</div>
<div class="col-12 mt-1 text-center">
<img class="rounded-3 img-fluid" src="{{ asset($formationSection->data['bloc_image_n1']['value']) }}"
alt="{{ $formationSection->data['bloc_image_n1']['alt'] }}"
alt="{{ $formationSection->data['bloc_image_n1']['title'] }}">
</div>
@php $formationImg = $formationSection->data['bloc_image_n1'] @endphp
@if ($formationImg['value'] && $formationImg['alt'] && $formationImg['title'])
<div class="col-12 mt-1 text-center">
<img class="rounded-3 img-fluid" src="{{ asset($formationImg['value']) }}"
alt="{{ $formationImg['alt'] }}"
title="{{ $formationImg['title'] }}">
</div>
@endif
<div class="col-12 mb-1 mt-4 text-center">
<a class="btn btn-primary rounded-pill fw-bold text-initial" href="#we-testimonials"
title="{{ __('Lire nos témoignages') }}">{{ __('On témoigne') }}</a>
@ -216,11 +219,6 @@
{!! clean($formationSection->data['bloc_n3_content']['value']) !!}
</div>
</div>
<div class="col-12 mt-1 text-center">
<img class="rounded-3 img-fluid" src="{{ asset($formationSection->data['bloc_image_n2']['value']) }}"
alt="{{ $formationSection->data['bloc_image_n2']['alt'] }}"
alt="{{ $formationSection->data['bloc_image_n2']['title'] }}">
</div>
</div>
</div>
</section>
@ -292,28 +290,44 @@
</div>
<div class="row justify-content-center">
<div class="col-12 my-3">
<div class="card bg-quad-light">
<img class="card-img-top rounded-top-4 p-2" src="{{ $paritySection->data['bloc_image_n1']['value'] }}"
alt="{{ $paritySection->data['bloc_image_n1']['alt'] }}">
<div class="card-body text-center">
<p class="card-text">{{ $paritySection->data['bloc_image_n1']['title'] }}</p>
@php $parityImg = $paritySection->data['bloc_image_n1'] @endphp
@if ($parityImg['value'] && $parityImg['alt'] && $parityImg['title'])
<div class="card bg-quad-light">
<img class="card-img-top rounded-top-4 p-2" src="{{ $parityImg['value'] }}" alt="{{ $parityImg['alt'] }}">
<div class="card-body text-center">
<p class="card-text">{{ $parityImg['title'] }}</p>
</div>
</div>
</div>
<div class="ck-content my-1">
@endif
<h2 class="text-sixt fw-bold text-center my-4">
{{ $paritySection->data['bloc_title_n2']['value'] }}
</h2>
<div class="ck-content mb-4">
{!! clean($paritySection->data['bloc_contenu_n2_content']['value']) !!}
</div>
</div>
<div class="col-12 my-3">
<div class="card bg-tertiary-light">
<img class="card-img-top rounded-top-4 p-2" src="{{ $paritySection->data['bloc_image_n2']['value'] }}"
alt="{{ $paritySection->data['bloc_image_n2']['alt'] }}">
<div class="card-body text-center">
<p class="card-text">{{ $paritySection->data['bloc_image_n2']['title'] }}</p>
</div>
</div>
<div class="ck-content my-1">
@include('fo.partials.hire.carousel', [
'carousel' => $carouselSectionParity->first()
])
<h2 class="text-sixt fw-bold text-center my-4">
{{ $paritySection->data['bloc_title_n3']['value'] }}
</h2>
<div class="ck-content mb-4">
{!! clean($paritySection->data['bloc_contenu_n3_content']['value']) !!}
</div>
@include('fo.partials.hire.carousel', [
'carousel' => $carouselSectionParity->get(1)
])
<h2 class="text-sixt fw-bold text-center my-4">
{{ $paritySection->data['bloc_title_n4']['value'] }}
</h2>
<div class="ck-content mb-4">
{!! clean($paritySection->data['bloc_contenu_n4_content']['value']) !!}
</div>
@include('fo.partials.hire.carousel', [
'carousel' => $carouselSectionParity->get(2)
])
</div>
</div>
@if ($paritySection->data['bloc_button_on_bouge_ensemble']['value'])

View file

@ -0,0 +1,23 @@
@if (!empty($carousel))
<div class="row">
<div class="col-12 position-relative mb-5">
<div class="swiper swiper-carousels mt-lg-0 mt-5 overflow-visible" id="swiper-carousels">
<div class="swiper-wrapper h-fit">
@foreach ($carousel->media as $medias)
@foreach ($medias as $media)
<div class="swiper-slide">
<img class="rounded-3 w-100" src="{{ asset($media['path']) }}" alt="{{ $media['alt'] }}" title="{{ $media['title'] }}" />
<div class="ck-content mt-3 text-center">
{!! $media['legend'] !!}
</div>
</div>
@endforeach
@endforeach
</div>
<div
class="position-relative swiper-pagination swiper-carousels-pagination swiper-pagination-clickable swiper-pagination-bullets swiper-pagination-horizontal d-flex justify-content-center align-items-center mt-5 flex-nowrap px-5">
</div>
</div>
</div>
</div>
@endif

View file

@ -3,6 +3,7 @@
use App\Models\ActivityLog;
use App\Models\Aisle;
use App\Models\Brand;
use App\Models\Carousel;
use App\Models\HistoricalMilestone;
use App\Models\JobVideo;
use App\Models\Logo;
@ -278,6 +279,26 @@ Breadcrumbs::for('bo.aisles.duplicate', function (Generator $trail) {
$trail->push(Str::ucfirst(trans('crud.actions.duplicate')));
});
// * CAROUSELS
Breadcrumbs::for('bo.carousels.index', function (Generator $trail) {
$trail->push(
Str::ucfirst(trans_choice('models.classes.carousel', \INF)),
route('bo.carousels.index', ['sort_col' => 'updated_at', 'sort_way' => 'desc'])
);
});
Breadcrumbs::for('bo.carousels.create', function (Generator $trail) {
$trail->parent('bo.carousels.index');
$trail->push(Str::ucfirst(trans('crud.actions.create')), route('bo.carousels.create'));
});
Breadcrumbs::for('bo.carousels.edit', function (Generator $trail, Carousel $carousel) {
$trail->parent('bo.carousels.index');
$trail->push(Str::ucfirst(trans('crud.actions.edit')), route('bo.carousels.edit', $carousel));
});
Breadcrumbs::for('bo.carousels.duplicate', function (Generator $trail) {
$trail->parent('bo.carousels.index');
$trail->push(Str::ucfirst(trans('crud.actions.duplicate')));
});
// * BRANDS
Breadcrumbs::for('bo.brands.index', function (Generator $trail) {
$trail->push(

View file

@ -3,6 +3,7 @@
use App\Http\Controllers\Bo\ActivityLogController;
use App\Http\Controllers\Bo\AisleController;
use App\Http\Controllers\Bo\BrandController;
use App\Http\Controllers\Bo\CarouselController;
use App\Http\Controllers\Bo\DashboardController;
use App\Http\Controllers\Bo\HistoricalMilestoneController;
use App\Http\Controllers\Bo\JobVideoController;
@ -197,6 +198,19 @@ Route::middleware(['admin-secure'])->prefix('bo')
'changeCustomOrder'
])->name('aisles.change-order');
// * CAROUSELS
Route::resource('carousels', CarouselController::class)->except('show')
->withoutMiddleware('htmlsanitize')
->middleware('htmlsanitize:media');
Route::get('/carousels/{carousel}/duplicate', [
CarouselController::class,
'duplicate'
])->name('carousels.duplicate');
Route::patch('/carousels/{carousel}/change-published', [
CarouselController::class,
'changePublished'
])->name('carousels.change-published');
// * BRANDS
Route::resource('brands', BrandController::class)->except('show')
->withoutMiddleware('htmlsanitize')