diff --git a/app/Enums/InputType.php b/app/Enums/InputType.php index 8418ddac..6d3dddbc 100644 --- a/app/Enums/InputType.php +++ b/app/Enums/InputType.php @@ -22,4 +22,5 @@ enum InputType: int case text = 12; case url = 13; case videoVimeo = 14; + case videoYoutube = 15; } diff --git a/app/Http/Middleware/SecureHeaders.php b/app/Http/Middleware/SecureHeaders.php index 07c92cd0..4ecdac6f 100644 --- a/app/Http/Middleware/SecureHeaders.php +++ b/app/Http/Middleware/SecureHeaders.php @@ -60,6 +60,7 @@ class SecureHeaders ]) . ';', "img-src * data: blob:;", 'frame-src ' . implode(' ', [ + 'https://www.youtube.com', 'https://www.google.com/recaptcha/', 'https://recaptcha.google.com/recaptcha/', 'https://www.google.com/maps/' diff --git a/app/Http/Requests/Bo/Concepts/StoreConceptRequest.php b/app/Http/Requests/Bo/Concepts/StoreConceptRequest.php index a5028919..66a99aa9 100644 --- a/app/Http/Requests/Bo/Concepts/StoreConceptRequest.php +++ b/app/Http/Requests/Bo/Concepts/StoreConceptRequest.php @@ -11,6 +11,13 @@ class StoreConceptRequest extends UpdateMiniCrudRequest { use HasPicture; + /** + * The model parameter name + * + * @var string + */ + protected $modelName = 'concept'; + /** * Determine if the user is authorized to make this request. * @@ -57,6 +64,7 @@ class StoreConceptRequest extends UpdateMiniCrudRequest 'picture_title' => 'required|min:3|max:255', ]; return \array_merge( + parent::rules(), $rules, $this->pictureRules('picture', 1512, 354, 1512, 354, false), ); diff --git a/app/Http/Requests/Bo/Sliders/StoreSliderRequest.php b/app/Http/Requests/Bo/Sliders/StoreSliderRequest.php index 7e85b792..37d5e163 100644 --- a/app/Http/Requests/Bo/Sliders/StoreSliderRequest.php +++ b/app/Http/Requests/Bo/Sliders/StoreSliderRequest.php @@ -11,6 +11,13 @@ class StoreSliderRequest extends UpdateMiniCrudRequest { use HasPicture; + /** + * The model parameter name + * + * @var string + */ + protected $modelName = 'slider'; + /** * Determine if the user is authorized to make this request. * @@ -60,6 +67,7 @@ class StoreSliderRequest extends UpdateMiniCrudRequest ]; return \array_merge( $rules, + parent::rules(), $this->pictureRules('picture', 1270, 930, 1270, 930, false), ); } diff --git a/app/Http/Requests/Bo/UpdateMiniCrudRequest.php b/app/Http/Requests/Bo/UpdateMiniCrudRequest.php index 8aed946a..7dce267f 100644 --- a/app/Http/Requests/Bo/UpdateMiniCrudRequest.php +++ b/app/Http/Requests/Bo/UpdateMiniCrudRequest.php @@ -45,6 +45,11 @@ abstract class UpdateMiniCrudRequest extends FormRequest "data.$iptName.value" => $this->compileValidate('required|numeric', $input) ]); break; + case InputType::videoYoutube->value: + $rules = array_merge($rules, [ + "data.$iptName.value" => $this->compileValidate('regex:%([^"&?\/\s]{11})%i', $input) + ]); + break; case InputType::string->value: $rules = array_merge($rules, [ "data.$iptName.value" => $this->compileValidate('required|string|min:3', $input) @@ -65,37 +70,53 @@ abstract class UpdateMiniCrudRequest extends FormRequest return $rule; }); $rules = array_merge($rules, [ - "data.$iptName.text" => $mergeRules + "data.$iptName.value" => $mergeRules ]); break; case InputType::image->value: - $ipt = $this->data[$iptName]; - $max = isset($ipt['dimensionsMax']); - $dim = collect(explode( + $required = $this->isUpdate() ? 'sometimes' : 'required'; + + $ipt = $this->data[$iptName]; + $max = isset($ipt['dimensionsMax']); + $dim = \is_string($input['dimensions']) ? collect(explode( ',', - $ipt['dimensionsMax'] ?? $input['dimensions'] + (isset($ipt['dimensionsMax']) and is_string($ipt['dimensionsMax'])) ? + $ipt['dimensionsMax'] : $input['dimensions'] ))->mapWithKeys(function ($v) { $v = explode(' ', $v); return [$v[0] => $v[1]]; - }); + }) : $input['dimensions']; + + // * Adapt dimensions + $sizes = (object)[ + 'minHeight' => $dim['height'], + 'maxHeight' => $dim['height'], + 'minWith' => $dim['width'], + 'maxWith' => $dim['width'], + ]; $rules = array_merge($rules, [ - "data.$iptName.value" => $this->compileValidate( + "data.$iptName.value" => [ + function (string $attribute, $value, callable $fail) use ($sizes) { + return ToolboxHelper::validatePicture($attribute, $value, $fail, $sizes); + } + ], + $this->compileValidate( implode('|', [ - 'required', + $required, 'image', 'mimes:png,jpg,jpeg', sprintf( 'dimensions:%s=%d,%s=%d', $max ? 'max_height' : 'height', - $dim['hauteur'], + $dim['height'], $max ? 'max_width' : 'width', - $dim['largeur'] + $dim['width'] ) ]), $input ), - "data.$iptName.alt" => $this->compileValidate('required|string|min:5', $input), - "data.$iptName.title" => $this->compileValidate('required|string|min:5', $input) + "data.$iptName.alt" => $this->compileValidate("$required|string|min:5", $input), + "data.$iptName.title" => $this->compileValidate("$required|string|min:5", $input) ]); break; case InputType::url->value: @@ -153,4 +174,14 @@ abstract class UpdateMiniCrudRequest extends FormRequest } return \implode('|', $out); } + + /** + * Is this request an update request ? + * + * @return boolean + */ + private function isUpdate(): bool + { + return $this->isMethod('PUT'); + } } diff --git a/app/Lib/Helpers/ToolboxHelper.php b/app/Lib/Helpers/ToolboxHelper.php index 7c96184a..92804d79 100644 --- a/app/Lib/Helpers/ToolboxHelper.php +++ b/app/Lib/Helpers/ToolboxHelper.php @@ -5,7 +5,9 @@ namespace App\Lib\Helpers; use Illuminate\Contracts\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Http\Request; +use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Route; +use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Validator; use Illuminate\Validation\Rule; use Illuminate\Validation\ValidationException; @@ -321,4 +323,88 @@ class ToolboxHelper throw new \RuntimeException('Cannot handle $input of type ' . \gettype($input)); } } + + /** + * Validate picture + * + * @param string $attribute + * @param string|null $value + * @param callback $fail + * @param object $sizes + * @return void + */ + public static function validatePicture(string $attribute, string|null $value, callable $fail, object $sizes) + { + if (!($useStorage = Storage::exists($value)) and !File::exists($value)) { + $fail(trans(':attribute n\'existe pas dans le système de fichier', [ + 'attribute' => $attribute + ])); + return; + } + /** @var string */ + $fileFullPath = self::getUploadedFileFullPath($value, $useStorage); + if ( + !\in_array(self::getUploadedFileMimeType($value, $useStorage), [ + 'image/jpeg', 'image/jpg', 'image/png', 'image/gif' + ]) + ) { + $fail(trans(':attribute doit être une image de type \'image/jpeg,image/jpg,image/png,image/gif\'', [ + 'attribute' => $attribute + ])); + } + if (!($dimensions = \getimagesize($fileFullPath)) or !isset($dimensions[0]) or !isset($dimensions[1])) { + $fail(trans(':attribute impossible de valider les dimensions', [ + 'attribute' => $attribute + ])); + } else { + $width = intval($dimensions[0]); + $height = intval($dimensions[1]); + + $xMessage = trans( + ":attribute la largeur doit être comprise entre {$sizes->minWith}px et {$sizes->maxWith}px" + ); + if ($sizes->minWith === $sizes->maxWith) { + $xMessage = trans(":attribute la largeur doit être de {$sizes->minWith}px"); + } + $yMessage = trans( + ":attribute la hauteur doit être comprise entre {$sizes->minWith}px et {$sizes->maxWith}px" + ); + if ($sizes->minHeight === $sizes->maxHeight) { + $yMessage = trans(":attribute la hauteur doit être de {$sizes->minWith}px"); + } + + if (($width < $sizes->minWith) or ($width > $sizes->maxWith)) { + $fail($xMessage); + } + if (($height < $sizes->minHeight) or ($height > $sizes->maxHeight)) { + $fail($yMessage); + } + } //end if + } + + /** + * Get the file full path + * + * @param string $value + * @param boolean $useStorage + * @return string + */ + private static function getUploadedFileFullPath(string $value, bool $useStorage): string + { + /** @var \Illuminate\Filesystem\FilesystemAdapter */ + $disk = Storage::disk(); + return !$useStorage ? (realpath($value) ?: $value) : $disk->path($value); + } + + /** + * Get the file mimetype + * + * @param string $value + * @param boolean $useStorage + * @return string|false + */ + private static function getUploadedFileMimeType(string $value, bool $useStorage) + { + return !$useStorage ? File::mimeType($value) : Storage::mimeType($value); + } } diff --git a/app/Models/Concept.php b/app/Models/Concept.php index 675c4e0f..ce0ffa06 100644 --- a/app/Models/Concept.php +++ b/app/Models/Concept.php @@ -199,115 +199,50 @@ class Concept extends Model string $imgTitle = '' ) { $color = $color ?: \collect(ColorsEnum::cases())->random()->value; - return [ - // * BLOC 1 - "bloc_1_title" => [ - "label" => "Bloc 1 - Title", - "inputType" => InputType::string->value, - "value" => $title, - "validate" => "required|min:4|max:100" - ], - "bloc_1_color" => [ - "label" => "Bloc 1 - Color", - "inputType" => InputType::selectEnum->value, - "value" => $color, - "validate" => ["required", "integer", new Enum(ColorsEnum::class)] - ], - "bloc_1_text" => [ - "label" => "Bloc 1 - Text", - "inputType" => InputType::text->value, - "value" => $text, - "validate" => "string|min:10" - ], - "bloc_1_image" => [ - "label" => "Bloc 1 - Image", - "dimensions" => ["width" => 1140, "height" => 795], - "inputType" => InputType::image->value, - "alt" => $imgAlt, - "title" => $imgTitle, - "value" => $img, - ], - // * BLOC 2 - "bloc_2_title" => [ - "label" => "Bloc 2 - Title", - "inputType" => InputType::string->value, - "value" => $title, - "validate" => "required|min:4|max:100" - ], - "bloc_2_color" => [ - "label" => "Bloc 2 - Color", - "inputType" => InputType::selectEnum->value, - "value" => $color, - "validate" => ["required", "integer", new Enum(ColorsEnum::class)] - ], - "bloc_2_text" => [ - "label" => "Bloc 2 - Text", - "inputType" => InputType::text->value, - "value" => $text, - "validate" => "string|min:10" - ], - "bloc_2_image" => [ - "label" => "Bloc 2 - Image", - "dimensions" => ["width" => 1140, "height" => 795], - "inputType" => InputType::image->value, - "alt" => $imgAlt, - "title" => $imgTitle, - "value" => $img, - ], - // * BLOC 3 - "bloc_3_title" => [ - "label" => "Bloc 3 - Title", - "inputType" => InputType::string->value, - "value" => $title, - "validate" => "required|min:4|max:100" - ], - "bloc_3_color" => [ - "label" => "Bloc 3 - Color", - "inputType" => InputType::selectEnum->value, - "value" => $color, - "validate" => ["required", "integer", new Enum(ColorsEnum::class)] - ], - "bloc_3_text" => [ - "label" => "Bloc 3 - Text", - "inputType" => InputType::text->value, - "value" => $text, - "validate" => "string|min:10" - ], - "bloc_3_image" => [ - "label" => "Bloc 3 - Image", - "dimensions" => ["width" => 1140, "height" => 795], - "inputType" => InputType::image->value, - "alt" => $imgAlt, - "title" => $imgTitle, - "value" => $img, - ], - // * BLOC 4 - "bloc_4_title" => [ - "label" => "Bloc 4 - Title", - "inputType" => InputType::string->value, - "value" => $title, - "validate" => "required|min:4|max:100" - ], - "bloc_4_color" => [ - "label" => "Bloc 4 - Color", - "inputType" => InputType::selectEnum->value, - "value" => $color, - "validate" => ["required", "integer", new Enum(ColorsEnum::class)] - ], - "bloc_4_text" => [ - "label" => "Bloc 4 - Text", - "inputType" => InputType::text->value, - "value" => $text, - "validate" => "string|min:10" - ], - "bloc_4_image" => [ - "label" => "Bloc 4 - Image", - "dimensions" => ["width" => 1140, "height" => 795], - "inputType" => InputType::image->value, - "alt" => $imgAlt, - "title" => $imgTitle, - "value" => $img, - ], - ]; + $data = []; + for ($i = 1; $i <= 4; $i++) { + $data["bloc_{$i}_title"] = [ + 'label' => "Bloc {$i} - Title", + 'inputType' => InputType::string->value, + 'value' => $title, + 'validate' => 'required|min:4|max:100' + ]; + $data["bloc_{$i}_color"] = [ + 'label' => "Bloc {$i} - Color", + 'inputType' => InputType::selectEnum->value, + 'value' => $color, + 'validate' => ['required', 'integer', new Enum(ColorsEnum::class)] + ]; + $data["bloc_{$i}_text"] = [ + 'label' => "Bloc {$i} - Text", + 'inputType' => InputType::text->value, + 'value' => $text, + 'validate' => 'string|min:10' + ]; + $data["bloc_{$i}_image"] = [ + 'label' => "Bloc {$i} - Image", + 'dimensions' => ['width' => 1140, 'height' => 795], + 'inputType' => InputType::image->value, + 'alt' => $imgAlt, + 'title' => $imgTitle, + 'value' => $img, + 'validate' => "required_if:data.bloc_{$i}_has_video.value,false", + ]; + + $data["bloc_{$i}_video_youtube"] = [ + 'label' => "Bloc {$i} - Video", + 'inputType' => InputType::videoYoutube->value, + 'value' => '', + 'validate' => "required_if:data.bloc_{$i}_has_video.value,true|nullable|regex:%([^\"&?\/\s]{11})%i", + 'required' => false + ]; + + $data["bloc_{$i}_has_video"] = [ + 'label' => "Bloc {$i} - Display video", + 'inputType' => InputType::boolean->value, + 'value' => false, + ]; + } //end for + return $data; } } diff --git a/app/Traits/Models/MiniCrudModel.php b/app/Traits/Models/MiniCrudModel.php index 9009cf0e..f0f3eb2c 100644 --- a/app/Traits/Models/MiniCrudModel.php +++ b/app/Traits/Models/MiniCrudModel.php @@ -151,18 +151,19 @@ trait MiniCrudModel $data[$o]['value'] = \preg_replace( '/^\/|^/', '', - FileStorageHelper::storeFile($this, $input['value']) + FileStorageHelper::storeFile($this, $input['value'], true) ); break; case InputType::selectEnum->value: case InputType::boolean->value: - case InputType::text->value: case InputType::password->value: case InputType::email->value: case InputType::number->value: - case InputType::string->value: case InputType::videoVimeo->value: + case InputType::videoYoutube->value: break; + case InputType::string->value: + case InputType::text->value: case InputType::url->value: ToolboxHelper::assertNoInternalBoUrls('content', $input['value']); break; diff --git a/app/Traits/Requests/HasPicture.php b/app/Traits/Requests/HasPicture.php index 0fa79d76..5cc57dd3 100644 --- a/app/Traits/Requests/HasPicture.php +++ b/app/Traits/Requests/HasPicture.php @@ -2,8 +2,7 @@ namespace App\Traits\Requests; -use Illuminate\Support\Facades\File; -use Illuminate\Support\Facades\Storage; +use App\Lib\Helpers\ToolboxHelper; /** * ! This assumes file system is using local storage @@ -67,7 +66,7 @@ trait HasPicture $name => [ !$nullable ? 'required' : 'nullable', function (string $attribute, $value, callable $fail) use ($sizes) { - return $this->validatePicture($attribute, $value, $fail, $sizes); + return ToolboxHelper::validatePicture($attribute, $value, $fail, $sizes); } ] ]; @@ -108,59 +107,6 @@ trait HasPicture ]; } - /** - * Validate picture - * - * @param string $attribute - * @param string|null $value - * @param callback $fail - * @param object $sizes - * @return void - * @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed - */ - private function validatePicture(string $attribute, string|null $value, callable $fail, object $sizes) - { - if (!($useStorage = Storage::exists($value)) and !File::exists($value)) { - $fail(trans(":attribute n'existe pas dans le système de fichier")); - return; - } - /** @var string */ - $fileFullPath = $this->getUploadedFileFullPath($value, $useStorage); - if ( - !\in_array($this->getUploadedFileMimeType($value, $useStorage), [ - 'image/jpeg', 'image/jpg', 'image/png', 'image/gif' - ]) - ) { - $fail(trans(":attribute doit être une image de type 'image/jpeg,image/jpg,image/png,image/gif'")); - } - if (!($dimensions = \getimagesize($fileFullPath)) or !isset($dimensions[0]) or !isset($dimensions[1])) { - $fail(trans(":attribute impossible de valider les dimensions")); - } else { - $width = intval($dimensions[0]); - $height = intval($dimensions[1]); - - $xMessage = trans( - ":attribute la largeur doit être comprise entre {$sizes->minWith}px et {$sizes->maxWith}px" - ); - if ($sizes->minWith === $sizes->maxWith) { - $xMessage = trans(":attribute la largeur doit être de {$sizes->minWith}px"); - } - $yMessage = trans( - ":attribute la hauteur doit être comprise entre {$sizes->minWith}px et {$sizes->maxWith}px" - ); - if ($sizes->minHeight === $sizes->maxHeight) { - $yMessage = trans(":attribute la hauteur doit être de {$sizes->minWith}px"); - } - - if (($width < $sizes->minWith) or ($width > $sizes->maxWith)) { - $fail($xMessage); - } - if (($height < $sizes->minHeight) or ($height > $sizes->maxHeight)) { - $fail($yMessage); - } - } //end if - } - /** * Merges products pictures fields with request data * @@ -225,30 +171,4 @@ trait HasPicture } } //end if } - - /** - * Get the file full path - * - * @param string $value - * @param boolean $useStorage - * @return string - */ - private function getUploadedFileFullPath(string $value, bool $useStorage): string - { - /** @var \Illuminate\Filesystem\FilesystemAdapter */ - $disk = Storage::disk(); - return !$useStorage ? (realpath($value) ?: $value) : $disk->path($value); - } - - /** - * Get the file mimetype - * - * @param string $value - * @param boolean $useStorage - * @return string|false - */ - private function getUploadedFileMimeType(string $value, bool $useStorage) - { - return !$useStorage ? File::mimeType($value) : Storage::mimeType($value); - } } diff --git a/package-lock.json b/package-lock.json index 76839241..95f3b3f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "eslint": "^8.35.0", "eslint-import-resolver-alias": "^1.1.2", "eslint-plugin-vue": "^9.4.0", + "fslightbox": "^3.4.1", "laravel-vite-plugin": "^0.7.2", "postcss": "^8.4.23", "postcss-discard-comments": "^5.1.2", @@ -5178,6 +5179,12 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/fslightbox": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/fslightbox/-/fslightbox-3.4.1.tgz", + "integrity": "sha512-/YkPP9jCnZMIlPuJPUo10JTCOCntU0vHeIKe0cB5ruR0ss2QCLhzxY5h24grZ2gUsF//0NXik7iGMU05RV/jcg==", + "dev": true + }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", diff --git a/package.json b/package.json index 3633c210..46cbd155 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "eslint": "^8.35.0", "eslint-import-resolver-alias": "^1.1.2", "eslint-plugin-vue": "^9.4.0", + "fslightbox": "^3.4.1", "laravel-vite-plugin": "^0.7.2", "postcss": "^8.4.23", "postcss-discard-comments": "^5.1.2", diff --git a/resources/sass/fo/base/_common.scss b/resources/sass/fo/base/_common.scss index 2bd6dce4..617e025b 100644 --- a/resources/sass/fo/base/_common.scss +++ b/resources/sass/fo/base/_common.scss @@ -1 +1,8 @@ // Empty. + +.youtube-play-button { + top: 50%; + left: 50%; + font-size: 3rem; + transform: translateX(-50%) translateY(-50%); +} diff --git a/resources/ts/fo.ts b/resources/ts/fo.ts index 62bdb641..ab3ee523 100644 --- a/resources/ts/fo.ts +++ b/resources/ts/fo.ts @@ -1,6 +1,8 @@ // ASSETS import.meta.glob(["./../assets/images/**", "./../assets/fonts/**"]); +import "fslightbox"; + // UTILITIES import "./utilities/bootstrap"; import "./utilities/fontawesome"; diff --git a/resources/ts/utilities/fontawesome.ts b/resources/ts/utilities/fontawesome.ts index 74805584..627f1c7f 100644 --- a/resources/ts/utilities/fontawesome.ts +++ b/resources/ts/utilities/fontawesome.ts @@ -39,7 +39,7 @@ import { faHeader, faCircleCheck, } from "@fortawesome/free-solid-svg-icons"; -import { faCircleXmark } from "@fortawesome/free-regular-svg-icons"; +import { faCircleXmark, faCirclePlay } from "@fortawesome/free-regular-svg-icons"; config.autoReplaceSvg = false; library.add(faEraser); @@ -83,5 +83,6 @@ library.add(faCircleXmark); library.add(faCircleNotch); library.add(faSliders); library.add(faCircleCheck); +library.add(faCirclePlay); dom.watch(); diff --git a/resources/views/bo/modules/minicrud-form-inputs.blade.php b/resources/views/bo/modules/minicrud-form-inputs.blade.php index f0aee940..42830a0c 100644 --- a/resources/views/bo/modules/minicrud-form-inputs.blade.php +++ b/resources/views/bo/modules/minicrud-form-inputs.blade.php @@ -1,15 +1,37 @@ {{-- MINI CRUD EDIT WHEN JSON IS TRANSLATABLE --}} @php $data = (isset($model->data)) ? $model->getEmptyData() : $conceptData; -if (isset($model->data)) { - ($model->data = $model->getTranslation('data', App::currentLocale())); -} @endphp @foreach ($data as $k => $input) @php $input = json_decode(json_encode($input), true); @endphp @switch(intval($input['inputType'])) + @case(intval(App\Enums\InputType::videoYoutube->value)) +