feat: ✨ allow choosing between picto or image and slow down strip animation by 30%

This commit is contained in:
Corentin Huet 2026-03-05 15:31:37 +01:00
parent 4b281f8abf
commit 5b74ca5e78
12 changed files with 278 additions and 50 deletions

View file

@ -9,6 +9,7 @@ use App\Models\News;
use App\Models\Strip;
use App\Traits\Controllers\UpdateModelPublished;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
@ -131,6 +132,15 @@ class StripController extends Controller
if ($request->news_id === null) {
$strip->news()->dissociate();
}
if (!Arr::exists($request->validated(), 'picture')) {
$strip->picture = null;
$strip->picture_alt = null;
$strip->picture_title = null;
} elseif (!Arr::exists($request->validated(), 'picto')) {
$strip->picto = null;
}
if ($strip->saveOrFail()) {
return redirect()->route('bo.strips.edit', $strip)
->with('success', trans('crud.messages.has_been_updated', [

View file

@ -31,6 +31,9 @@ class StoreStripRequest extends FormRequest
$this->merge([
'published' => $this->boolean('published'),
]);
if ($this->input('picture')) {
$this->mergePicture('picture');
}
}
/**
@ -40,11 +43,7 @@ class StoreStripRequest extends FormRequest
*/
public function rules(): array
{
return [
'picto' => [
'required',
new Enum(\App\Enums\Strips\PictoEnum::class)
],
$rules = [
'text' => [
'required',
'string',
@ -55,6 +54,29 @@ class StoreStripRequest extends FormRequest
'publish_at' => 'nullable|date',
'unpublish_at' => 'nullable|date|after:publish_at',
];
if ($this->input('picture')) {
$rules = array_merge(
$rules,
[
'picture_alt' => 'required_with|string|between:3,255',
'picture_title' => 'required_with|string|between:3,255',
],
$this->pictureRules('picture', 100, 65, 100, 65, true),
);
} else {
$rules = array_merge(
$rules,
[
'picto' => [
'required',
new Enum(\App\Enums\Strips\PictoEnum::class)
],
],
);
}
return $rules;
}
/**
@ -65,12 +87,15 @@ class StoreStripRequest extends FormRequest
public function attributes(): array
{
return [
'picto' => trans('validation.custom.picto'),
'text' => trans('validation.attributes.content'),
'news_id' => trans('models.classes.news'),
'published' => trans('validation.custom.activated'),
'publish_at' => trans('validation.custom.publish_at'),
'unpublish_at' => trans('validation.custom.unpublish_at'),
'picto' => trans('validation.custom.picto'),
'picture' => trans('validation.custom.picture'),
'picture_alt' => trans('validation.custom.alt_attribute'),
'picture_title' => trans('validation.custom.title_attribute'),
'text' => trans('validation.attributes.content'),
'news_id' => trans('models.classes.news'),
'published' => trans('validation.custom.activated'),
'publish_at' => trans('validation.custom.publish_at'),
'unpublish_at' => trans('validation.custom.unpublish_at'),
];
}
}

View file

@ -3,19 +3,23 @@
namespace App\Models;
use App\Enums\Strips\PictoEnum;
use App\Lib\Helpers\FileStorageHelper;
use App\Traits\Models\ActivityLog;
use App\Traits\Models\PlanificatedStatus;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property App\Enums\Strips\PictoEnum $picto Picto.
* @property string $text WYSIWYG restrained (blod and underline only). Text.
* @property boolean $published Publication status.
* @property \Carbon\Carbon|null $publish_at Publication date.
* @property \Carbon\Carbon|null $unpublish_at Unpublication status update date.
* @property-read \Carbon\Carbon $created_at Created date.
* @property-read \Carbon\Carbon $updated_at Updated date.
* @property App\Enums\Strips\PictoEnum $picto Picto.
* @property string $picture Strip picture path.
* @property string $picture_alt Strip picture alt attribute.
* @property string $picture_title Strip picture title attribute.
* @property string $text WYSIWYG restrained (blod and underline only). Text.
* @property boolean $published Publication status.
* @property \Carbon\Carbon|null $publish_at Publication date.
* @property \Carbon\Carbon|null $unpublish_at Unpublication status update date.
* @property-read \Carbon\Carbon $created_at Created date.
* @property-read \Carbon\Carbon $updated_at Updated date.
*
* @property-read \App\Models\News $news News Belongs to relationship.
* @property-read \App\Models\ActivityLog $activityLogs Activities logs One-to-many relationship.
@ -33,6 +37,9 @@ class Strip extends Model
*/
protected $fillable = [
'picto',
'picture',
'picture_alt',
'picture_title',
'text',
'published',
'publish_at',
@ -65,6 +72,12 @@ class Strip extends Model
static::updating(function (self $model) {
self::manageFields($model);
});
static::updated(function (self $model) {
FileStorageHelper::removeOldFile($model, 'picture');
});
static::deleted(function (self $model) {
FileStorageHelper::removeFile($model, 'picture');
});
}
// * METHODS
@ -78,6 +91,9 @@ class Strip extends Model
private static function manageFields(self $model): void
{
self::makeStatusSense($model);
if ($model->picture) {
$model->picture = FileStorageHelper::storeFile($model, $model->picture, true);
}
}
// * RELATIONS

View file

@ -0,0 +1,34 @@
<?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::table('strips', function (Blueprint $table) {
$table->string('picto')->nullable()->comment('Picto file path')->change();
$table->string('picture')->nullable()->comment('Picture file path')->after('picto');
$table->string('picture_title')->nullable()->comment('Picture title')->after('picture');
$table->string('picture_alt')->nullable()->comment('Picture alt text')->after('picture_title');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('strips', function (Blueprint $table) {
$table->string('picto')->nullable(false)->change();
$table->dropColumn('picture');
$table->dropColumn('picture_title');
$table->dropColumn('picture_alt');
});
}
};

View file

@ -302,6 +302,7 @@ return [
'title' => 'titre de l\'ancre',
'link' => 'lien de l\'ancre',
],
'media' => 'média',
'anchor' => 'ancre'
],
];

View file

@ -134,51 +134,51 @@
@media (prefers-reduced-motion: no-preference) {
// Adapt the animation duration according to the number of element + 2 (the 2 duplicated)
.moving-strip:has(> :nth-child(3)) {
animation: marquee 8s linear infinite;
animation: marquee 10s linear infinite;
}
.moving-strip:has(> :nth-child(4)) {
animation: marquee 16s linear infinite;
animation: marquee 21s linear infinite;
}
.moving-strip:has(> :nth-child(5)) {
animation: marquee 24s linear infinite;
animation: marquee 27s linear infinite;
}
.moving-strip:has(> :nth-child(6)) {
animation: marquee 32s linear infinite;
animation: marquee 42s linear infinite;
}
.moving-strip:has(> :nth-child(7)) {
animation: marquee 40s linear infinite;
animation: marquee 52s linear infinite;
}
.moving-strip:has(> :nth-child(8)) {
animation: marquee 48s linear infinite;
animation: marquee 62s linear infinite;
}
.moving-strip:has(> :nth-child(9)) {
animation: marquee 56s linear infinite;
animation: marquee 73s linear infinite;
}
.moving-strip:has(> :nth-child(10)) {
animation: marquee 64s linear infinite;
animation: marquee 83s linear infinite;
}
.moving-strip:has(> :nth-child(11)) {
animation: marquee 72s linear infinite;
animation: marquee 94s linear infinite;
}
.moving-strip:has(> :nth-child(12)) {
animation: marquee 80s linear infinite;
animation: marquee 104s linear infinite;
}
.moving-strip:has(> :nth-child(13)) {
animation: marquee 88s linear infinite;
animation: marquee 114s linear infinite;
}
.moving-strip:has(> :nth-child(14)) {
animation: marquee 96s linear infinite;
animation: marquee 125s linear infinite;
}
}
}

View file

@ -13,3 +13,7 @@ import "./../modules/sweetalert";
// CUSTOM
import "./base/common";
// PAGES
import "./pages/strips";

View file

@ -0,0 +1,40 @@
document.addEventListener("DOMContentLoaded", function () {
const stripMedia = document.getElementById("stripItemMedia");
if (stripMedia === null) {
return;
}
const mediaPicto = document.getElementById("mediaSelectorPicto") as HTMLInputElement,
mediaImage = document.getElementById("mediaSelectorImage") as HTMLInputElement;
mediaPicto?.addEventListener("change", function() {
hideFormPart(".stripItemPicture");
showFormPart(".stripItemPicto");
});
mediaImage?.addEventListener("change", function() {
hideFormPart(".stripItemPicto");
showFormPart(".stripItemPicture");
});
});
function hideFormPart(id: string) {
const parts = document.querySelectorAll(id);
parts.forEach(partToHide => {
partToHide.classList.add("d-none");
const inputs = partToHide.querySelectorAll("input");
for (const input of inputs) {
input.disabled = true;
}
});
}
function showFormPart(id: string) {
const parts = document.querySelectorAll(id);
parts.forEach(partToShow => {
partToShow.classList.remove("d-none");
const inputs = partToShow.querySelectorAll("input");
for (const input of inputs) {
input.disabled = false;
}
});
}

View file

@ -0,0 +1,97 @@
<div id="stripItemMedia" 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="mediaSelectorPicto" name="mediaSelector" type="radio" autocomplete="off"
@checked(isset($stripModel->picto))>
<label class="btn btn-secondary rounded-start" for="mediaSelectorPicto">{{ __('Pictogramme') }}</label>
<input class="btn-check" id="mediaSelectorImage" name="mediaSelector" type="radio" autocomplete="off"
@checked(isset($stripModel->picture))>
<label class="btn btn-secondary" for="mediaSelectorImage">{{ __('Image') }}</label>
</div>
</div>
{{-- PICTO --}}
<div @class([
"stripItemPicto col-12 col-md-6 mb-3",
'd-none' => empty($stripModel->picto),
])>
@include('bo.partials.inputs.tinyint-enum', [
'id' => 'stripPicto',
'label' => __('Associer un pictogramme'),
'name' => 'picto',
'model' => $stripModel ?? null,
'required' => false,
'associatedModels' => \App\Enums\Strips\PictoEnum::toArray(),
'helper' => __('Sélectionnez un pictogramme dans la liste déroulante.'),
])
</div>
{{-- PICTURE --}}
<div @class([
"stripItemPicture col-12 col-md-6 mb-3",
'd-none' => empty($stripModel->picture),
])>
@include('bo.partials.inputs.varchar-picture', [
'id' => "stripItemPicture",
'name' => "picture",
'width' => 100,
'height' => 65,
'model' => $stripModel ?? null,
'value' => $stripModel->picture ?? null,
'helper' => __('crud.minicrud.dimensions_must_be', [
'width' => 100,
'height' => 65,
]),
'required' => true,
'preview' => false,
])
</div>
{{-- PICTURE_ALT --}}
<div @class([
"stripItemPicture col-12 col-md-6 mb-3",
'd-none' => empty($stripModel->picture),
])>
@include('bo.partials.inputs.varchar', [
'id' => "stripItemPictureAlt",
'label' => __(':field de l\':model', [
'field' => str(__('validation.custom.alt_attribute'))->ucFirst(),
'model' => trans_choice('models.classes.news', 1),
]),
'hasWordCounter' => true,
'name' => "picture_alt",
'required' => true,
'model' => $stripModel ?? null,
'disabled' => empty($stripModel->picture_alt),
'value' => $stripModel->picture_alt ?? '',
'helper' => __('crud.minicrud.picture_alt_helper'),
'placeholder' => __(':field de l\':model', [
'field' => str(__('validation.custom.alt_attribute'))->ucFirst(),
'model' => trans_choice('models.classes.news', 1),
]),
])
</div>
{{-- PICTURE_TITLE --}}
<div @class([
"stripItemPicture col-12 col-md-6 mb-3",
'd-none' => empty($stripModel->picture),
])>
@include('bo.partials.inputs.varchar', [
'id' => "stripItemPictureTitle",
'label' => __(':field de l\':model', [
'field' => str(__('validation.custom.title_attribute'))->ucFirst(),
'model' => trans_choice('models.classes.news', 1),
]),
'hasWordCounter' => true,
'name' => "picture_title",
'required' => true,
'model' => $stripModel ?? null,
'disabled' => empty($stripModel->picture_title),
'value' => $stripModel->picture_title ?? '',
'helper' => __('crud.minicrud.picture_title_helper'),
'placeholder' => __(':field de l\':model', [
'field' => str(__('validation.custom.title_attribute'))->ucFirst(),
'model' => trans_choice('models.classes.news', 1),
]),
])
</div>
</div>

View file

@ -1,25 +1,17 @@
<div class="row">
{{-- PICTO --}}
<div class="col-12 col-md-6 mb-3">
{{-- MEDIA --}}
<div class="col-12 mb-3">
<fieldset class="h-100 border p-3">
<legend>{{ Str::ucfirst(__('validation.custom.picto')) }}</legend>
<legend>{{ str(__('validation.custom.media'))->ucFirst()->plural() }}&nbsp;*</legend>
<div class="row">
<div class="col-12">
@include('bo.partials.inputs.tinyint-enum', [
'id' => 'stripPicto',
'label' => __('Associer un pictogramme'),
'name' => 'picto',
'model' => $stripModel ?? null,
'required' => true,
'associatedModels' => \App\Enums\Strips\PictoEnum::toArray(),
'helper' => __('Sélectionnez un pictogramme dans la liste déroulante.'),
])
@include('bo.pages.strips.form-media-part')
</div>
</div>
</fieldset>
</div>
{{-- NEWS_ID --}}
<div class="col-12 col-md-6 mb-3">
<div class="col-12 mb-3">
<fieldset class="h-100 border p-3">
<legend>{{ Str::ucfirst(trans_choice('models.classes.news', 1)) }}</legend>
<div class="row">

View file

@ -3,21 +3,25 @@
<thead>
@include('bo.modules.table-col-sorter', [
'cols' => [
'picto' => Str::ucfirst(__('validation.custom.picto')),
'media' => Str::ucfirst(__('validation.custom.media')),
'text' => Str::ucfirst(__('validation.attributes.content')),
'news_id' => Str::ucfirst(trans_choice('models.classes.news', 1)),
'published' => Str::ucfirst(__('validation.custom.activated')),
'updated_at' => Str::ucfirst(__('validation.attributes.updated_at')),
],
'ignore' => ['media'],
])
</thead>
<tbody>
@foreach ($stripModels as $stripModel)
<tr class="border-bottom list-item">
<tr class="border-bottom">
<td class="text-center align-middle">
@if ($stripModel->picto !== \App\Enums\Strips\PictoEnum::none)
<img src="{{ Vite::asset($stripModel->picto->picture()) }}" title="{{ $stripModel->picto->title() }}"
alt="{{ $stripModel->picto->alt() }}" height="50">
@if ($stripModel->picto && $stripModel->picto !== \App\Enums\Strips\PictoEnum::none)
<img src="{{ Vite::asset($stripModel->picto->picture()) }}" title="{{ $stripModel->picto->title() }}"
alt="{{ $stripModel->picto->alt() }}" height="50">
@elseif ($stripModel->picture)
<img src="{{ asset($stripModel->picture) }}" title="{{ $stripModel->picture_title }}"
alt="{{ $stripModel->picture_alt }}" height="50">
@else
{{ __('Aucun') }}
@endif

View file

@ -1,7 +1,12 @@
<div class="strip-item d-flex align-items-center px-3 py-2 text-white">
<div class="strip-item-picto d-flex justify-content-center align-items-center me-4 overflow-hidden">
<img class="object-fit-contain w-100" src="{{ Vite::asset($stripModel->picto->picture()) }}" title="{{ $stripModel->picto->title() }}"
alt="{{ $stripModel->picto->alt() }}">
@if ($stripModel->picto)
<img class="object-fit-contain w-100" src="{{ Vite::asset($stripModel->picto->picture()) }}" title="{{ $stripModel->picto->title() }}"
alt="{{ $stripModel->picto->alt() }}">
@else
<img class="object-fit-contain w-100" src="{{ asset($stripModel->picture) }}" title="{{ $stripModel->picture_title }}"
alt="{{ $stripModel->picture_alt }}">
@endif
</div>
<div class="strip-item-content">
<p>{!! clean($stripModel->text) !!}</p>