feat: ✨ add image cropper to dynamic pages

This commit is contained in:
K Muller 2025-09-26 15:08:37 +02:00 • committed by Alexis Gousseau
parent 5fb0263186
commit 3e464e6d1f
No known key found for this signature in database
GPG key ID: 4A614CB3DBBA2406
24 changed files with 1799 additions and 279 deletions

View file

@ -14,6 +14,7 @@ enum DynamicFieldTypes: int
case boolean = 4;
case url = 5;
case email = 6;
case image = 7;
/**
* Labels definition.
@ -27,6 +28,7 @@ enum DynamicFieldTypes: int
self::boolean->name => 'enums.dynamic-field-types.boolean',
self::url->name => 'enums.dynamic-field-types.url',
self::email->name => 'enums.dynamic-field-types.email',
self::image->name => 'enums.dynamic-field-types.image',
];
/**
@ -39,6 +41,7 @@ enum DynamicFieldTypes: int
self::boolean->name => 'required|boolean',
self::url->name => 'required|url:https',
self::email->name => 'required|email:rfc,strict,dns,spoof,filter',
self::image->name => 'required|file|mimes:jpg,jpeg,png,pdf|max:5120|dimensions:width=100,height=100',
];
/**

View file

@ -0,0 +1,103 @@
<?php
namespace App\Helpers;
use App\Enums\DynamicFieldTypes;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Model;
use Uwil\LaravelCommon\Helpers\FileHelper;
class DynamicPageHelper
{
/**
* Remove imgs from wysiwyg OR dynamic wysiwyg OR image input.
*
* @param Model $model
* @param bool $forceDeleteImage In forceDelete event we need to ensure all the images will be destroyed
* @return void
*/
public static function removeImgs(Model $model, $forceDeleteImage = false): void
{
$locale = config('app.locale');
$model = $model->load('config');
$currentContent = json_decode($model->getAttributes()['content'], true);
$originalContent = $model->getOriginal('content');
$currentContentLocal = is_array($currentContent[$locale]) ? $currentContent[$locale] : ["text_" => $currentContent[$locale]];
$originalContentLocal = is_array($originalContent[$locale]) ? $originalContent[$locale] : ["text_" => $originalContent[$locale]];
static::removeDynamicImages($model, $originalContentLocal, $currentContentLocal, $forceDeleteImage);
static::removeWysiwygImages($originalContentLocal, $currentContentLocal);
}
/**
* Remove imgs from image inputs.
*
* @param Model $model
* @param array $oldContent
* @param array $newContent
* @param bool $forceDeleteImage In forceDelete event we need to ensure all the images will be destroyed
* @return void
*/
protected static function removeDynamicImages(Model $model, array $oldContent, array $newContent, bool $forceDeleteImage): void
{
$prefix = DynamicFieldTypes::image->name . '_';
$allKeys = static::getDynamicFieldKeys($oldContent, $newContent, $prefix);
foreach ($allKeys as $key) {
$old = $oldContent[$key] ?? null;
$new = $newContent[$key] ?? null;
// Cas 1 : Valeurs identiques
if ((is_string($old) === is_string($new)) && $forceDeleteImage) {
// In this case we need to remove the actual file, not the old as we do in other conditions
FileHelper::removeFile($model, "content.$key");
}
// // Cas 2 : ancienne = string, nouvelle = null → suppression
if (is_string($old) && is_null($new)) {
// Action suppression
FileHelper::removeOldFile($model, "content.$key");
continue;
}
// // Cas 3 : ancienne = null, nouvelle = string → ajout
// // Cas 4 : ancienne = string, nouvelle = autre string → remplacement
if (is_string($old) && is_string($new) && $old !== $new) {
FileHelper::removeOldFile($model, "content.$key");
continue;
}
}
}
/**
* Remove imgs from wysiwygs.
*
* @param array $oldContent
* @param array $newContent
* @return void
*/
protected static function removeWysiwygImages(array $oldContent, array $newContent): void
{
$prefix = DynamicFieldTypes::text->name . '_';
static::getDynamicFieldKeys($oldContent, $newContent, $prefix)
->each(function ($key) use ($oldContent) {
$old = $oldContent[$key] ?? null;
FileHelper::removeOldModelFilesFromHtml('', $old);
});
}
/**
* Get the field keys of wysiwygs or images field input and group them.
*
* @param array $oldContent
* @param array $newContent
* @return \Illuminate\Support\Collection
*/
protected static function getDynamicFieldKeys(array $oldContent, array $newContent, string $prefix): \Illuminate\Support\Collection
{
$filter = fn($value, $key) => Str::startsWith($key, $prefix);
$old = collect($oldContent)->filter($filter);
$new = collect($newContent)->filter($filter);
return $new->keys()->merge($old->keys())->unique();
}
}

View file

@ -105,7 +105,8 @@ class DynamicPageController extends Controller
{
return DB::transaction(function () use ($request) {
$dynamicPageModel = new DynamicPage();
$dynamicPageModel->fill($request->validated());
$dynamicPageModel->inertiaFill($request->validated());
if ($dynamicPageModel->saveOrFail()) {
return redirect()->route('back.dynamic-pages.edit', ['dynamic_page' => $dynamicPageModel])
@ -147,7 +148,7 @@ class DynamicPageController extends Controller
DynamicPage $dynamic_page
): \Illuminate\Http\RedirectResponse {
return DB::transaction(function () use ($request, $dynamic_page) {
$dynamic_page->fill($request->validated());
$dynamic_page->inertiaFill($request->validated());
if ($dynamic_page->saveOrFail()) {
return redirect()->route('back.dynamic-pages.edit', ['dynamic_page' => $dynamic_page])

View file

@ -4,6 +4,7 @@ namespace App\Http\Controllers\Back;
use App\Http\Requests\Back\PageConfigs\StorePageConfigRequest;
use App\Http\Requests\Back\PageConfigs\UpdatePageConfigRequest;
use App\Models\DynamicPage;
use App\Models\PageConfig;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

View file

@ -48,6 +48,7 @@ class HandleInertiaRequests extends Middleware
'appEnv' => [
'mode' => config('app.env'),
'name' => config('app.name'),
'url' => config('app.url'),
'locale' => app()->getLocale(),
'locales' => config('app.locales'),
'fallbackLocale' => app()->getFallbackLocale(),

View file

@ -4,6 +4,7 @@ namespace App\Http\Requests\Back;
use App\Enums\DynamicFieldTypes;
use App\Models\PageConfig;
use App\Traits\Requests\HasInertiaPicture;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
@ -11,6 +12,8 @@ use Uwil\LaravelCommon\Helpers\ToolboxHelper;
abstract class UpdateDynamicFieldRequest extends FormRequest
{
use HasInertiaPicture;
/** @var \App\Models\PageConfig|null $configModel */
private $configModel = null;
@ -35,6 +38,7 @@ abstract class UpdateDynamicFieldRequest extends FormRequest
protected function prepareForValidation(): void
{
$this->configModel = PageConfig::query()->whereKey($this->input('config_id'))->firstOrFail();
$this->mergeInertiaPicture('content');
}
/**
@ -60,6 +64,7 @@ abstract class UpdateDynamicFieldRequest extends FormRequest
$mergeRules = $this->compileValidate('required|string|min:15', $input);
$mergeRules = collect($mergeRules)
->map(function ($rule) {
// TODO: check pour utiliser la rule wysiwyg length.
if (\strpos($rule, 'max:') !== false) {
$max = \intval(str_replace('max:', '', $rule));
return function ($attribute, $value, $fail) use ($max) {
@ -112,6 +117,12 @@ abstract class UpdateDynamicFieldRequest extends FormRequest
$rules = array_merge($rules, [
$ruleKey => $this->compileValidate('required|email|min:3', $input)
]);
case DynamicFieldTypes::image->value:
$defaultRules = 'required|file|mimes:jpg,jpeg,png,pdf|max:5120|dimensions:width=100,height=100';
$imgRules = $this->compileValidate($defaultRules, $input);
$rules = array_merge($rules, [
$ruleKey => $this->pictureRules($ruleKey, implode("|", $imgRules)),
]);
break;
} //end switch
} //end foreach
@ -138,32 +149,22 @@ abstract class UpdateDynamicFieldRequest extends FormRequest
/**
* Merge native validations and custom validations.
*
* @param string|array $inputTypeValidate
* @param string|array $defaultValidations
* @param array $input
* @return array
* @phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh
*/
private function compileValidate(string|array $inputTypeValidate, array $input): array
private function compileValidate(string|array $defaultValidations, array $input): array
{
// phpcs:enable
if (\is_string($inputTypeValidate)) {
$inputTypeValidate = explode('|', $inputTypeValidate);
}
/** @var array|string */
$inputValidate = \collect($input)->get('validation', $input);
$inputValidate = \is_string($inputValidate) ? explode('|', $inputValidate) : $inputValidate;
foreach ($inputValidate as $validate) {
if (!\in_array($validate, $inputTypeValidate)) {
$inputTypeValidate[] = $validate;
}
}
$newValidations = $this->mergeValidations($defaultValidations, $input);
$rules = \collect($inputTypeValidate)->unique()->sort(function ($valueA, $valueB) {
// Get unique rule and priorise important rules first.
$rules = \collect($newValidations)->unique()->sort(function ($valueA, $valueB) {
$valueA = $this->ruleHasToComeFirst($valueA);
$valueB = $this->ruleHasToComeFirst($valueB);
return $valueA - $valueB;
});
// Conditions the rules.
$hasRequire = $rules->first(fn($rule) => $rule === 'required') ? true : false;
$hasRequireCond = $rules->first(fn($rule) => \strpos($rule, 'required_') !== false) ? true : false;
$hasNullable = $rules->first(fn($rule) => \strpos($rule, 'nullable') !== false) ? true : false;
@ -173,9 +174,55 @@ abstract class UpdateDynamicFieldRequest extends FormRequest
if ($hasRequire and ($hasRequireCond or $hasNullable or $hasSometime or $hasPresent)) {
$rules = $rules->reject(fn($rule) => $rule === 'required');
}
return $rules->all();
}
/**
* Merge new validations into default validations.
*
* @param string|array $defaultValidations
* @param array $input
* @return array
*/
private function mergeValidations($defaultValidations, $input): array
{
if (\is_string($defaultValidations)) {
$defaultValidations = explode('|', $defaultValidations);
}
/** @var array|string $inputValidations */
$inputValidations = \collect($input)->get('validation', $input);
$inputValidations = \is_string($inputValidations) ? explode('|', $inputValidations) : $inputValidations;
foreach ($inputValidations as $validate) {
// Split a string by a special character.
$parts = explode(':', $validate, 2);
// Check if there is a special character.
if (count($parts) > 1) {
[$ruleName, $ruleValue] = $parts;
// Deletes any old rule that starts with “ruleName:”.
$defaultValidations = collect($defaultValidations)
->reject(function ($existingRule) use ($ruleName) {
return is_string($existingRule) && str_starts_with($existingRule, sprintf('%s:', $ruleName));
})
->values()
->all();
// Add the new rule.
$defaultValidations[] = $validate;
} else {
// Check if rule without special character already exist.
if (!in_array($validate, $defaultValidations)) {
// Add the input rule.
$defaultValidations[] = $validate;
}
}
}
return $defaultValidations;
}
/**
* Does the rule need to come first ?
*

View file

@ -3,12 +3,15 @@
namespace App\Models;
use App\Contracts\TranslatableModel;
use App\Enums\DynamicFieldTypes;
use App\Enums\DynamicPages\PageStatusEnum;
use App\Helpers\DynamicPageHelper;
use App\Helpers\TranslationHelper;
use App\Traits\Models\HasTranslations;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
@ -69,6 +72,24 @@ class DynamicPage extends Model implements TranslatableModel
'content',
];
public function inertiaFill(array $attributes)
{
if (!empty($attributes['content'])) {
$attributes['content'] = collect($attributes['content'])
->map(function ($value, $key) {
if (
Str::startsWith($key, DynamicFieldTypes::image->name . '_') &&
$value instanceof UploadedFile
) {
return FileHelper::storeFile($this, $value);
}
return $value;
})
->all();
}
parent::fill($attributes);
}
/**
* Perform any actions required after the model boots.
*
@ -87,7 +108,7 @@ class DynamicPage extends Model implements TranslatableModel
});
static::updated(function (self $model) {
self::resetCache();
self::removeImgs($model);
DynamicPageHelper::removeImgs($model);
});
static::deleted(function () {
self::resetCache();
@ -105,7 +126,7 @@ class DynamicPage extends Model implements TranslatableModel
});
static::forceDeleted(function (self $model) {
self::resetCache();
self::removeImgs($model);
DynamicPageHelper::removeImgs($model, true);
});
}
@ -128,25 +149,6 @@ class DynamicPage extends Model implements TranslatableModel
self::validateFallbackTranslations($model);
}
/**
* Remove imgs from wysiwyg OR dynamic wysiwyg.
*
* @param self $model
* @return void
*/
public static function removeImgs(self $model): void
{
$locale = config('app.locale');
$model = $model->load('config');
$currentContent = json_decode($model->getAttributes()['content'], true);
$originalContent = $model->getOriginal('content');
$currentHtml = self::flattenContent($currentContent[$locale] ?? null);
$originalHtml = self::flattenContent($originalContent[$locale] ?? null);
FileHelper::removeOldModelFilesFromHtml($currentHtml, $originalHtml);
}
/**
* Concat all wysiwyg strings in only one.
*

View file

@ -75,8 +75,11 @@ class PageConfig extends Model implements TranslatableModel
static::deleted(function (self $model) {
$model->pages()->delete();
});
static::forceDeleted(function (self $model) {
$model->pages()->forceDelete();
static::forceDeleting(function (self $model) {
$pages = $model->pages()->withTrashed()->get();
$pages->each(function ($page) {
$page->forceDelete();
});
});
}

View file

@ -0,0 +1,64 @@
<?php
namespace App\Traits\Requests;
trait HasInertiaPicture
{
/**
* Prepare the data for validation.
*
* @param string $field
* @return void
*/
protected function mergeInertiaPicture(string $field = 'content'): void
{
// Si rien à merger (convertedFiles est null ou ne contient pas le champ), on return en douceur
if (empty($this->convertedFiles) || empty($this->convertedFiles[$field])) {
return;
}
$converted = $this->convertedFiles[$field]; // Ce qu'on veut merger
// Si l'input du champ n'existe pas, on le crée avec converted
if (!$this->has($field)) {
$this->merge([$field => $converted]);
return;
}
// Si l'input existe et que converted a aussi des données => merge récursif
$original = $this->input($field);
// On vérifie que les deux sont bien des tableaux (sinon array_merge_recursive plantera dans le bueno)
if (!is_array($original)) {
$original = [$original];
}
if (!is_array($converted)) {
$converted = [$converted];
}
$merged = array_merge_recursive($original, $converted);
$this->merge([$field => $merged]);
}
protected function pictureRules(string $field = "picture", string $rules)
{
if (is_string($this->$field) && (\request()->method() === "PUT")) {
$isNullable = \strpos($rules, 'nullable');
$extensions = explode("|", explode("|mimes:", $rules)[1])[0];
$allowedExtensions = explode(',', $extensions); // Ex: "jpg,png" → ['jpg', 'png']
return [
$isNullable ? 'nullable' : 'required',
'string',
'regex:/^storage\/modelfiles\/+[a-zA-Z0-9_]*\/+[0-9]{2}\/+[0-9]{2}\/+[0-9]{2}\/+[a-zA-Z0-9]{2}\/+[a-zA-Z0-9_-]*/',
function ($attribute, $value, $fail) use ($allowedExtensions) {
$extension = pathinfo($value, PATHINFO_EXTENSION);
if (!in_array(strtolower($extension), $allowedExtensions)) {
$fail("Le fichier doit avoir une extension valide : " . implode(', ', $allowedExtensions));
}
}
];
}
return $rules;
}
}

View file

@ -21,7 +21,7 @@
"spatie/laravel-translatable": "^6.11",
"tightenco/ziggy": "^2.5",
"uwil/laravel-backend": "^3.0",
"uwil/laravel-common": "^4.1.1"
"uwil/laravel-common": "^4.1"
},
"require-dev": {
"fakerphp/faker": "^1.23",

301
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "40d7c92cb446269a04483548cf3ddc61",
"content-hash": "7b52ba13bb6f291b8488e22d72ccecbc",
"packages": [
{
"name": "24slides/laravel-saml2",
@ -1314,16 +1314,16 @@
},
{
"name": "inertiajs/inertia-laravel",
"version": "v2.0.7",
"version": "v2.0.10",
"source": {
"type": "git",
"url": "https://github.com/inertiajs/inertia-laravel.git",
"reference": "a08aea5a16fed187dcf815c9037b62cc544ab809"
"reference": "07da425d58a3a0e3ace9c296e67bd897a6e47009"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/inertiajs/inertia-laravel/zipball/a08aea5a16fed187dcf815c9037b62cc544ab809",
"reference": "a08aea5a16fed187dcf815c9037b62cc544ab809",
"url": "https://api.github.com/repos/inertiajs/inertia-laravel/zipball/07da425d58a3a0e3ace9c296e67bd897a6e47009",
"reference": "07da425d58a3a0e3ace9c296e67bd897a6e47009",
"shasum": ""
},
"require": {
@ -1378,9 +1378,9 @@
],
"support": {
"issues": "https://github.com/inertiajs/inertia-laravel/issues",
"source": "https://github.com/inertiajs/inertia-laravel/tree/v2.0.7"
"source": "https://github.com/inertiajs/inertia-laravel/tree/v2.0.10"
},
"time": "2025-09-24T16:49:48+00:00"
"time": "2025-09-28T21:21:36+00:00"
},
{
"name": "intervention/gif",
@ -1650,16 +1650,16 @@
},
{
"name": "laravel/framework",
"version": "v12.31.1",
"version": "v12.33.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "281b711710c245dd8275d73132e92635be3094df"
"reference": "124efc5f09d4668a4dc13f94a1018c524a58bcb1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/281b711710c245dd8275d73132e92635be3094df",
"reference": "281b711710c245dd8275d73132e92635be3094df",
"url": "https://api.github.com/repos/laravel/framework/zipball/124efc5f09d4668a4dc13f94a1018c524a58bcb1",
"reference": "124efc5f09d4668a4dc13f94a1018c524a58bcb1",
"shasum": ""
},
"require": {
@ -1687,7 +1687,6 @@
"monolog/monolog": "^3.0",
"nesbot/carbon": "^3.8.4",
"nunomaduro/termwind": "^2.0",
"phiki/phiki": "^2.0.0",
"php": "^8.2",
"psr/container": "^1.1.1|^2.0.1",
"psr/log": "^1.0|^2.0|^3.0",
@ -1866,7 +1865,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2025-09-23T15:33:04+00:00"
"time": "2025-10-07T14:30:39+00:00"
},
{
"name": "laravel/prompts",
@ -1990,16 +1989,16 @@
},
{
"name": "laravel/telescope",
"version": "v5.12.0",
"version": "v5.14.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/telescope.git",
"reference": "b4fb98b57f4aeb48b74f79aa7c684a20e302dacd"
"reference": "99e9487604e67424d07b83b5ab3401850d237685"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/telescope/zipball/b4fb98b57f4aeb48b74f79aa7c684a20e302dacd",
"reference": "b4fb98b57f4aeb48b74f79aa7c684a20e302dacd",
"url": "https://api.github.com/repos/laravel/telescope/zipball/99e9487604e67424d07b83b5ab3401850d237685",
"reference": "99e9487604e67424d07b83b5ab3401850d237685",
"shasum": ""
},
"require": {
@ -2053,9 +2052,9 @@
],
"support": {
"issues": "https://github.com/laravel/telescope/issues",
"source": "https://github.com/laravel/telescope/tree/v5.12.0"
"source": "https://github.com/laravel/telescope/tree/v5.14.0"
},
"time": "2025-09-18T15:08:51+00:00"
"time": "2025-10-06T14:52:49+00:00"
},
{
"name": "laravel/tinker",
@ -2125,16 +2124,16 @@
},
{
"name": "ldc/ldccrmv4client",
"version": "4.1.4",
"version": "4.1.5",
"source": {
"type": "git",
"url": "git@lab.s-web.fr:ldc-web/projets/crm-client.git",
"reference": "9aa66a8b3f62c401b0fd43616ba5fd52fe15541a"
"reference": "9725ed12914d75c39f0ee66990a1bff6c77f457c"
},
"dist": {
"type": "zip",
"url": "https://lab.s-web.fr/api/v4/projects/26/packages/composer/archives/ldc/ldccrmv4client.zip?sha=9aa66a8b3f62c401b0fd43616ba5fd52fe15541a",
"reference": "9aa66a8b3f62c401b0fd43616ba5fd52fe15541a",
"url": "https://lab.s-web.fr/api/v4/projects/26/packages/composer/archives/ldc/ldccrmv4client.zip?sha=9725ed12914d75c39f0ee66990a1bff6c77f457c",
"reference": "9725ed12914d75c39f0ee66990a1bff6c77f457c",
"shasum": ""
},
"require": {
@ -2164,21 +2163,16 @@
"license": [
"MIT"
],
"authors": [
{
"name": "Jérémy Munsch",
"email": "jeremy.munsch@ldc.fr",
"homepage": "https://jeremydev.ovh"
}
],
"description": "LDC CRM Client V4",
"homepage": "https://github.com/kwaadpepper",
"keywords": [
"CRM",
"Client",
"LDC",
"Laravel"
]
],
"support": {
"email": "digital@ldc.fr"
}
},
{
"name": "league/commonmark",
@ -3531,77 +3525,6 @@
],
"time": "2025-05-25T14:28:00+00:00"
},
{
"name": "phiki/phiki",
"version": "v2.0.4",
"source": {
"type": "git",
"url": "https://github.com/phikiphp/phiki.git",
"reference": "160785c50c01077780ab217e5808f00ab8f05a13"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phikiphp/phiki/zipball/160785c50c01077780ab217e5808f00ab8f05a13",
"reference": "160785c50c01077780ab217e5808f00ab8f05a13",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"league/commonmark": "^2.5.3",
"php": "^8.2",
"psr/simple-cache": "^3.0"
},
"require-dev": {
"illuminate/support": "^11.45",
"laravel/pint": "^1.18.1",
"orchestra/testbench": "^9.15",
"pestphp/pest": "^3.5.1",
"phpstan/extension-installer": "^1.4.3",
"phpstan/phpstan": "^2.0",
"symfony/var-dumper": "^7.1.6"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Phiki\\Adapters\\Laravel\\PhikiServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Phiki\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ryan Chandler",
"email": "support@ryangjchandler.co.uk",
"homepage": "https://ryangjchandler.co.uk",
"role": "Developer"
}
],
"description": "Syntax highlighting using TextMate grammars in PHP.",
"support": {
"issues": "https://github.com/phikiphp/phiki/issues",
"source": "https://github.com/phikiphp/phiki/tree/v2.0.4"
},
"funding": [
{
"url": "https://github.com/sponsors/ryangjchandler",
"type": "github"
},
{
"url": "https://buymeacoffee.com/ryangjchandler",
"type": "other"
}
],
"time": "2025-09-20T17:21:02+00:00"
},
{
"name": "phpoption/phpoption",
"version": "1.9.4",
@ -4927,16 +4850,16 @@
},
{
"name": "symfony/console",
"version": "v7.3.3",
"version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7"
"reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7",
"reference": "cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7",
"url": "https://api.github.com/repos/symfony/console/zipball/2b9c5fafbac0399a20a2e82429e2bd735dcfb7db",
"reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db",
"shasum": ""
},
"require": {
@ -5001,7 +4924,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v7.3.3"
"source": "https://github.com/symfony/console/tree/v7.3.4"
},
"funding": [
{
@ -5021,7 +4944,7 @@
"type": "tidelift"
}
],
"time": "2025-08-25T06:35:40+00:00"
"time": "2025-09-22T15:31:00+00:00"
},
{
"name": "symfony/css-selector",
@ -5157,16 +5080,16 @@
},
{
"name": "symfony/error-handler",
"version": "v7.3.2",
"version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/error-handler.git",
"reference": "0b31a944fcd8759ae294da4d2808cbc53aebd0c3"
"reference": "99f81bc944ab8e5dae4f21b4ca9972698bbad0e4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/error-handler/zipball/0b31a944fcd8759ae294da4d2808cbc53aebd0c3",
"reference": "0b31a944fcd8759ae294da4d2808cbc53aebd0c3",
"url": "https://api.github.com/repos/symfony/error-handler/zipball/99f81bc944ab8e5dae4f21b4ca9972698bbad0e4",
"reference": "99f81bc944ab8e5dae4f21b4ca9972698bbad0e4",
"shasum": ""
},
"require": {
@ -5214,7 +5137,7 @@
"description": "Provides tools to manage errors and ease debugging PHP code",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/error-handler/tree/v7.3.2"
"source": "https://github.com/symfony/error-handler/tree/v7.3.4"
},
"funding": [
{
@ -5234,7 +5157,7 @@
"type": "tidelift"
}
],
"time": "2025-07-07T08:17:57+00:00"
"time": "2025-09-11T10:12:26+00:00"
},
{
"name": "symfony/event-dispatcher",
@ -5466,16 +5389,16 @@
},
{
"name": "symfony/http-foundation",
"version": "v7.3.3",
"version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
"reference": "7475561ec27020196c49bb7c4f178d33d7d3dc00"
"reference": "c061c7c18918b1b64268771aad04b40be41dd2e6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/7475561ec27020196c49bb7c4f178d33d7d3dc00",
"reference": "7475561ec27020196c49bb7c4f178d33d7d3dc00",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/c061c7c18918b1b64268771aad04b40be41dd2e6",
"reference": "c061c7c18918b1b64268771aad04b40be41dd2e6",
"shasum": ""
},
"require": {
@ -5525,7 +5448,7 @@
"description": "Defines an object-oriented layer for the HTTP specification",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/http-foundation/tree/v7.3.3"
"source": "https://github.com/symfony/http-foundation/tree/v7.3.4"
},
"funding": [
{
@ -5545,20 +5468,20 @@
"type": "tidelift"
}
],
"time": "2025-08-20T08:04:18+00:00"
"time": "2025-09-16T08:38:17+00:00"
},
{
"name": "symfony/http-kernel",
"version": "v7.3.3",
"version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-kernel.git",
"reference": "72c304de37e1a1cec6d5d12b81187ebd4850a17b"
"reference": "b796dffea7821f035047235e076b60ca2446e3cf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/72c304de37e1a1cec6d5d12b81187ebd4850a17b",
"reference": "72c304de37e1a1cec6d5d12b81187ebd4850a17b",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/b796dffea7821f035047235e076b60ca2446e3cf",
"reference": "b796dffea7821f035047235e076b60ca2446e3cf",
"shasum": ""
},
"require": {
@ -5643,7 +5566,7 @@
"description": "Provides a structured process for converting a Request into a Response",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/http-kernel/tree/v7.3.3"
"source": "https://github.com/symfony/http-kernel/tree/v7.3.4"
},
"funding": [
{
@ -5663,20 +5586,20 @@
"type": "tidelift"
}
],
"time": "2025-08-29T08:23:45+00:00"
"time": "2025-09-27T12:32:17+00:00"
},
{
"name": "symfony/mailer",
"version": "v7.3.3",
"version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/mailer.git",
"reference": "a32f3f45f1990db8c4341d5122a7d3a381c7e575"
"reference": "ab97ef2f7acf0216955f5845484235113047a31d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mailer/zipball/a32f3f45f1990db8c4341d5122a7d3a381c7e575",
"reference": "a32f3f45f1990db8c4341d5122a7d3a381c7e575",
"url": "https://api.github.com/repos/symfony/mailer/zipball/ab97ef2f7acf0216955f5845484235113047a31d",
"reference": "ab97ef2f7acf0216955f5845484235113047a31d",
"shasum": ""
},
"require": {
@ -5727,7 +5650,7 @@
"description": "Helps sending emails",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/mailer/tree/v7.3.3"
"source": "https://github.com/symfony/mailer/tree/v7.3.4"
},
"funding": [
{
@ -5747,20 +5670,20 @@
"type": "tidelift"
}
],
"time": "2025-08-13T11:49:31+00:00"
"time": "2025-09-17T05:51:54+00:00"
},
{
"name": "symfony/mime",
"version": "v7.3.2",
"version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
"reference": "e0a0f859148daf1edf6c60b398eb40bfc96697d1"
"reference": "b1b828f69cbaf887fa835a091869e55df91d0e35"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mime/zipball/e0a0f859148daf1edf6c60b398eb40bfc96697d1",
"reference": "e0a0f859148daf1edf6c60b398eb40bfc96697d1",
"url": "https://api.github.com/repos/symfony/mime/zipball/b1b828f69cbaf887fa835a091869e55df91d0e35",
"reference": "b1b828f69cbaf887fa835a091869e55df91d0e35",
"shasum": ""
},
"require": {
@ -5815,7 +5738,7 @@
"mime-type"
],
"support": {
"source": "https://github.com/symfony/mime/tree/v7.3.2"
"source": "https://github.com/symfony/mime/tree/v7.3.4"
},
"funding": [
{
@ -5835,7 +5758,7 @@
"type": "tidelift"
}
],
"time": "2025-07-15T13:41:35+00:00"
"time": "2025-09-16T08:38:17+00:00"
},
{
"name": "symfony/options-resolver",
@ -6739,16 +6662,16 @@
},
{
"name": "symfony/process",
"version": "v7.3.3",
"version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "32241012d521e2e8a9d713adb0812bb773b907f1"
"reference": "f24f8f316367b30810810d4eb30c543d7003ff3b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/32241012d521e2e8a9d713adb0812bb773b907f1",
"reference": "32241012d521e2e8a9d713adb0812bb773b907f1",
"url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b",
"reference": "f24f8f316367b30810810d4eb30c543d7003ff3b",
"shasum": ""
},
"require": {
@ -6780,7 +6703,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/process/tree/v7.3.3"
"source": "https://github.com/symfony/process/tree/v7.3.4"
},
"funding": [
{
@ -6800,7 +6723,7 @@
"type": "tidelift"
}
],
"time": "2025-08-18T09:42:54+00:00"
"time": "2025-09-11T10:12:26+00:00"
},
{
"name": "symfony/psr-http-message-bridge",
@ -6887,16 +6810,16 @@
},
{
"name": "symfony/routing",
"version": "v7.3.2",
"version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/routing.git",
"reference": "7614b8ca5fa89b9cd233e21b627bfc5774f586e4"
"reference": "8dc648e159e9bac02b703b9fbd937f19ba13d07c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/routing/zipball/7614b8ca5fa89b9cd233e21b627bfc5774f586e4",
"reference": "7614b8ca5fa89b9cd233e21b627bfc5774f586e4",
"url": "https://api.github.com/repos/symfony/routing/zipball/8dc648e159e9bac02b703b9fbd937f19ba13d07c",
"reference": "8dc648e159e9bac02b703b9fbd937f19ba13d07c",
"shasum": ""
},
"require": {
@ -6948,7 +6871,7 @@
"url"
],
"support": {
"source": "https://github.com/symfony/routing/tree/v7.3.2"
"source": "https://github.com/symfony/routing/tree/v7.3.4"
},
"funding": [
{
@ -6968,7 +6891,7 @@
"type": "tidelift"
}
],
"time": "2025-07-15T11:36:08+00:00"
"time": "2025-09-11T10:12:26+00:00"
},
{
"name": "symfony/service-contracts",
@ -7055,16 +6978,16 @@
},
{
"name": "symfony/string",
"version": "v7.3.3",
"version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c"
"reference": "f96476035142921000338bad71e5247fbc138872"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/17a426cce5fd1f0901fefa9b2a490d0038fd3c9c",
"reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c",
"url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872",
"reference": "f96476035142921000338bad71e5247fbc138872",
"shasum": ""
},
"require": {
@ -7079,7 +7002,6 @@
},
"require-dev": {
"symfony/emoji": "^7.1",
"symfony/error-handler": "^6.4|^7.0",
"symfony/http-client": "^6.4|^7.0",
"symfony/intl": "^6.4|^7.0",
"symfony/translation-contracts": "^2.5|^3.0",
@ -7122,7 +7044,7 @@
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v7.3.3"
"source": "https://github.com/symfony/string/tree/v7.3.4"
},
"funding": [
{
@ -7142,20 +7064,20 @@
"type": "tidelift"
}
],
"time": "2025-08-25T06:35:40+00:00"
"time": "2025-09-11T14:36:48+00:00"
},
{
"name": "symfony/translation",
"version": "v7.3.3",
"version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
"reference": "e0837b4cbcef63c754d89a4806575cada743a38d"
"reference": "ec25870502d0c7072d086e8ffba1420c85965174"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/translation/zipball/e0837b4cbcef63c754d89a4806575cada743a38d",
"reference": "e0837b4cbcef63c754d89a4806575cada743a38d",
"url": "https://api.github.com/repos/symfony/translation/zipball/ec25870502d0c7072d086e8ffba1420c85965174",
"reference": "ec25870502d0c7072d086e8ffba1420c85965174",
"shasum": ""
},
"require": {
@ -7222,7 +7144,7 @@
"description": "Provides tools to internationalize your application",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/translation/tree/v7.3.3"
"source": "https://github.com/symfony/translation/tree/v7.3.4"
},
"funding": [
{
@ -7242,7 +7164,7 @@
"type": "tidelift"
}
],
"time": "2025-08-01T21:02:37+00:00"
"time": "2025-09-07T11:39:36+00:00"
},
{
"name": "symfony/translation-contracts",
@ -7398,16 +7320,16 @@
},
{
"name": "symfony/var-dumper",
"version": "v7.3.3",
"version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
"reference": "34d8d4c4b9597347306d1ec8eb4e1319b1e6986f"
"reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/34d8d4c4b9597347306d1ec8eb4e1319b1e6986f",
"reference": "34d8d4c4b9597347306d1ec8eb4e1319b1e6986f",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb",
"reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb",
"shasum": ""
},
"require": {
@ -7461,7 +7383,7 @@
"dump"
],
"support": {
"source": "https://github.com/symfony/var-dumper/tree/v7.3.3"
"source": "https://github.com/symfony/var-dumper/tree/v7.3.4"
},
"funding": [
{
@ -7481,7 +7403,7 @@
"type": "tidelift"
}
],
"time": "2025-08-13T11:49:31+00:00"
"time": "2025-09-11T10:12:26+00:00"
},
{
"name": "tightenco/ziggy",
@ -7726,16 +7648,16 @@
},
{
"name": "uwil/laravel-common",
"version": "4.1.1",
"version": "4.1.2",
"source": {
"type": "git",
"url": "git@lab.s-web.fr:ldc-web/projets/laravel-common.git",
"reference": "7c08c4f545307bdba8bd09835b59982094b06e49"
"reference": "ae2ef93a05ae8fbeae1799ad12f9dc5172f50f60"
},
"dist": {
"type": "zip",
"url": "https://lab.s-web.fr/api/v4/projects/215/packages/composer/archives/uwil/laravel-common.zip?sha=7c08c4f545307bdba8bd09835b59982094b06e49",
"reference": "7c08c4f545307bdba8bd09835b59982094b06e49",
"url": "https://lab.s-web.fr/api/v4/projects/215/packages/composer/archives/uwil/laravel-common.zip?sha=ae2ef93a05ae8fbeae1799ad12f9dc5172f50f60",
"reference": "ae2ef93a05ae8fbeae1799ad12f9dc5172f50f60",
"shasum": ""
},
"require": {
@ -8966,16 +8888,11 @@
},
{
"name": "phpstan/phpstan",
"version": "2.1.29",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan-phar-composer-source.git",
"reference": "git"
},
"version": "2.1.30",
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/d618573eed4a1b6b75e37b2e0b65ac65c885d88e",
"reference": "d618573eed4a1b6b75e37b2e0b65ac65c885d88e",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/a4a7f159927983dd4f7c8020ed227d80b7f39d7d",
"reference": "a4a7f159927983dd4f7c8020ed227d80b7f39d7d",
"shasum": ""
},
"require": {
@ -9020,7 +8937,7 @@
"type": "github"
}
],
"time": "2025-09-25T06:58:18+00:00"
"time": "2025-10-02T16:07:52+00:00"
},
{
"name": "phpunit/php-code-coverage",
@ -9359,16 +9276,16 @@
},
{
"name": "phpunit/phpunit",
"version": "11.5.41",
"version": "11.5.42",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "b42782bcb947d2c197aea42ce9714ee2d974b283"
"reference": "1c6cb5dfe412af3d0dfd414cfd110e3b9cfdbc3c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b42782bcb947d2c197aea42ce9714ee2d974b283",
"reference": "b42782bcb947d2c197aea42ce9714ee2d974b283",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1c6cb5dfe412af3d0dfd414cfd110e3b9cfdbc3c",
"reference": "1c6cb5dfe412af3d0dfd414cfd110e3b9cfdbc3c",
"shasum": ""
},
"require": {
@ -9440,7 +9357,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.41"
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.42"
},
"funding": [
{
@ -9464,7 +9381,7 @@
"type": "tidelift"
}
],
"time": "2025-09-24T06:32:10+00:00"
"time": "2025-09-28T12:09:13+00:00"
},
{
"name": "sebastian/cli-parser",
@ -10701,12 +10618,12 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"stability-flags": [],
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
"php": "^8.2"
},
"platform-dev": {},
"plugin-api-version": "2.6.0"
"platform-dev": [],
"plugin-api-version": "2.3.0"
}

View file

@ -93,4 +93,39 @@ return [
"missing_translations" => "The translation of the :field for the language :locales is missing.|The translation
of the :field for :locales languages are missing.",
"archives" => "archives",
"validate" => "confirm",
"cancel" => "cancel",
"crop" => "crop",
"filter" => "filter|filters",
"image_cropper" => [
"action_the_crop" => ":action the crop",
"toast" => [
"too_big" => "Max size : :maxSize. This file is too big",
"no_file" => "no file selected !",
"forbidden_file" => "file not allowed.",
"too_many" => "the max file allowed exceeded (1)",
"defaultError" => "an error occured !"
],
"validation" => [
"rules_between" => "media must do :width px width and :height px height.",
"rules_weight" => "max size : :size",
"allowed_formats" => "allowed formats : :formats"
],
"filters" => [
'zoomIn' => "zoom in",
"zoomOut" => "zoom out",
"zoom" => "zoom",
"rotation_forward" => "rotation forward",
"rotation_back" => "rotation back",
"rotation" => "rotation",
"grayscale" => "grayscale",
"sepia" => "sepia",
"saturation" => "saturation",
"contrast" => "contrast",
"brightness" => "brightness",
"reset" => "reset",
"vertical_flip" => "vertical flip",
"horizontal_flip" => "horizontal flip",
]
]
];

View file

@ -89,8 +89,45 @@ return [
en conséquence.",
"config_save_warning_list" => "Voici la liste des :models concernés:|Voici la liste des :models concernées:",
"config_archived" => "Cette configuration est actuellement archivée",
"config_complete_label" => "Label complet de l'input:",
"unsupported_type" => "Type non supporté:",
"missing_translations" => "La traduction du champs :field pour la langue :locales est manquante.|La traduction
du champs :field pour les langues :locales sont manquantes.",
"archives" => "archives",
"validate" => "valider",
"cancel" => "annuler",
"crop" => "recadrer",
"filter" => "filtre|filtres",
"image_cropper" => [
"action_the_crop" => ":action le cadrage",
"toast" => [
"too_big" => "taille maximale autorisée : :maxSize. Le fichier est trop volumineux" ,
"no_file" => "aucun fichier sélectionné !",
"forbidden_file" => "fichier non autorisé.",
"too_many" => "le nombre de fichier autorisé est dépassé (1)",
"defaultError" => "une erreur est survenue !"
],
"validation" => [
"rules_between" => "le média doit faire :width px de large et :height px de haut.",
"rules_weight" => "taille max : :size",
"allowed_formats" => "formats autorisés : :formats",
"all_formats" => "tous"
],
"filters" => [
'zoomIn' => "zoom avant",
"zoomOut" => "zoom arrière",
"zoom" => "zoom",
"rotation_forward" => "rotation avant",
"rotation_back" => "rotation arrière",
"rotation" => "rotation",
"grayscale" => "noir & Blanc",
"sepia" => "sépia",
"saturation" => "saturation",
"contrast" => "contraste",
"brightness" => "luminosité",
"reset" => "réinitialiser",
"vertical_flip" => "inverser verticalement",
"horizontal_flip" => "inverser horizontalement",
]
]
];

View file

@ -18,6 +18,7 @@ return [
'boolean' => 'Booléen',
'url' => 'Lien',
'email' => 'E-mail',
'image' => 'Image'
],
'contactFormType' => [
'collaborator' => [

104
package-lock.json generated
View file

@ -1,5 +1,5 @@
{
"name": "ldc",
"name": "srcs",
"lockfileVersion": 3,
"requires": true,
"packages": {
@ -10,6 +10,8 @@
"@dnd-kit/utilities": "^3.2.2",
"@inertiajs/react": "^2.0.17",
"@ldc-web/cookie-notice": "^6.1.1",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-slider": "^1.3.6",
"@tanstack/react-table": "^8.21.3",
"@types/pluralize": "^0.0.33",
"@types/react": "^19.1.9",
@ -19,6 +21,7 @@
"laravel-vite-plugin": "^1.3.0",
"lucide-react": "^0.503.0",
"motion": "^12.23.12",
"react-dropzone": "^14.3.8",
"reactjs-tiptap-editor": "^0.3.23",
"slugify": "^1.6.6",
"tailwind-merge": "^3.3.1",
@ -3289,6 +3292,30 @@
}
}
},
"node_modules/@radix-ui/react-progress": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz",
"integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-primitive": "2.1.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
@ -3386,6 +3413,39 @@
}
}
},
"node_modules/@radix-ui/react-slider": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz",
"integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==",
"license": "MIT",
"dependencies": {
"@radix-ui/number": "1.1.1",
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-collection": "1.1.7",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-use-layout-effect": "1.1.1",
"@radix-ui/react-use-previous": "1.1.1",
"@radix-ui/react-use-size": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
@ -5947,6 +6007,15 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/attr-accept": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz",
"integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/autoprefixer": {
"version": "10.4.21",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
@ -8600,6 +8669,18 @@
"node": "^10.12.0 || >=12.0.0"
}
},
"node_modules/file-selector": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz",
"integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==",
"license": "MIT",
"dependencies": {
"tslib": "^2.7.0"
},
"engines": {
"node": ">= 12"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@ -9950,7 +10031,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT"
},
"node_modules/js-yaml": {
@ -10558,7 +10638,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
@ -12307,7 +12386,6 @@
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
@ -12647,6 +12725,23 @@
"react": "^19.1.1"
}
},
"node_modules/react-dropzone": {
"version": "14.3.8",
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.8.tgz",
"integrity": "sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==",
"license": "MIT",
"dependencies": {
"attr-accept": "^2.2.4",
"file-selector": "^2.1.0",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">= 10.13"
},
"peerDependencies": {
"react": ">= 16.8 || 18.0.0"
}
},
"node_modules/react-image-crop": {
"version": "11.0.10",
"resolved": "https://registry.npmjs.org/react-image-crop/-/react-image-crop-11.0.10.tgz",
@ -12660,7 +12755,6 @@
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true,
"license": "MIT"
},
"node_modules/react-refresh": {

View file

@ -66,6 +66,8 @@
"@dnd-kit/utilities": "^3.2.2",
"@inertiajs/react": "^2.0.17",
"@ldc-web/cookie-notice": "^6.1.1",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-slider": "^1.3.6",
"@tanstack/react-table": "^8.21.3",
"@types/pluralize": "^0.0.33",
"@types/react": "^19.1.9",
@ -75,6 +77,7 @@
"laravel-vite-plugin": "^1.3.0",
"lucide-react": "^0.503.0",
"motion": "^12.23.12",
"react-dropzone": "^14.3.8",
"reactjs-tiptap-editor": "^0.3.23",
"slugify": "^1.6.6",
"tailwind-merge": "^3.3.1",

View file

@ -8,12 +8,14 @@ import SimpleWysiwyg from "@/back/Components/Inputs/SimpleWysiwyg";
import { DynamicFieldTypes, DynamicField } from "@/back/types/global";
import str from "@/hooks/use-string";
import useTrans from "@/hooks/use-translations";
import ImageCropper from "./Inputs/ImageCropper";
type DynamicInputsProps = {
name: string;
config: DynamicField[];
content: Record<string, string | boolean | number> | null;
setData: (name: string, value: string | boolean) => void;
setData: (name: string, value: string | boolean | File | null) => void;
reset: (name?: string) => void;
};
type MinMax = {
@ -26,6 +28,7 @@ export default function DynamicInputs({
config,
content = {},
setData,
reset,
}: DynamicInputsProps) {
const enumDynamicFieldTypes = usePage().props.enumDynamicFieldTypes as DynamicFieldTypes;
const transAssociateTo = useTrans("back.form_associate_to");
@ -81,6 +84,47 @@ export default function DynamicInputs({
return validation.split("|").includes("required");
};
/**
* Extracts a number value from a Laravel validation rule.
*
* Supports both:
* - Simple rules like "max:255"
* - Complex rules like "dimensions:width=350,height=250"
*
* @param validation Full validation string (e.g. "required|max:255|dimensions:width=350,height=250")
* @param ruleName Rule to look for (e.g. "max" or "dimensions")
* @param key Optional. If the rule contains key=value pairs (e.g. "width=350"), specify the key
* @returns number|null
*/
const extractLaravelValidationValue = (
validation: string,
ruleName: string,
key?: string
): string | null => {
const rules = validation.split("|");
const rule = rules.find(r => r.startsWith(`${ruleName}:`));
if (!rule) return null;
const [, rawValue] = rule.split(":");
if (key) {
// Complex case like dimensions:width=350,height=250
const pairs = rawValue.split(",");
const match = pairs.find(pair => pair.startsWith(`${key}=`));
if (match) {
const [, value] = match.split("=");
return value;
}
} else {
return rawValue;
}
return null;
};
return (
<div className="grid grid-cols-1 md:grid-cols-2 items-start gap-6">
{Object.entries(config).map(([id, field]) => {
@ -161,6 +205,25 @@ export default function DynamicInputs({
setData={setData}
/>
);
case enumDynamicFieldTypes.image.value:
return (
<ImageCropper
key={id}
fileName="fichier"
name={fieldName}
label={getTrans(field.label)}
setData={setData}
reset={reset}
value={String(fieldValue)}
required={extractRequiredValidation(field.validation)}
maxFileSize={extractLaravelValidationValue(field.validation, 'max')}
allowedFormats={extractLaravelValidationValue(field.validation, 'mimes')}
validationRules={{
width: extractLaravelValidationValue(field.validation, "dimensions", 'width'),
height: extractLaravelValidationValue(field.validation, "dimensions", 'height')
}}
/>
);
// Email input.
case enumDynamicFieldTypes.email.value:
return (

File diff suppressed because it is too large Load diff

View file

@ -30,13 +30,17 @@ export default function SimpleNumber({
label,
id = name,
step = 1,
limits = { min: 0, max: 1000 },
limits,
placeholder = "back.enter_number_here",
required = true,
disabled = false,
setData,
}: SimpleNumberProps) {
const safeValue = value >= limits.min && value <= limits.max ? value : limits.min;
const localLimits = {
min: limits?.min ? limits.min : 1,
max: limits?.max ? limits.max : 1000
}
const safeValue = value >= localLimits.min && value <= localLimits.max ? value : localLimits.min;
const { errors } = usePage().props;
const [inputValue, setInputValue] = useState(safeValue);
@ -55,8 +59,8 @@ export default function SimpleNumber({
type="number"
placeholder={useTrans(placeholder)}
value={inputValue}
min={limits.min}
max={limits.max}
min={localLimits.min}
max={localLimits.max}
step={step}
onChange={(e) => {
setInputValue(Number(e.target.value));

View file

@ -167,12 +167,12 @@ export default function AppSidebar() {
icon: History,
show: authPolicies.activityLogs.viewAny,
},
{
title: useTrans("back.telescope"),
link: route("telescope"),
icon: Telescope,
show: appEnv.mode === "local",
},
// {
// title: useTrans("back.telescope"),
// link: route("telescope"),
// icon: Telescope,
// show: appEnv.mode === "local",
// },
],
},
];

View file

@ -1,5 +1,5 @@
// * Packages
import { TrashIcon, GripVerticalIcon } from "lucide-react";
import { TrashIcon, GripVerticalIcon, InfoIcon, Copy } from "lucide-react";
import { usePage } from "@inertiajs/react";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
@ -15,6 +15,9 @@ import EnumSelect from "@/back/Components/Inputs/EnumSelect";
import SimpleString from "@/back/Components/Inputs/SimpleString";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import { DynamicField, DynamicFieldTypes } from "@/back/types/global";
import useTrans from "@/hooks/use-translations";
import str from "@/hooks/use-string";
import { useEffect, useState } from "react";
type SortableFieldProps = {
id: string;
@ -47,6 +50,31 @@ export default function SortableField({
transDragAndDrop,
}: SortableFieldProps) {
const enumDynamicFieldTypes = usePage().props.enumDynamicFieldTypes as DynamicFieldTypes;
const getEnumDynamicFieldType = (value: number) => {
return Object.values(enumDynamicFieldTypes).find((def) => def.value === value);
};
const enumCurrent = getEnumDynamicFieldType(field.type)?.name + "_";
const splitAndGetEditableKey = (localeEditableKey: string): string => {
const baseKeyArray = localeEditableKey.split(enumCurrent);
return baseKeyArray[baseKeyArray.length - 1];
}
const [editableKey, setEditableKey] = useState(splitAndGetEditableKey(field.key))
const [isTypeChanged, setIsTypeChanged] = useState(false)
const completeKey = enumCurrent + editableKey
useEffect(() => {
if (isTypeChanged) {
setEditableKey(splitAndGetEditableKey(editableKey))
}
setIsTypeChanged(false)
}, [isTypeChanged])
useEffect(() => {
updateField(id, { key: completeKey })
}, [fieldKey, editableKey, completeKey])
const {
attributes,
@ -63,7 +91,9 @@ export default function SortableField({
opacity: isDragging ? 0.5 : 1,
zIndex: isDragging ? 10 : 1,
};
const copyToClipBoard = (data: string) => {
navigator.clipboard.writeText(data)
}
return (
<div
ref={setNodeRef}
@ -109,22 +139,41 @@ export default function SortableField({
value={field.label}
setData={(_, value) => updateField(id, { label: value })}
/>
<SimpleString
name={`config.${fieldKey}.key`}
label={getTransInput("key", index + 1)}
placeholder="ex: John Doe"
value={field.key}
setData={(_, value) => updateField(id, { key: value })}
/>
<div className="flex items-end gap-2">
<SimpleString
name={`config.${fieldKey}.key`}
label={getTransInput("key", index + 1)}
placeholder="ex: John Doe"
value={editableKey}
setData={(_, value) => {
setEditableKey(value)
// updateField(id, { key: value })
}}
/>
<div className="flex flex-col items-center gap-1.5">
<Tooltip>
<TooltipTrigger asChild>
<InfoIcon className="text-blue-500 size-4" />
</TooltipTrigger>
<TooltipContent>
<h4 className="font-semibold">{str(useTrans('back.config_complete_label')).ucFirst().value()}</h4>
<p className="text-muted-foreground mt-1">{completeKey}
</p>
</TooltipContent>
</Tooltip>
<Button type="button" variant={"ghost"} size={"icon"} onClick={() => copyToClipBoard(completeKey)}><Copy className="size-4" /></Button>
</div>
</div>
<EnumSelect
name={`config.${fieldKey}.type`}
label={getTransInput("type", index + 1)}
placeholder={`ex: ${enumDynamicFieldTypes?.string?.label}`}
selected={String(field.type)}
options={enumDynamicFieldTypes}
setData={(_, value) =>
setData={(_, value) => {
setIsTypeChanged(true)
updateField(id, { type: Number(value) })
}
}}
required={required}
disabled={disabled}
/>

View file

@ -116,15 +116,20 @@ export default function Form({
const { toast } = useToast();
/** Form section */
const { post, patch, setData, data } = useForm({
const { post, setData, data, reset } = useForm({
_method: method === Methods.POST ? 'post' : 'put',
title: model?.title ? model?.title[locale]: "",
content: model?.content ? model?.content[locale]: "",
config_id: model?.config_id ?? configOptions[defaultConfigId].id,
status: model?.status ?? enumDynamicPageStatus.draft.value,
isLocked: model?.isLocked ?? false,
});
const routeOptions = () => {
return {
const submit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const routeUrl = route(`back.${modelRoute}.${action}`, model?.id);
if (method !== Methods.PATCH && method !== Methods.POST) throw new Error("The method must be post or patch");
post(routeUrl, {
preserveScroll: true,
onSuccess: () => {
toast({
@ -139,21 +144,7 @@ export default function Form({
description: messages.descriptionError,
});
},
};
};
const submit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const routeUrl = route(`back.${modelRoute}.${action}`, model?.id);
switch (method) {
case Methods.POST:
post(routeUrl, routeOptions());
break;
case Methods.PATCH:
patch(routeUrl, routeOptions());
break;
default:
throw new Error("The method must be post or patch");
}
});
};
useEffect(() => {
@ -289,6 +280,7 @@ export default function Form({
config={getConfig(data.config_id)}
content={typeof data.content === "object" ? data.content : {}}
setData={setData}
reset={reset}
/>
)}
</FieldsetWrapper>

View file

@ -6,6 +6,7 @@ import { ElementType } from "react";
type AppEnv = {
mode: string;
name: string;
url: string;
locale: string;
locales: string;
fallbackLocale: string;

View file

@ -0,0 +1,63 @@
"use client"
import * as React from "react"
import * as SliderPrimitive from "@radix-ui/react-slider"
import { cn } from "@/lib/utils"
function Slider({
className,
defaultValue,
value,
min = 0,
max = 100,
...props
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
const _values = React.useMemo(
() =>
Array.isArray(value)
? value
: Array.isArray(defaultValue)
? defaultValue
: [min, max],
[value, defaultValue, min, max]
)
return (
<SliderPrimitive.Root
data-slot="slider"
defaultValue={defaultValue}
value={value}
min={min}
max={max}
className={cn(
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
className
)}
{...props}
>
<SliderPrimitive.Track
data-slot="slider-track"
className={cn(
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
)}
>
<SliderPrimitive.Range
data-slot="slider-range"
className={cn(
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
)}
/>
</SliderPrimitive.Track>
{Array.from({ length: _values.length }, (_, index) => (
<SliderPrimitive.Thumb
data-slot="slider-thumb"
key={index}
className="border-primary bg-background ring-ring/50 block size-4 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
/>
))}
</SliderPrimitive.Root>
)
}
export { Slider }