Retour sprint 1
This commit is contained in:
parent
a10600adb1
commit
2e033368e2
48 changed files with 2736 additions and 1642 deletions
|
|
@ -14,7 +14,7 @@ trim_trailing_whitespace = false
|
|||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[*.{vue}]
|
||||
[*.vue]
|
||||
indent_size = 2
|
||||
|
||||
[docker-compose.yml]
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ class HtmlColor implements CastsAttributes
|
|||
*/
|
||||
public function get($model, $key, $value, $attributes)
|
||||
{
|
||||
// phpcs:enable
|
||||
return "#" . $this->zeropad(dechex($value), 6);
|
||||
}
|
||||
|
||||
|
|
@ -36,9 +37,14 @@ class HtmlColor implements CastsAttributes
|
|||
* @param mixed $value
|
||||
* @param array $attributes
|
||||
* @return integer
|
||||
* @phpcs:disable Squiz.Commenting.FunctionComment.TypeHintMissing
|
||||
* @phpcs:disable Squiz.Commenting.FunctionComment.ScalarTypeHintMissing
|
||||
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceBeforeLastUsed
|
||||
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceAfterLastUsed
|
||||
*/
|
||||
public function set($model, $key, $value, $attributes)
|
||||
{
|
||||
// phpcs:enable
|
||||
return \intval(hexdec(ltrim($value, '#')));
|
||||
}
|
||||
|
||||
|
|
@ -49,7 +55,7 @@ class HtmlColor implements CastsAttributes
|
|||
* @param integer $lim
|
||||
* @return string
|
||||
*/
|
||||
private function zeropad(string $num, int $lim): string
|
||||
protected function zeropad(string $num, int $lim): string
|
||||
{
|
||||
return (strlen($num) >= $lim) ? $num : $this->zeropad("0" . $num, $lim);
|
||||
}
|
||||
|
|
|
|||
62
app/Casts/RgbaColor.php
Normal file
62
app/Casts/RgbaColor.php
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
namespace App\Casts;
|
||||
|
||||
/**
|
||||
* Custom cast RGBA Color
|
||||
* https://laravel.com/docs/8.x/eloquent-mutators#custom-casts
|
||||
*/
|
||||
class RgbaColor extends HtmlColor
|
||||
{
|
||||
/**
|
||||
* Cast the given value.
|
||||
*
|
||||
* @param Model $model
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @param array $attributes
|
||||
* @return string
|
||||
* @phpcs:disable Squiz.Commenting.FunctionComment.TypeHintMissing
|
||||
* @phpcs:disable Squiz.Commenting.FunctionComment.ScalarTypeHintMissing
|
||||
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed
|
||||
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed
|
||||
*/
|
||||
public function get($model, $key, $value, $attributes)
|
||||
{
|
||||
// phpcs:enable
|
||||
$intValues = collect(\str_split($this->zeropad(\dechex(\intval($value)), 8), 2))->map(function ($hex) {
|
||||
return intval(\hexdec($hex));
|
||||
});
|
||||
// * Set Transparency as float
|
||||
$hexString = $intValues->push(\round($intValues->pop() / 100, 2))->implode(',');
|
||||
return "rgba($hexString)";
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the given value for storage.
|
||||
*
|
||||
* @param Model $model
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @param array $attributes
|
||||
* @return integer
|
||||
* @phpcs:disable Squiz.Commenting.FunctionComment.TypeHintMissing
|
||||
* @phpcs:disable Squiz.Commenting.FunctionComment.ScalarTypeHintMissing
|
||||
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed
|
||||
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed
|
||||
*/
|
||||
public function set($model, $key, $value, $attributes)
|
||||
{
|
||||
// phpcs:enable
|
||||
$value = \rtrim(\ltrim(\str_replace(' ', '', \str_replace('rgba', '', \strval($value))), '('), ')');
|
||||
$intValues = collect(\explode(',', $value))->map(function ($val) {
|
||||
return \floatval($val);
|
||||
});
|
||||
// * Set Transparency as int
|
||||
$hexConcat = $intValues->push($intValues->pop() * 100)->map(function ($value) {
|
||||
return $this->zeropad(dechex(\intval($value)), 2);
|
||||
})->implode('');
|
||||
|
||||
return \intval(\hexdec($hexConcat));
|
||||
}
|
||||
}
|
||||
|
|
@ -2,15 +2,16 @@
|
|||
|
||||
namespace App\Casts;
|
||||
|
||||
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
|
||||
use Illuminate\Support\Carbon;
|
||||
use App\Casts\RgbaColor as CastsRgbaColor;
|
||||
|
||||
//phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceAfterLastUsed
|
||||
//phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceBeforeLastUsed
|
||||
class NiceDate implements CastsAttributes
|
||||
/**
|
||||
* Custom cast RGBA Color nullable
|
||||
* https://laravel.com/docs/8.x/eloquent-mutators#custom-casts
|
||||
*/
|
||||
class RgbaColorNullable extends CastsRgbaColor
|
||||
{
|
||||
/**
|
||||
* Return a nice human readable carbon date.
|
||||
* Cast the given value.
|
||||
*
|
||||
* @param Model $model
|
||||
* @param string $key
|
||||
|
|
@ -22,26 +23,24 @@ class NiceDate implements CastsAttributes
|
|||
*/
|
||||
public function get($model, $key, $value, $attributes)
|
||||
{
|
||||
$value = Carbon::parse($value);
|
||||
return $value->isToday() ?
|
||||
trans('Aujourd\'hui à :time', [
|
||||
'time' => $value->isoFormat('HH:mm')
|
||||
]) : $value->isoFormat('YYYY/MM/DD HH:mm');
|
||||
// phpcs:enable
|
||||
return \is_null($value) ? $value : parent::get($model, $key, $value, $attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a date string to a date time date.
|
||||
* Prepare the given value for storage.
|
||||
*
|
||||
* @param Model $model
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @param array $attributes
|
||||
* @return string
|
||||
* @return integer
|
||||
* @phpcs:disable Squiz.Commenting.FunctionComment.TypeHintMissing
|
||||
* @phpcs:disable Squiz.Commenting.FunctionComment.ScalarTypeHintMissing
|
||||
*/
|
||||
public function set($model, $key, $value, $attributes)
|
||||
{
|
||||
return Carbon::parse($value)->isoFormat('YYYY/MM/DD HH:mm');
|
||||
// phpcs:enable
|
||||
return \is_null($value) ? $value : parent::set($model, $key, $value, $attributes);
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ use Kwaadpepper\Enum\BaseEnum;
|
|||
* @method static self center()
|
||||
* @method static self right()
|
||||
*/
|
||||
final class SliderPosition extends BaseEnum
|
||||
final class SliderElementPosition extends BaseEnum
|
||||
{
|
||||
/**
|
||||
* Enum values
|
||||
|
|
@ -59,7 +59,7 @@ class NewsController extends Controller
|
|||
public function create()
|
||||
{
|
||||
/** @var \Illuminate\Database\Eloquent\Collection */
|
||||
$tags = Tag::query()->select(['id', 'name'])->get();
|
||||
$tags = Tag::query()->select(['id', 'name', 'slug'])->get();
|
||||
return view('back.news.create', compact('tags'));
|
||||
}
|
||||
|
||||
|
|
@ -78,7 +78,7 @@ class NewsController extends Controller
|
|||
if ($news->saveOrFail()) {
|
||||
$news->tags()->sync(collect($request->tags)->pluck('id'));
|
||||
return redirect()
|
||||
->route('back.news.edit', $news)
|
||||
->route('bo.news.edit', $news)
|
||||
->with('success', trans(
|
||||
':model a été créé',
|
||||
['model' => Str::singular(trans('models.classes.news'))]
|
||||
|
|
@ -98,7 +98,7 @@ class NewsController extends Controller
|
|||
{
|
||||
/** @var \Illuminate\Database\Eloquent\Collection */
|
||||
$tags = Tag::whereNotIn('id', $news->tags()->pluck('id'))
|
||||
->select(['id', 'name'])->get();
|
||||
->select(['id', 'name', 'slug'])->get();
|
||||
|
||||
return view('back.news.edit', [
|
||||
'newsModel' => $news,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use App\Http\Requests\Bo\ChangeOrderRequest;
|
|||
use App\Http\Requests\Bo\Sliders\StoreSliderRequest;
|
||||
use App\Http\Requests\Bo\Sliders\UpdateSliderRequest;
|
||||
use App\Models\Slider;
|
||||
use App\Traits\UpdateModelStatus;
|
||||
use App\Traits\Controllers\UpdateModelStatus;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
|
|
|||
|
|
@ -74,6 +74,23 @@ class TagController extends Controller
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*
|
||||
* @param \App\Http\Requests\Bo\Tags\StoreTagRequest $request
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function jsonStore(StoreTagRequest $request)
|
||||
{
|
||||
return DB::transaction(function () use ($request) {
|
||||
/** @var \App\Models\Tag */
|
||||
$tag = Tag::where('slug', Str::slug($request->name))->firstOrNew();
|
||||
$tag->fill($request->validated());
|
||||
$saved = $tag->saveOrFail();
|
||||
return \response()->json($saved ? $tag->toArray() : [], $saved ? 200 : 500);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ class Kernel extends HttpKernel
|
|||
*/
|
||||
protected $middlewareGroups = [
|
||||
'web' => [
|
||||
'htmlsanitize',
|
||||
\App\Http\Middleware\EncryptCookies::class,
|
||||
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
||||
\Illuminate\Session\Middleware\StartSession::class,
|
||||
|
|
@ -58,6 +59,6 @@ class Kernel extends HttpKernel
|
|||
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
|
||||
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
||||
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
|
||||
'xss' => \App\Http\Middleware\XssSanitizer::class,
|
||||
'htmlsanitize' => \App\Http\Middleware\HtmlSanitizer::class,
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class Authenticate extends Middleware
|
|||
}
|
||||
}
|
||||
}
|
||||
return route('login');
|
||||
return collect($logins)->first() ?? '/';
|
||||
}
|
||||
abort(401);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ namespace App\Http\Middleware;
|
|||
use App\Lib\ToolboxHelper;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class XssSanitizer
|
||||
class HtmlSanitizer
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
|
|
@ -38,7 +38,6 @@ class XssSanitizer
|
|||
}
|
||||
} else {
|
||||
$input = strip_tags($input);
|
||||
$input = \filter_var($input, FILTER_SANITIZE_STRING);
|
||||
}
|
||||
});
|
||||
$request->merge($inputs);
|
||||
|
|
@ -44,6 +44,7 @@ class SecureHeaders
|
|||
$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains;');
|
||||
$cspPolicies = [
|
||||
"default-src 'none';",
|
||||
"manifest-src 'self';",
|
||||
// * CKEDITOR Specific https://ckeditor.com/docs/ckeditor5/latest/installation/advanced/csp.html
|
||||
"connect-src 'self' blob:;",
|
||||
"img-src * data: blob:;",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Http\Requests\Bo\News;
|
||||
|
||||
use App\Enums\PublishedStatus;
|
||||
use App\Traits\Requests\HasPicture;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Str;
|
||||
use Kwaadpepper\Enum\Rules\EnumIsValidRule;
|
||||
|
|
@ -12,6 +13,8 @@ use Kwaadpepper\Enum\Rules\EnumIsValidRule;
|
|||
*/
|
||||
class StoreNewsRequest extends FormRequest
|
||||
{
|
||||
use HasPicture;
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
|
|
@ -32,6 +35,7 @@ class StoreNewsRequest extends FormRequest
|
|||
$this->merge([
|
||||
'slug' => Str::slug($this->title),
|
||||
]);
|
||||
$this->mergePicture();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -41,13 +45,12 @@ class StoreNewsRequest extends FormRequest
|
|||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
return \array_merge([
|
||||
'title' => 'required|min:6|max:255',
|
||||
'slug' => 'required|max:255',
|
||||
'seo_title' => 'required|min:53|max:60',
|
||||
'seo_title' => 'required|min:45|max:70',
|
||||
'seo_description' => 'required|min:50|max:160',
|
||||
'content' => 'required|string|min:15',
|
||||
'picture' => 'sometimes|image|dimensions:width=300,height=300|mimes:png,jpg,jpeg,gif',
|
||||
'picture_alt' => 'required_with:picture|nullable|string|min:6|max:255',
|
||||
'picture_title' => 'required_with:picture|nullable|string|min:6|max:255',
|
||||
'published' => ['required', new EnumIsValidRule(PublishedStatus::class)],
|
||||
|
|
@ -55,7 +58,7 @@ class StoreNewsRequest extends FormRequest
|
|||
'tags.*' => 'required|array',
|
||||
'tags.*.id' => 'required|numeric|exists:tags,id|distinct',
|
||||
'tags.*.name' => 'required|string',
|
||||
];
|
||||
], $this->pictureRules(300, 300, 300, 300));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -2,12 +2,16 @@
|
|||
|
||||
namespace App\Http\Requests\Bo\Sliders;
|
||||
|
||||
use App\Enums\Sliders\SliderPosition;
|
||||
use App\Enums\Sliders\SliderElementPosition;
|
||||
use App\Enums\Sliders\SliderType;
|
||||
use App\Traits\Requests\HasPicture;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Kwaadpepper\Enum\Rules\EnumIsValidRule;
|
||||
|
||||
class StoreSliderRequest extends FormRequest
|
||||
{
|
||||
use HasPicture;
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
|
|
@ -31,6 +35,7 @@ class StoreSliderRequest extends FormRequest
|
|||
'publish_at' => $this->publish_at ? \str_replace('T', ' ', $this->publish_at) : null,
|
||||
'unpublish_at' => $this->unpublish_at ? \str_replace('T', ' ', $this->unpublish_at) : null,
|
||||
]);
|
||||
$this->mergePicture();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -40,21 +45,31 @@ class StoreSliderRequest extends FormRequest
|
|||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
$rules = [
|
||||
'title' => 'required|min:3|max:255',
|
||||
'title_color' => 'required|color_hex',
|
||||
'title_color_bg' => 'required|color_hex',
|
||||
'position' => ['required', new EnumIsValidRule(SliderPosition::class)],
|
||||
'picture' => 'required|image|dimensions:width=1400,height=630|mimes:png,jpg,jpeg,gif',
|
||||
'title_color' => 'nullable|color_rgba',
|
||||
'title_color_bg' => 'nullable|color_rgba',
|
||||
'subtitle' => 'required|min:3|max:255',
|
||||
'subtitle_color' => 'nullable|color_rgba',
|
||||
'subtitle_color_bg' => 'nullable|color_rgba',
|
||||
'text_position_desktop' => ['required', new EnumIsValidRule(SliderElementPosition::class)],
|
||||
'picture_position_mobile' => ['required', new EnumIsValidRule(SliderElementPosition::class)],
|
||||
'link' => 'required|url',
|
||||
'link_title' => 'required|min:3|max:255',
|
||||
'link_color' => 'required|color_hex',
|
||||
'link_color_bg' => 'required|color_hex',
|
||||
'link_color' => 'nullable|color_rgba',
|
||||
'link_color_hover' => 'nullable|color_rgba',
|
||||
'link_color_bg' => 'nullable|color_rgba',
|
||||
'link_color_bg_hover' => 'nullable|color_rgba',
|
||||
'link_new_tab' => 'required|boolean',
|
||||
'status' => 'required|boolean',
|
||||
'publish_at' => 'nullable|date_format:Y-m-d H:i',
|
||||
'unpublish_at' => 'nullable|date_format:Y-m-d H:i',
|
||||
];
|
||||
|
||||
if (!$this->slider) {
|
||||
$rules['type'] = ['required', new EnumIsValidRule(SliderType::class)];
|
||||
}
|
||||
return \array_merge($rules, $this->pictureRules(1400, 630, 1400, 630));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -68,13 +83,15 @@ class StoreSliderRequest extends FormRequest
|
|||
'title' => trans('title'),
|
||||
'title_color' => trans('couleur de titre'),
|
||||
'title_color_bg' => trans('couleur de fond du titre'),
|
||||
'position' => trans('position de centrage de l\'image'),
|
||||
'picture_position_mobile' => trans('position de centrage de l\'image en mobile'),
|
||||
'text_position_desktop' => trans('position de centrage du texte en desktop'),
|
||||
'link' => trans('lien'),
|
||||
'link_title' => trans('titre du lien'),
|
||||
'link_color' => trans('couleur du lien'),
|
||||
'link_color_bg' => trans('couleur de fond du lien'),
|
||||
'link_new_tab' => trans('nouvel onglet'),
|
||||
'status' => trans('publication'),
|
||||
'picture' => trans('image'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ class UpdateSliderRequest extends StoreSliderRequest
|
|||
*/
|
||||
public function rules()
|
||||
{
|
||||
return \array_merge(parent::rules(), [
|
||||
'picture' => 'sometimes|image|dimensions:width=1400,height=630|mimes:png,jpg,jpeg,gif'
|
||||
]);
|
||||
$rules = parent::rules();
|
||||
unset($rules['type']);
|
||||
return $rules;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -205,10 +205,7 @@ class ToolboxHelper
|
|||
$table = $model->getTable();
|
||||
if (\is_object($image) and $image instanceof \Illuminate\Http\UploadedFile) {
|
||||
/** @var \Illuminate\Http\UploadedFile $image */
|
||||
$newPath = self::prepareMoveFile($table, \filter_var(
|
||||
\strip_tags(\urldecode($image->getClientOriginalName())),
|
||||
\FILTER_SANITIZE_STRING
|
||||
));
|
||||
$newPath = self::prepareMoveFile($table, $image->getClientOriginalName());
|
||||
$newPath = str_replace(storage_path('app'), '', $newPath);
|
||||
$image->storePubliclyAs(
|
||||
pathinfo($newPath, \PATHINFO_DIRNAME),
|
||||
|
|
@ -241,6 +238,15 @@ class ToolboxHelper
|
|||
*/
|
||||
private static function prepareMoveFile(string $table, string $filename): string
|
||||
{
|
||||
// * Security filter + beautifier
|
||||
$filename = self::sanitizeFileName($filename);
|
||||
// * Security filter
|
||||
$filename = \filter_var($filename, \FILTER_SANITIZE_STRING);
|
||||
// * Slugify filename
|
||||
$ext = \pathinfo($filename, \PATHINFO_EXTENSION);
|
||||
$filename = Str::slug(\pathinfo($filename, \PATHINFO_FILENAME)) .
|
||||
(\strlen($ext) ? ".{$ext}" : '');
|
||||
|
||||
$pathFolder = \storage_path(sprintf('app/public/images/%s', $table));
|
||||
if (!File::exists($pathFolder)) {
|
||||
File::makeDirectory($pathFolder, 0755, true);
|
||||
|
|
@ -371,4 +377,79 @@ class ToolboxHelper
|
|||
->withoutMiddleware(\App\Http\Middleware\VerifyCsrfToken::class)
|
||||
->name('cspReportUri');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize File name for uploaded files
|
||||
*
|
||||
* @param string $filename The original uploaded client file name.
|
||||
* @param boolean $beautify Beautify the filename.
|
||||
* @return string The sanitized file name.
|
||||
*/
|
||||
public static function sanitizeFileName(string $filename, bool $beautify = true): string
|
||||
{
|
||||
// * Sanitize filename
|
||||
// phpcs:disable Generic.Files.LineLength.TooLong
|
||||
$filename = preg_replace(
|
||||
'~
|
||||
[<>:"/\\\|?*]| # file system reserved https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
|
||||
[\x00-\x1F]| # control characters http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
|
||||
[\x7F\xA0\xAD]| # non-printing characters DEL, NO-BREAK SPACE, SOFT HYPHEN
|
||||
[#\[\]@!$&\'()+,;=]| # URI reserved https://tools.ietf.org/html/rfc3986#section-2.2
|
||||
[{}^\~`] # URL unsafe characters https://www.ietf.org/rfc/rfc1738.txt
|
||||
~x',
|
||||
'-',
|
||||
$filename
|
||||
);
|
||||
// phpcs:enable
|
||||
// * Avoids '.' '..' or '.hiddenFiles' .
|
||||
$filename = Str::of($filename)->ltrim('.-');
|
||||
// * Optional beautification
|
||||
if ($beautify) {
|
||||
$filename = static::beautifyFilename($filename);
|
||||
}
|
||||
// * Maximize filename length to 255 bytes http://serverfault.com/a/9548/44086
|
||||
/** @var string */
|
||||
$extension = pathinfo($filename, \PATHINFO_EXTENSION);
|
||||
/** @var string */
|
||||
$filename = pathinfo($filename, \PATHINFO_FILENAME);
|
||||
|
||||
$encoding = mb_detect_encoding($filename);
|
||||
$encoding = $encoding ? $encoding : null;
|
||||
return \mb_strcut(
|
||||
$filename,
|
||||
0,
|
||||
255 - (!empty($extension) ? strlen($extension) + 1 : 0),
|
||||
$encoding
|
||||
) . (!empty($extension) ? ".$extension" : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Beautify a filename
|
||||
*
|
||||
* @param string $filename
|
||||
* @return string
|
||||
*/
|
||||
private static function beautifyFilename(string $filename): string
|
||||
{
|
||||
// Reduce consecutive characters.
|
||||
$filename = preg_replace([
|
||||
// * "file name.zip" becomes 'file-name.zip'
|
||||
'/ +/',
|
||||
// * "file___name.zip" becomes 'file-name.zip'
|
||||
'/_+/',
|
||||
// * "file---name.zip" becomes 'file-name.zip'
|
||||
'/-+/'
|
||||
], '-', $filename);
|
||||
$filename = preg_replace([
|
||||
// * "file--.--.-.--name.zip" becomes "file.name.zip"
|
||||
'/-*\.-*/',
|
||||
// * "file...name..zip" becomes "file.name.zip"
|
||||
'/\.{2,}/'
|
||||
], '.', $filename);
|
||||
// Lowercase for windows/unix interoperability http://support.microsoft.com/kb/100625 .
|
||||
$filename = mb_strtolower($filename, mb_detect_encoding($filename));
|
||||
// * ".file-name.-" becomes "file-name"
|
||||
$filename = trim($filename, '.-');
|
||||
return $filename;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Casts\NiceDate;
|
||||
use App\Enums\PublishedStatus;
|
||||
use App\Lib\ToolboxHelper;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
|
@ -148,14 +147,4 @@ class News extends Model
|
|||
ToolboxHelper::assertFieldIsUnique($table, 'title', $title, $id);
|
||||
ToolboxHelper::assertFieldIsUnique($table, 'slug', Str::slug($title), $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* updated_at_date getter
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUpdatedAtDateAttribute(): string
|
||||
{
|
||||
return (new NiceDate())->get($this, 'published_at', $this->published_at, []);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Casts\HtmlColor;
|
||||
use App\Casts\NiceDate;
|
||||
use App\Enums\Sliders\SliderPosition;
|
||||
use App\Casts\RgbaColorNullable;
|
||||
use App\Enums\Sliders\SliderElementPosition;
|
||||
use App\Enums\Sliders\SliderType;
|
||||
use App\Lib\ToolboxHelper;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
|
@ -29,13 +28,15 @@ class Slider extends Model
|
|||
'subtitle',
|
||||
'subtitle_color',
|
||||
'subtitle_color_bg',
|
||||
'position',
|
||||
'type',
|
||||
'text_position_desktop',
|
||||
'picture_position_mobile',
|
||||
'picture',
|
||||
'link',
|
||||
'link_title',
|
||||
'link_color',
|
||||
'link_color_hover',
|
||||
'link_color_bg',
|
||||
'link_color_bg_hover',
|
||||
'link_new_tab',
|
||||
'status',
|
||||
'order',
|
||||
|
|
@ -49,7 +50,8 @@ class Slider extends Model
|
|||
* @var array
|
||||
*/
|
||||
protected $enumCasts = [
|
||||
'position' => SliderPosition::class,
|
||||
'text_position_desktop' => SliderElementPosition::class,
|
||||
'picture_position_mobile' => SliderElementPosition::class,
|
||||
'type' => SliderType::class
|
||||
];
|
||||
|
||||
|
|
@ -59,12 +61,14 @@ class Slider extends Model
|
|||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'title_color' => HtmlColor::class,
|
||||
'title_color_bg' => HtmlColor::class,
|
||||
'subtitle_color' => HtmlColor::class,
|
||||
'subtitle_color_bg' => HtmlColor::class,
|
||||
'link_color' => HtmlColor::class,
|
||||
'link_color_bg' => HtmlColor::class,
|
||||
'title_color' => RgbaColorNullable::class,
|
||||
'title_color_bg' => RgbaColorNullable::class,
|
||||
'subtitle_color' => RgbaColorNullable::class,
|
||||
'subtitle_color_bg' => RgbaColorNullable::class,
|
||||
'link_color' => RgbaColorNullable::class,
|
||||
'link_color_hover' => RgbaColorNullable::class,
|
||||
'link_color_bg' => RgbaColorNullable::class,
|
||||
'link_color_bg_hover' => RgbaColorNullable::class,
|
||||
'link_new_tab' => 'bool',
|
||||
'status' => 'bool',
|
||||
'publish_at' => 'datetime',
|
||||
|
|
@ -115,14 +119,4 @@ class Slider extends Model
|
|||
}
|
||||
|
||||
// * METHODS
|
||||
|
||||
/**
|
||||
* updated_at_date getter
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUpdatedAtDateAttribute(): string
|
||||
{
|
||||
return (new NiceDate())->get($this, 'updated_at', $this->updated_at, []);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
namespace App\Traits\Controllers;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use App\Enums\InputType;
|
||||
use App\Lib\ToolboxHelper;
|
||||
|
||||
trait MiniCrudModel
|
||||
{
|
||||
/**
|
||||
* Fill the model with an array of attributes.
|
||||
*
|
||||
* @param array $attributes
|
||||
* @return void
|
||||
* @throws \Illuminate\Database\Eloquent\MassAssignmentException If fill prop is not allowed.
|
||||
* @phpcs:disable Squiz.Commenting.FunctionComment.EmptyThrows
|
||||
*/
|
||||
public function fill(array $attributes)
|
||||
{
|
||||
parent::fill($attributes);
|
||||
if (isset($attributes['data'])) {
|
||||
$this->setData($attributes['data']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a specific prop
|
||||
*
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
private function setData(array $data): void
|
||||
{
|
||||
$attrs = $this->attributes;
|
||||
if (isset($attrs['data'])) {
|
||||
unset($attrs['data']);
|
||||
}
|
||||
$data = $this->serializeData($data);
|
||||
$this->setRawAttributes(\array_merge_recursive(
|
||||
$attrs,
|
||||
['data' => $data]
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data attribute
|
||||
* Cast json_decode to object.
|
||||
*
|
||||
* @return object|null
|
||||
*/
|
||||
protected function getDataAttribute(): ?object
|
||||
{
|
||||
return \json_decode($this->getAttributes()['data'] ?? '[]');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data attribute
|
||||
* Cast json_decode to object.
|
||||
*
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
protected function setDataAttribute(array $data): void
|
||||
{
|
||||
$this->setData($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize properties
|
||||
*
|
||||
* @param array $data
|
||||
* @return string
|
||||
* @throws \ErrorException If input type is no handled.
|
||||
* @throws \Illuminate\Validation\ValidationException If url is a bo url.
|
||||
* @phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
|
||||
* @phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh
|
||||
*/
|
||||
private function serializeData(array $data): string
|
||||
{
|
||||
foreach ($data as $o => $input) {
|
||||
switch ($input['inputType']->value ?? $input['inputType']['value']) {
|
||||
case InputType::image()->value:
|
||||
$data[$o]['value'] = ToolboxHelper::storeImage($this->getTable(), $input['value']);
|
||||
break;
|
||||
case InputType::boolean()->value:
|
||||
case InputType::text()->value:
|
||||
case InputType::number()->value:
|
||||
case InputType::string()->value:
|
||||
case InputType::videoVimeo()->value:
|
||||
break;
|
||||
case InputType::url()->value:
|
||||
ToolboxHelper::assertNoInternalBoUrls('content', $input['value']);
|
||||
break;
|
||||
default:
|
||||
throw new \ErrorException(sprintf(
|
||||
'Unknown input Type %s',
|
||||
$input['inputType']->label ?? $input['inputType']['label']
|
||||
));
|
||||
}
|
||||
}//end foreach
|
||||
return json_encode($data);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +1,58 @@
|
|||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
namespace App\Traits\Requests;
|
||||
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
trait HasPicture
|
||||
{
|
||||
/**
|
||||
* Images rules
|
||||
* Minimum width
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
private $minWith = 400;
|
||||
|
||||
/**
|
||||
* Maximum width
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
private $maxWith = 1800;
|
||||
|
||||
/**
|
||||
* Minimum height
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
private $minHeight = 300;
|
||||
|
||||
/**
|
||||
* Maximum height
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
private $maxHeight = 1800;
|
||||
|
||||
/**
|
||||
* Image rules
|
||||
*
|
||||
* @param integer|null $minWith
|
||||
* @param integer|null $minHeight
|
||||
* @param integer|null $maxWith
|
||||
* @param integer|null $maxHeight
|
||||
* @return array
|
||||
*/
|
||||
public function pictureRule(): array
|
||||
{
|
||||
public function pictureRules(
|
||||
?int $minWith = null,
|
||||
?int $minHeight = null,
|
||||
?int $maxWith = null,
|
||||
?int $maxHeight = null
|
||||
): array {
|
||||
$this->minWith = $minWith ?? $this->minWith;
|
||||
$this->minHeight = $minHeight ?? $this->minHeight;
|
||||
$this->maxWith = $maxWith ?? $this->maxWith;
|
||||
$this->maxHeight = $maxHeight ?? $this->maxHeight;
|
||||
return [
|
||||
'picture' => [
|
||||
'required',
|
||||
|
|
@ -26,10 +66,22 @@ trait HasPicture
|
|||
/**
|
||||
* Images rules
|
||||
*
|
||||
* @param integer|null $minWith
|
||||
* @param integer|null $minHeight
|
||||
* @param integer|null $maxWith
|
||||
* @param integer|null $maxHeight
|
||||
* @return array
|
||||
*/
|
||||
public function pictureRules(): array
|
||||
{
|
||||
public function picturesRules(
|
||||
?int $minWith = null,
|
||||
?int $minHeight = null,
|
||||
?int $maxWith = null,
|
||||
?int $maxHeight = null
|
||||
): array {
|
||||
$this->minWith = $minWith ?? $this->minWith;
|
||||
$this->minHeight = $minHeight ?? $this->minHeight;
|
||||
$this->maxWith = $maxWith ?? $this->maxWith;
|
||||
$this->maxHeight = $maxHeight ?? $this->maxHeight;
|
||||
return [
|
||||
'pictures' => 'sometimes|array',
|
||||
'pictureValidate' => 'required_with:pictures|array',
|
||||
|
|
@ -57,25 +109,35 @@ trait HasPicture
|
|||
$fail(trans(":attribute n'existe pas dans le système de fichier"));
|
||||
return;
|
||||
}
|
||||
if (!\in_array(File::mimeType($value), ['image/jpeg', 'image/jpg', 'image/png'])) {
|
||||
$fail(trans(":attribute doit être une image de type 'image/jpeg,image/jpg,image/png'"));
|
||||
if (!\in_array(File::mimeType($value), ['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($value)) or !isset($dimensions[0]) or !isset($dimensions[1])) {
|
||||
$fail(trans(":attribute impossible de valider les dimensions"));
|
||||
} else {
|
||||
$minWith = 400;
|
||||
$maxWith = 1800;
|
||||
$minHeight = 300;
|
||||
$maxHeight = 1800;
|
||||
$width = intval($dimensions[0]);
|
||||
$height = intval($dimensions[1]);
|
||||
if (($width < $minWith) or ($width > $maxWith)) {
|
||||
$fail(trans(":attribute la largeur doit être comprise entre $minWith et $maxWith"));
|
||||
$width = intval($dimensions[0]);
|
||||
$height = intval($dimensions[1]);
|
||||
|
||||
$xMessage = trans(
|
||||
":attribute la largeur doit être comprise entre {$this->minWith}px et {$this->maxWith}px"
|
||||
);
|
||||
if ($this->minWith === $this->maxWith) {
|
||||
$xMessage = trans(":attribute la largeur doit être de {$this->minWith}px");
|
||||
}
|
||||
if (($height < $minHeight) or ($height > $maxHeight)) {
|
||||
$fail(trans(":attribute la hauteur doit être comprise entre $minHeight et $maxHeight"));
|
||||
$yMessage = trans(
|
||||
":attribute la hauteur doit être comprise entre {$this->minWith}px et {$this->maxWith}px"
|
||||
);
|
||||
if ($this->minHeight === $this->maxHeight) {
|
||||
$yMessage = trans(":attribute la hauteur doit être de {$this->minWith}px");
|
||||
}
|
||||
}
|
||||
|
||||
if (($width < $this->minWith) or ($width > $this->maxWith)) {
|
||||
$fail($xMessage);
|
||||
}
|
||||
if (($height < $this->minHeight) or ($height > $this->maxHeight)) {
|
||||
$fail($yMessage);
|
||||
}
|
||||
} //end if
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -101,7 +163,6 @@ trait HasPicture
|
|||
[\public_path(ltrim($decodedPath, '/'))]
|
||||
)
|
||||
]);
|
||||
$changedPicture = $this->picture;
|
||||
$changedPicture = ltrim($decodedPath, '/');
|
||||
$this->merge([
|
||||
'picture' => $changedPicture
|
||||
896
composer.lock
generated
896
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Enums\Sliders\SliderPosition;
|
||||
use App\Enums\Sliders\SliderElementPosition;
|
||||
use App\Enums\Sliders\SliderType;
|
||||
use App\Models\Slider;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
|
@ -34,7 +34,8 @@ class SliderFactory extends Factory
|
|||
$subtitle = $this->faker->unique()->words(rand(8, 10), true);
|
||||
return [
|
||||
'type' => Collect(SliderType::toArray())->random(),
|
||||
'position' => Collect(SliderPosition::toArray())->random(),
|
||||
'text_position_desktop' => Collect(SliderElementPosition::toArray())->random(),
|
||||
'picture_position_mobile' => Collect(SliderElementPosition::toArray())->random(),
|
||||
'title' => $title,
|
||||
'title_color' => $this->faker->hexColor(),
|
||||
'title_color_bg' => $this->faker->hexColor(),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\Sliders\SliderPosition;
|
||||
use App\Enums\Sliders\SliderElementPosition;
|
||||
use App\Enums\Sliders\SliderType;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
|
|
@ -20,8 +20,8 @@ class CreateSlidersTable extends Migration
|
|||
|
||||
$table->unsignedTinyInteger('type')->default(SliderType::home())
|
||||
->comment('Enums\\SliderType type de slider');
|
||||
$table->unsignedTinyInteger('position')->default(SliderPosition::center())
|
||||
->comment('Enums\\SliderPosition slider centrage du slider en responsive');
|
||||
$table->unsignedTinyInteger('position')->default(SliderElementPosition::center())
|
||||
->comment('Enums\\SliderElementPosition slider centrage du slider en responsive');
|
||||
|
||||
$table->string('title')->comment('Titre du slider');
|
||||
$table->unsignedInteger('title_color')->comment('Couleur du titre');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddHoverColorsOnSliders extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('sliders', function (Blueprint $table) {
|
||||
$table->unsignedInteger('link_color')->comment('Couleur du lien')->change();
|
||||
$table->unsignedInteger('link_color_bg')->comment('Couleur de fond du lien')->change();
|
||||
|
||||
// Add columns.
|
||||
$table->unsignedInteger('link_color_hover')->comment('Couleur du lien (au survol)')->after('link_color');
|
||||
$table->unsignedInteger('link_color_bg_hover')->comment('Couleur de fond du lien (au survol)')
|
||||
->after('link_color_bg');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('sliders', function (Blueprint $table) {
|
||||
$table->string('link_color')->change()->comment('Couleur du lien');
|
||||
$table->string('link_color_bg')->change()->comment('Couleur de fond du lien');
|
||||
|
||||
// Add columns.
|
||||
$table->removeColumn('link_color_hover');
|
||||
$table->removeColumn('link_color_bg_hover');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class SetColorsNullableToSliders extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('sliders', function (Blueprint $table) {
|
||||
$table->unsignedInteger('title_color')->nullable()->comment('Couleur du titre')->change();
|
||||
$table->unsignedInteger('title_color_bg')->nullable()->comment('Couleur de fond du titre')->change();
|
||||
$table->unsignedInteger('subtitle_color')->nullable()->comment('Couleur du soustitre')->change();
|
||||
$table->unsignedInteger('subtitle_color_bg')->nullable()->comment('Couleur de fond du soustitre')->change();
|
||||
$table->unsignedInteger('link_color')->nullable()->comment('Couleur du lien')->change();
|
||||
$table->unsignedInteger('link_color_hover')->nullable()
|
||||
->comment('Couleur de fond du lien (au survol)')->change();
|
||||
$table->unsignedInteger('link_color_bg')->nullable()->comment('Couleur de fond du lien')->change();
|
||||
$table->unsignedInteger('link_color_bg_hover')->nullable()
|
||||
->comment('Couleur de fond du lien (au survol)')->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('sliders', function (Blueprint $table) {
|
||||
$table->unsignedInteger('title_color')->comment('Couleur du titre')->change();
|
||||
$table->unsignedInteger('title_color_bg')->comment('Couleur de fond du titre')->change();
|
||||
$table->unsignedInteger('subtitle_color')->comment('Couleur du soustitre')->change();
|
||||
$table->unsignedInteger('subtitle_color_bg')->comment('Couleur de fond du soustitre')->change();
|
||||
$table->unsignedInteger('link_color')->comment('Couleur du lien')->change();
|
||||
$table->unsignedInteger('link_color_bg')->comment('Couleur de fond du lien')->change();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\Sliders\SliderElementPosition;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class TextPositionOnSliders extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('sliders', function (Blueprint $table) {
|
||||
$table->renameColumn('position', 'picture_position_mobile');
|
||||
$table->unsignedTinyInteger('text_position_desktop')
|
||||
->after('subtitle_color_bg')
|
||||
->default(SliderElementPosition::right())
|
||||
->comment('Enums\\SliderElementPosition postionnement du texte en desktop');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('sliders', function (Blueprint $table) {
|
||||
$table->renameColumn('picture_position_mobile', 'position');
|
||||
$table->dropColumn('text_position_desktop');
|
||||
});
|
||||
}
|
||||
}
|
||||
1422
package-lock.json
generated
1422
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -10,6 +10,7 @@
|
|||
"production": "mix --production"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ckpack/vue-color": "^1.1.9",
|
||||
"@fortawesome/fontawesome-free": "^6.1.1",
|
||||
"@popperjs/core": "^2.11.5",
|
||||
"@sipec/vue3-tags-input": "^3.0.4",
|
||||
|
|
@ -28,14 +29,12 @@
|
|||
"resolve-url-loader": "^5.0.0",
|
||||
"sass": "^1.52.1",
|
||||
"sass-loader": "^12.6.0",
|
||||
"slugify": "^1.6.5",
|
||||
"sweetalert2": "^11.4.17",
|
||||
"toastify-js": "^1.11.2",
|
||||
"ts-loader": "^9.3.0",
|
||||
"typescript": "^4.7.3",
|
||||
"vue": "^3.2.36",
|
||||
"vue-loader": "^16.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"autoprefixer": "10.4.5"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// LIBS
|
||||
@import './common.scss';
|
||||
@import "./common.scss";
|
||||
|
||||
.icon-size {
|
||||
font-size: 20px;
|
||||
|
|
@ -14,3 +14,17 @@
|
|||
@extend .icon-size;
|
||||
color: seagreen;
|
||||
}
|
||||
|
||||
.table.fixAction {
|
||||
& th:last-child,
|
||||
& td:last-child {
|
||||
// * Shrink
|
||||
width: 0.1%;
|
||||
white-space: nowrap;
|
||||
// * Sticky
|
||||
position: sticky;
|
||||
right: 0;
|
||||
background-color: $gray-200;
|
||||
color: $gray-900;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,10 +27,10 @@ export default defineComponent({
|
|||
}
|
||||
},
|
||||
mounted() {
|
||||
const json = String(this.$attrs.json ?? ""),
|
||||
const json = String(this.$attrs.json ?? "{}"),
|
||||
data = JSON.parse(json);
|
||||
this.name = data.name;
|
||||
this.internalText = data.text;
|
||||
this.internalText = data.text ? String(data.text) : "";
|
||||
this.internalId = data.id ? data.id : "";
|
||||
this.internalRequired = Boolean(data.required);
|
||||
this.makeSuperBuild();
|
||||
|
|
|
|||
263
resources/ts/components/bo/ColorPickerComponent.vue
Normal file
263
resources/ts/components/bo/ColorPickerComponent.vue
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
<template>
|
||||
<div
|
||||
:class="`input-group color-picker-component ${_rgbaMode ? '' : 'noTransparent'}`"
|
||||
ref="colorpicker"
|
||||
>
|
||||
<div
|
||||
:id="`colorPickerFakeInput${_id}`"
|
||||
class="d-inline-block"
|
||||
ref="colorPickerFakeInput"
|
||||
>
|
||||
<div class="form-check form-switch d-inline-block pt-3">
|
||||
<b>
|
||||
<label v-if="_label.length" :for="_id ? _id : ''" class="form-label">
|
||||
{{ _label }}
|
||||
</label>
|
||||
</b>
|
||||
<input
|
||||
:id="_id"
|
||||
type="color"
|
||||
class="form-control form-control-color"
|
||||
:value="_internalHex"
|
||||
:title="_title"
|
||||
:aria-describedby="_ariaDescribedby"
|
||||
data-bs-toggle="tooltip"
|
||||
v-on:click.prevent="togglePicker()"
|
||||
/>
|
||||
<input :name="_name" type="text" class="d-none" :value="_value" />
|
||||
</div>
|
||||
<div
|
||||
:class="`form-check form-switch pt-3 ${_nullable ? 'd-inline-block' : 'd-none'}`"
|
||||
>
|
||||
<input
|
||||
ref="nullableInput"
|
||||
:id="`checkbox${_id}`"
|
||||
class="form-check-input me-1 mt-4"
|
||||
type="checkbox"
|
||||
data-bs-toggle="tooltip"
|
||||
:title="__('Rendre la couleur transparente ?')"
|
||||
:checked="_value === null ? true : false"
|
||||
v-on:change="updateValue"
|
||||
/>
|
||||
<label class="form-check-label user-select-none mt-3" :for="`checkbox${_id}`">
|
||||
{{ __("Transparent ?") }}
|
||||
</label>
|
||||
</div>
|
||||
<div :class="`chromePicker ${_displayPicker ? 'd-inline-block' : 'd-none'}`">
|
||||
<chrome-picker ref="picker" v-model="_internalValue" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from "vue";
|
||||
// @ts-ignore
|
||||
import { Chrome } from "@ckpack/vue-color";
|
||||
import trans from "../../modules/trans";
|
||||
import Tooltip from "bootstrap/js/dist/tooltip";
|
||||
|
||||
export default defineComponent({
|
||||
inheritAttrs: false,
|
||||
mixins: [trans],
|
||||
components: {
|
||||
"chrome-picker": Chrome,
|
||||
},
|
||||
mounted() {
|
||||
this.__nullableInput = this.$refs.nullableInput as HTMLInputElement;
|
||||
this.__colorPickerFakeInput = this.$refs.colorPickerFakeInput as HTMLElement;
|
||||
this.__picker = this.$refs.picker as Chrome;
|
||||
|
||||
const json = String(this.$attrs.json ?? "{}"),
|
||||
data = JSON.parse(json);
|
||||
this._id = this.id ? this.id : data.id;
|
||||
this._name = this.name ? this.name : data.name;
|
||||
this._value = this.value ? this.value : data.value;
|
||||
this._title = this.title ? this.title : data.title;
|
||||
this._label = this.label ? this.label : data.label;
|
||||
this._ariaDescribedby = this.ariaDescribedby
|
||||
? this.ariaDescribedby
|
||||
: data.ariaDescribedby;
|
||||
this._nullable = this.nullable ? this.nullable : data.nullable ? true : false;
|
||||
this._rgbaMode = this.rgbaMode ? this.rgbaMode : data.rgbaMode ? true : false;
|
||||
|
||||
if (!this._nullable) {
|
||||
this._internalValue = this._value
|
||||
? this._value
|
||||
: this._rgbaMode
|
||||
? "rgba(0,0,0,0)"
|
||||
: "#000000";
|
||||
} else {
|
||||
this._internalValue = this._value ?? "";
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this._internalHex = this.__picker.$data.val.hex;
|
||||
});
|
||||
|
||||
this.initTooltips();
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
type: String as PropType<string | null>,
|
||||
default: null,
|
||||
},
|
||||
name: {
|
||||
type: String as PropType<string | undefined>,
|
||||
},
|
||||
value: {
|
||||
type: String as PropType<string | null>,
|
||||
default: null,
|
||||
},
|
||||
title: {
|
||||
type: String as PropType<string | undefined>,
|
||||
},
|
||||
label: {
|
||||
type: String as PropType<string | undefined>,
|
||||
},
|
||||
nullable: {
|
||||
type: Boolean as PropType<boolean | undefined>,
|
||||
},
|
||||
rgbaMode: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false,
|
||||
},
|
||||
ariaDescribedby: {
|
||||
type: String as PropType<string | undefined>,
|
||||
},
|
||||
},
|
||||
data(): {
|
||||
_id: string;
|
||||
_name: string;
|
||||
_value: string | null;
|
||||
_title: string;
|
||||
_label: string;
|
||||
_nullable: boolean;
|
||||
_rgbaMode: boolean;
|
||||
_ariaDescribedby: string;
|
||||
|
||||
_internalValue:
|
||||
| string
|
||||
| { hex: string; rgba: { a: string; b: string; g: string; r: string } };
|
||||
_internalHex: string;
|
||||
|
||||
_displayPicker: boolean;
|
||||
|
||||
__nullableInput: HTMLInputElement | null;
|
||||
__colorPickerFakeInput: HTMLElement | null;
|
||||
__picker: Chrome | null;
|
||||
} {
|
||||
return {
|
||||
_id: "",
|
||||
_name: "",
|
||||
_value: "",
|
||||
_title: "",
|
||||
_label: "",
|
||||
_nullable: false,
|
||||
_rgbaMode: false,
|
||||
_ariaDescribedby: "",
|
||||
|
||||
_internalValue: "",
|
||||
_internalHex: "",
|
||||
|
||||
_displayPicker: false,
|
||||
|
||||
__nullableInput: null,
|
||||
__colorPickerFakeInput: null,
|
||||
__picker: null,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
_internalValue() {
|
||||
this.updateValue();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateValue() {
|
||||
if (this.__nullableInput?.checked) {
|
||||
this.hidePicker();
|
||||
this._value = "";
|
||||
this._internalHex = "";
|
||||
return;
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this._internalHex = this.__picker.$data.val.hex;
|
||||
});
|
||||
if (typeof this._internalValue === "string") {
|
||||
this._value = this._internalValue;
|
||||
return;
|
||||
}
|
||||
const isNull = this.__nullableInput?.checked ? true : false;
|
||||
if (this._rgbaMode) {
|
||||
const rgba = this._internalValue?.rgba;
|
||||
this._value =
|
||||
!isNull && rgba
|
||||
? `rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${rgba.a})`
|
||||
: this._nullable
|
||||
? null
|
||||
: "";
|
||||
return;
|
||||
}
|
||||
this._value =
|
||||
!isNull && this._internalValue
|
||||
? this._internalValue.hex
|
||||
: this._nullable
|
||||
? null
|
||||
: "";
|
||||
},
|
||||
showPicker() {
|
||||
document.addEventListener("click", this.documentClick);
|
||||
this._displayPicker = true;
|
||||
const input = this.__nullableInput;
|
||||
if (!input) {
|
||||
throw new Error("input missing");
|
||||
}
|
||||
input.checked = false;
|
||||
},
|
||||
hidePicker() {
|
||||
document.removeEventListener("click", this.documentClick);
|
||||
this._displayPicker = false;
|
||||
},
|
||||
togglePicker() {
|
||||
this._displayPicker ? this.hidePicker() : this.showPicker();
|
||||
},
|
||||
documentClick(e: Event) {
|
||||
const colorpickerComponent = this.__colorPickerFakeInput,
|
||||
target = e.target;
|
||||
if (
|
||||
colorpickerComponent !== target &&
|
||||
colorpickerComponent &&
|
||||
(target as HTMLElement).closest(`#colorPickerFakeInput${this._id}`)?.id !==
|
||||
colorpickerComponent.id
|
||||
) {
|
||||
this.hidePicker();
|
||||
}
|
||||
},
|
||||
initTooltips() {
|
||||
// * INIT Bootstrap tooltips
|
||||
let tooltipTriggerList = [].slice.call(
|
||||
document.querySelectorAll('.color-picker-component [data-bs-toggle="tooltip"]')
|
||||
);
|
||||
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new Tooltip(tooltipTriggerEl, {
|
||||
delay: { show: 1000, hide: 300 },
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.color-picker-component {
|
||||
position: relative;
|
||||
.chromePicker {
|
||||
position: absolute;
|
||||
top: 6.4rem;
|
||||
left: 0;
|
||||
z-index: 9;
|
||||
}
|
||||
&.noTransparent .vc-chrome-alpha-wrap {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -2,9 +2,9 @@
|
|||
<div class="container ImageInput p-0">
|
||||
<div class="row">
|
||||
<div :class="`col-12 ${_preview ? 'col-md-6' : ''} form-group`">
|
||||
<label v-if="_preview" :for="_id" class="col-form-label"
|
||||
><b>{{ __("Choisir un fichier") }}</b></label
|
||||
>
|
||||
<label v-if="_preview" :for="_id" class="col-form-label">
|
||||
<b>{{ __("Choisir un fichier") }}</b>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<button
|
||||
v-on:click.prevent="chooseAFile"
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
type="file"
|
||||
class="d-none"
|
||||
:name="_name"
|
||||
accept="image/png,image/jpeg,.svg"
|
||||
accept=".jpg,.jpeg,.gif,.png"
|
||||
:aria-describedby="`Help${_id}`"
|
||||
/>
|
||||
<input
|
||||
|
|
@ -89,11 +89,13 @@
|
|||
v-if="_value"
|
||||
:id="`PreviewHelp${_id}`"
|
||||
class="form-text text-muted d-block"
|
||||
>{{ __("Prévisualisation de l'image")
|
||||
}}<span v-if="_originalFile" class="fw-bold"
|
||||
> {{ humanFileSize(_originalFile.size) }} </span
|
||||
></small
|
||||
>
|
||||
>{{ __("Prévisualisation de l'image") }}
|
||||
<span v-if="_originalFile" class="fw-bold">
|
||||
{{ humanFileSize(_originalFile.size) }}
|
||||
{{ `hauteur ${_originalFileDimensions.height}px` }}
|
||||
{{ `largeur ${_originalFileDimensions.width}px` }}
|
||||
</span>
|
||||
</small>
|
||||
<img
|
||||
v-if="_value"
|
||||
class="img-fluid"
|
||||
|
|
@ -114,7 +116,9 @@
|
|||
<div class="modal-dialog modal-xl" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{ __("Modification d'image avant import") }}</h5>
|
||||
<h5 class="modal-title">
|
||||
{{ __("Modification d'image avant import") }}
|
||||
</h5>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-close"
|
||||
|
|
@ -236,7 +240,11 @@
|
|||
</div>
|
||||
<div class="mt-1 ms-4 rotateDrag">
|
||||
<label for="range" class="form-label me-4"
|
||||
>{{ __(":deg degrès", { deg: String(_rotationText) }) }}
|
||||
>{{
|
||||
__(":deg degrès", {
|
||||
deg: String(_rotationText),
|
||||
})
|
||||
}}
|
||||
</label>
|
||||
<input
|
||||
ref="rotationSlider"
|
||||
|
|
@ -300,7 +308,9 @@
|
|||
<div class="modal-dialog modal-xl" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{ __("Prévisualisation") }}</h5>
|
||||
<h5 class="modal-title">
|
||||
{{ __("Prévisualisation") }}
|
||||
</h5>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-close"
|
||||
|
|
@ -352,7 +362,9 @@ export default defineComponent({
|
|||
document.querySelectorAll('.ImageInput [data-bs-toggle="tooltip"]')
|
||||
);
|
||||
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new Tooltip(tooltipTriggerEl, { delay: { show: 1000, hide: 300 } });
|
||||
return new Tooltip(tooltipTriggerEl, {
|
||||
delay: { show: 1000, hide: 300 },
|
||||
});
|
||||
});
|
||||
if (json.length) {
|
||||
// * Init uppon Laravel Blade
|
||||
|
|
@ -453,6 +465,10 @@ export default defineComponent({
|
|||
_cropper: Cropper | null;
|
||||
|
||||
_originalFile: File | null;
|
||||
_originalFileDimensions: {
|
||||
height: number;
|
||||
width: number;
|
||||
};
|
||||
_originalFileName: string;
|
||||
_previewStyle: string;
|
||||
} {
|
||||
|
|
@ -478,6 +494,10 @@ export default defineComponent({
|
|||
_cropper: null,
|
||||
|
||||
_originalFile: null,
|
||||
_originalFileDimensions: {
|
||||
height: 0,
|
||||
width: 0,
|
||||
},
|
||||
_originalFileName: "",
|
||||
_previewStyle: "",
|
||||
};
|
||||
|
|
@ -539,25 +559,25 @@ export default defineComponent({
|
|||
},
|
||||
reset() {
|
||||
if (!(this._cropper instanceof Cropper)) {
|
||||
throw new Error("cropped not initialized");
|
||||
return;
|
||||
}
|
||||
this._cropper.reset();
|
||||
},
|
||||
zoom(v: number) {
|
||||
if (!(this._cropper instanceof Cropper)) {
|
||||
throw new Error("cropped not initialized");
|
||||
return;
|
||||
}
|
||||
this._cropper.zoom(v);
|
||||
},
|
||||
move(x: number, y: number) {
|
||||
if (!(this._cropper instanceof Cropper)) {
|
||||
throw new Error("cropped not initialized");
|
||||
return;
|
||||
}
|
||||
this._cropper.move(x, y);
|
||||
},
|
||||
scale(horizontal: boolean) {
|
||||
if (!(this._cropper instanceof Cropper)) {
|
||||
throw new Error("cropped not initialized");
|
||||
return;
|
||||
}
|
||||
if (horizontal) {
|
||||
this._scaleX = -1 / this._scaleX;
|
||||
|
|
@ -625,6 +645,7 @@ export default defineComponent({
|
|||
if (el.files[0]) {
|
||||
self._originalFile = el.files[0];
|
||||
reader.readAsDataURL(el.files[0]);
|
||||
reader.onloadend = () => this.fetchDataUrlImageSizes(String(reader.result));
|
||||
this._valueFileName = el.files[0].name;
|
||||
}
|
||||
},
|
||||
|
|
@ -686,12 +707,26 @@ export default defineComponent({
|
|||
}
|
||||
return new Blob([u8arr], { type: mime[1] });
|
||||
},
|
||||
fetchDataUrlImageSizes(dataUrl: string) {
|
||||
const img = new Image(),
|
||||
self = this;
|
||||
try {
|
||||
img.onload = () => {
|
||||
self._originalFileDimensions.width = img.width;
|
||||
self._originalFileDimensions.height = img.height;
|
||||
};
|
||||
img.src = dataUrl;
|
||||
} catch (e) {
|
||||
self._originalFileDimensions.width = img.width;
|
||||
self._originalFileDimensions.height = img.height;
|
||||
}
|
||||
},
|
||||
exportToBlob() {
|
||||
const self = this,
|
||||
actualImage = this.$refs.actualImage as HTMLInputElement,
|
||||
container = new DataTransfer();
|
||||
if (!(self._cropper instanceof Cropper)) {
|
||||
throw new Error("cropped not initialized");
|
||||
return;
|
||||
}
|
||||
if (!actualImage.files) {
|
||||
throw new Error();
|
||||
|
|
@ -706,6 +741,7 @@ export default defineComponent({
|
|||
imageSmoothingQuality: "high",
|
||||
})
|
||||
.toDataURL("image/png");
|
||||
this.fetchDataUrlImageSizes(self._value);
|
||||
const file = new File([self.dataURLtoBlob(self._value)], self._originalFileName, {
|
||||
type: "image/png",
|
||||
lastModified: new Date().getTime(),
|
||||
|
|
|
|||
97
resources/ts/components/bo/InputTextAutoComponent.vue
Normal file
97
resources/ts/components/bo/InputTextAutoComponent.vue
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
<template>
|
||||
<WordCounter :id="_id" />
|
||||
<textarea
|
||||
ref="textarea"
|
||||
:id="_id"
|
||||
:class="`inputTextAutoComponent ${_class}`"
|
||||
:name="_name"
|
||||
:aria-describedby="_ariaDescribedby"
|
||||
:placeholder="_placeholder"
|
||||
:required="_required"
|
||||
v-on:input="onInput"
|
||||
:json="'{}'"
|
||||
>{{ _value }}</textarea
|
||||
>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, ref } from "vue";
|
||||
import trans from "../../modules/trans";
|
||||
import WordCounter from "./WordCounterComponent.vue";
|
||||
|
||||
const textarea = ref<HTMLTextAreaElement | null>(null);
|
||||
|
||||
export default defineComponent({
|
||||
inheritAttrs: false,
|
||||
mixins: [trans],
|
||||
components: {
|
||||
WordCounter,
|
||||
},
|
||||
setup() {
|
||||
return { textarea };
|
||||
},
|
||||
beforeMount() {
|
||||
if (!this.$attrs.json) {
|
||||
throw new Error("this component requires json");
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const json = String(this.$attrs.json ?? "{}"),
|
||||
data = JSON.parse(json);
|
||||
this._id = String(data.id);
|
||||
this._class = String(data.class);
|
||||
this._name = String(data.name);
|
||||
this._placeholder = String(data.placeholder);
|
||||
this._ariaDescribedby = String(data.ariaDescribedby);
|
||||
this._value = String(data.value);
|
||||
if (data.required) {
|
||||
this._required = true;
|
||||
}
|
||||
if (this.textarea) {
|
||||
// Default input init
|
||||
this.textarea.setAttribute("style", `height: 1rem;overflow-y: hidden;`);
|
||||
}
|
||||
// * Init input
|
||||
this.$nextTick(this.onInput);
|
||||
},
|
||||
data(): {
|
||||
_id: string;
|
||||
_class: string;
|
||||
_name: string;
|
||||
_placeholder: string;
|
||||
_ariaDescribedby: string;
|
||||
_required: boolean;
|
||||
_value: string;
|
||||
_textarea: HTMLTextAreaElement | null;
|
||||
} {
|
||||
return {
|
||||
_id: "",
|
||||
_class: "",
|
||||
_name: "",
|
||||
_placeholder: "",
|
||||
_ariaDescribedby: "",
|
||||
_required: false,
|
||||
_value: "",
|
||||
_textarea: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onInput() {
|
||||
const el = textarea.value;
|
||||
if (!el) {
|
||||
throw new Error("undefined textarea element");
|
||||
}
|
||||
// * Remove every new line char
|
||||
el.value = el.value.replace(/[\n\r]/, "");
|
||||
el.style.height = "auto";
|
||||
el.style.height = `${el.scrollHeight}px`;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.inputTextAutoComponent {
|
||||
resize: none;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,15 +1,30 @@
|
|||
<template>
|
||||
<div class="taggableDropdown">
|
||||
<VueTagsInputCls
|
||||
v-model="tag"
|
||||
:tags="tags"
|
||||
:autocomplete-items="filteredItems"
|
||||
:autocomplete-min-length="0"
|
||||
@before-adding-tag="beforeTagSave"
|
||||
@tags-changed="changedTags"
|
||||
:max-tags="6"
|
||||
placeholder="Ajoutez ici vos étiquettes"
|
||||
/>
|
||||
<div v-if="errorMessages.length">
|
||||
<span class="d-block text-danger" v-for="errorMessage of errorMessages">
|
||||
{{ errorMessage }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-inline-block tagInput">
|
||||
<VueTagsInputCls
|
||||
v-model="tag"
|
||||
:tags="tags"
|
||||
:autocomplete-items="filteredItems"
|
||||
:autocomplete-min-length="0"
|
||||
@before-adding-tag="beforeTagSave"
|
||||
@tags-changed="changedTags"
|
||||
:max-tags="6"
|
||||
:placeholder="__('Ajoutez ici vos étiquettes')"
|
||||
/>
|
||||
<div class="text-center py-2" v-if="waitingListObjects.length">
|
||||
<button class="btn btn-sm btn-primary ms-2" v-on:click.prevent="addTags">
|
||||
{{ __("Créer et ajouter à la liste") }}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-dark ms-2" v-on:click.prevent="cancelTags">
|
||||
{{ __("Annuler") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<template v-for="(sTag, index) in tags" :key="index">
|
||||
<input :name="`${name}[${String(index)}][id]`" type="hidden" :value="sTag.id" />
|
||||
<input :name="`${name}[${String(index)}][name]`" type="hidden" :value="sTag.text" />
|
||||
|
|
@ -21,11 +36,13 @@
|
|||
import { defineComponent } from "vue";
|
||||
import type VueTagsInput from "@sipec/vue3-tags-input";
|
||||
import { VueTagsInput as VueTagsInputCls } from "@sipec/vue3-tags-input";
|
||||
import slugify from "slugify";
|
||||
import route from "../../modules/route";
|
||||
import toast from "../../modules/toast";
|
||||
import trans from "../../modules/trans";
|
||||
|
||||
export default defineComponent({
|
||||
mixins: [toast, route],
|
||||
mixins: [toast, route, trans],
|
||||
components: {
|
||||
VueTagsInputCls,
|
||||
},
|
||||
|
|
@ -35,7 +52,7 @@ export default defineComponent({
|
|||
}
|
||||
},
|
||||
mounted() {
|
||||
const json = String(this.$attrs.json ?? ""),
|
||||
const json = String(this.$attrs.json ?? "{}"),
|
||||
data = JSON.parse(json);
|
||||
this.name = data.name;
|
||||
this.tags = this.parseTags(data.value);
|
||||
|
|
@ -46,42 +63,84 @@ export default defineComponent({
|
|||
tag: string;
|
||||
tags: Array<CustomTag>;
|
||||
autocompleteItems: Array<CustomTag>;
|
||||
waitingListObjects: Array<VueTagsInput.IAddArgs>;
|
||||
errorMessages: Array<string>;
|
||||
} {
|
||||
return {
|
||||
name: "",
|
||||
tag: "",
|
||||
tags: [],
|
||||
autocompleteItems: [],
|
||||
waitingListObjects: [],
|
||||
errorMessages: [],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
parseTags(tags: any) {
|
||||
parseTags(tags: Array<CustomTag>): Array<CustomTag> {
|
||||
return tags.map((tag: CustomTag) => {
|
||||
tag.text = String(tag.name);
|
||||
tag.slug = String(tag.slug);
|
||||
return tag;
|
||||
});
|
||||
},
|
||||
beforeTagSave(obj: VueTagsInput.IAddArgs) {
|
||||
const self = this;
|
||||
if (
|
||||
self.autocompleteItems.filter((tag: CustomTag) => {
|
||||
return tag.id === (obj.tag as CustomTag).id;
|
||||
}).length
|
||||
) {
|
||||
obj.addTag();
|
||||
addTags(): void {
|
||||
for (const obj of this.waitingListObjects) {
|
||||
this.ajaxSaveTag(obj);
|
||||
}
|
||||
},
|
||||
cancelTags(): void {
|
||||
this.$nextTick(() => {
|
||||
this.waitingListObjects = [];
|
||||
this.tag = "";
|
||||
this.showErrorMessages([]);
|
||||
});
|
||||
},
|
||||
beforeTagSave(obj: VueTagsInput.IAddArgs): void {
|
||||
this.waitingListObjects = [];
|
||||
this.showErrorMessages([]);
|
||||
// * Tag exists in selected tags
|
||||
if (this.tagExists(this.tags, obj.tag)) {
|
||||
this.showErrorMessages([this.__("Cette étiquette est déjà dans la liste.")]);
|
||||
return;
|
||||
}
|
||||
const storeTagRoute = route.methods.route("bo.tags.store");
|
||||
// * Tag exists in local list
|
||||
let customTag = this.tagExists(this.autocompleteItems, obj.tag);
|
||||
if (customTag) {
|
||||
obj.tag = customTag;
|
||||
this.tags.push(customTag);
|
||||
this.tag = "";
|
||||
return;
|
||||
}
|
||||
// * Add tag in waiting list
|
||||
this.waitingListObjects.push(obj);
|
||||
},
|
||||
ajaxSaveTag(obj: VueTagsInput.IAddArgs): void {
|
||||
const self = this,
|
||||
storeTagRoute = route.methods.route("bo.tags.jsonStore");
|
||||
if (!storeTagRoute) {
|
||||
throw new Error("Undefined route bo.tags.store");
|
||||
throw new Error("Undefined route bo.tags.jsonStore");
|
||||
}
|
||||
window.axios
|
||||
.post(storeTagRoute, { name: this.tag })
|
||||
.post(storeTagRoute, { name: self.tag })
|
||||
.then((reponse) => {
|
||||
this.autocompleteItems.push(this.parseTags([reponse.data])[0]);
|
||||
obj.addTag();
|
||||
const parsedTag = self.parseTags([reponse.data])[0] as CustomTag,
|
||||
tag = obj.tag as CustomTag;
|
||||
self.autocompleteItems.push(parsedTag);
|
||||
tag.id = parsedTag.id;
|
||||
tag.slug = parsedTag.slug;
|
||||
obj.tag = tag;
|
||||
this.tags.push(tag);
|
||||
})
|
||||
.catch((error) => {
|
||||
let message = "Une erreur est survenue merci de ressayer plus tard";
|
||||
if (error.response.status === 422) {
|
||||
message =
|
||||
error.response?.data?.errors?.name?.[0] ||
|
||||
error.response?.data?.errors?.slug?.[0] ||
|
||||
message;
|
||||
this.showErrorMessages([message]);
|
||||
return;
|
||||
}
|
||||
if (error.response) {
|
||||
console.error(error.response.data);
|
||||
} else if (error.request) {
|
||||
|
|
@ -90,11 +149,15 @@ export default defineComponent({
|
|||
// Something happened in setting up the request that triggered an Error
|
||||
console.error("Error", error.message);
|
||||
}
|
||||
self.errorNotify("Une erreur est survenue merci de ressayer plus tard");
|
||||
self.errorNotify(message);
|
||||
})
|
||||
.then(() => {
|
||||
this.waitingListObjects = [];
|
||||
this.tag = "";
|
||||
});
|
||||
},
|
||||
beforeTagDelete(obj: VueTagsInput.IDeleteArgs) {
|
||||
let self = this;
|
||||
beforeTagDelete(obj: VueTagsInput.IDeleteArgs): void {
|
||||
const self = this;
|
||||
if (obj.tag) {
|
||||
const destroyTagRoute = route.methods.route("bo.tags.destroy");
|
||||
if (!destroyTagRoute) {
|
||||
|
|
@ -109,9 +172,24 @@ export default defineComponent({
|
|||
});
|
||||
}
|
||||
},
|
||||
changedTags(newTags: Array<CustomTag>) {
|
||||
changedTags(newTags: Array<CustomTag>): void {
|
||||
this.tags = newTags as Array<CustomTag>;
|
||||
},
|
||||
tagExists(items: Array<CustomTag>, tagToCheck: VueTagsInput.ITag): CustomTag | null {
|
||||
let customTag: CustomTag | null = null;
|
||||
items.filter((tag: CustomTag) => {
|
||||
if (
|
||||
tag.id === (tagToCheck as CustomTag).id ||
|
||||
tag.slug === slugify(tagToCheck.text)
|
||||
) {
|
||||
customTag = tag;
|
||||
}
|
||||
});
|
||||
return customTag;
|
||||
},
|
||||
showErrorMessages(messages: Array<string>): void {
|
||||
this.errorMessages = messages;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
filteredItems() {
|
||||
|
|
@ -124,10 +202,23 @@ export default defineComponent({
|
|||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "../../../../node_modules/bootstrap/scss/bootstrap-utilities.scss";
|
||||
|
||||
.taggableDropdown {
|
||||
.ti-autocomplete {
|
||||
overflow-y: scroll !important;
|
||||
max-height: 10rem !important;
|
||||
.tagInput {
|
||||
width: 100%;
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
.vue-tags-input {
|
||||
max-width: inherit !important;
|
||||
}
|
||||
.ti-autocomplete {
|
||||
overflow-y: scroll !important;
|
||||
max-height: 10rem !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
<div class="wordCounter">
|
||||
<span class="words"
|
||||
><u>{{ __("Mots") }}</u
|
||||
> : <b>{{ words }}</b></span
|
||||
> : <b>{{ _words }}</b></span
|
||||
>
|
||||
<span class="chars"
|
||||
><u>{{ __("Caractères") }}</u
|
||||
> : <b>{{ chars }}</b></span
|
||||
> : <b>{{ _chars }}</b></span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -17,63 +17,81 @@ import trans from "../../modules/trans";
|
|||
|
||||
export default defineComponent({
|
||||
mixins: [trans],
|
||||
beforeMount() {
|
||||
if (!this.$attrs.json) {
|
||||
throw new Error("this component requires json");
|
||||
mounted() {
|
||||
const json = String(this.$attrs.json ?? "{}"),
|
||||
data = JSON.parse(json);
|
||||
this._id = String(this.id);
|
||||
if (data.id) {
|
||||
this._id = data.id;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const json = String(this.$attrs.json ?? ""),
|
||||
data = JSON.parse(json);
|
||||
this.id = data.id;
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data(): {
|
||||
id: string;
|
||||
words: number;
|
||||
chars: number;
|
||||
input: HTMLInputElement | null;
|
||||
_id: string;
|
||||
_words: number;
|
||||
_chars: number;
|
||||
_input: HTMLInputElement | HTMLTextAreaElement | null;
|
||||
} {
|
||||
return {
|
||||
id: "",
|
||||
words: 0,
|
||||
chars: 0,
|
||||
input: null,
|
||||
_id: "",
|
||||
_words: 0,
|
||||
_chars: 0,
|
||||
_input: null,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
id() {
|
||||
if (!this.id) {
|
||||
throw new Error("Missing ID");
|
||||
}
|
||||
const input = document.getElementById(this.id);
|
||||
if (!input || !(input instanceof HTMLInputElement)) {
|
||||
throw new Error(`Input ${this.id} could not be found`);
|
||||
}
|
||||
this.input = input;
|
||||
this._id = this.id;
|
||||
},
|
||||
input() {
|
||||
if (!this.input) {
|
||||
throw new Error("input has to be set");
|
||||
_id() {
|
||||
if (!this._id) {
|
||||
return;
|
||||
}
|
||||
if (this.input && !(this.input instanceof HTMLInputElement)) {
|
||||
throw new Error("input as to be HTMLInputElement");
|
||||
this.$nextTick(() => {
|
||||
const input = document.getElementById(this._id);
|
||||
if (
|
||||
!input ||
|
||||
!(input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement)
|
||||
) {
|
||||
throw new Error(`${this._id} is not a input or textarea`);
|
||||
}
|
||||
this._input = input;
|
||||
});
|
||||
},
|
||||
_input() {
|
||||
if (
|
||||
!this._input ||
|
||||
!(
|
||||
this._input instanceof HTMLInputElement ||
|
||||
this._input instanceof HTMLTextAreaElement
|
||||
)
|
||||
) {
|
||||
throw new Error(`${this._id} is not a input or textarea`);
|
||||
}
|
||||
this.input.addEventListener("input", (e) => {
|
||||
this._input.addEventListener("input", (e) => {
|
||||
const el = e.target;
|
||||
if (!el || !(el instanceof HTMLInputElement)) {
|
||||
throw new Error();
|
||||
if (
|
||||
!el ||
|
||||
!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)
|
||||
) {
|
||||
throw new Error(`${this._id} is not a input or textarea`);
|
||||
}
|
||||
this.updateValues(el.value);
|
||||
});
|
||||
this.updateValues(this.input.value);
|
||||
this.updateValues(this._input.value);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateValues(string: string) {
|
||||
let chars, words;
|
||||
[words, chars] = this.calculateValues(string);
|
||||
this.words = words;
|
||||
this.chars = chars;
|
||||
this._words = words;
|
||||
this._chars = chars;
|
||||
},
|
||||
calculateValues(string: string) {
|
||||
const arr = string.split(" ");
|
||||
|
|
|
|||
5
resources/ts/components/shims-vue.d.ts
vendored
Normal file
5
resources/ts/components/shims-vue.d.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
declare module "*.vue" {
|
||||
import type { DefineComponent } from "vue";
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
|
|
@ -1,21 +1,22 @@
|
|||
export default {
|
||||
const route = {
|
||||
methods: {
|
||||
route(
|
||||
routeName: string,
|
||||
parameters: Record<string, number | string> = {}
|
||||
): string | null {
|
||||
let route =
|
||||
Object.keys(window.__SYSTEM._routes).indexOf(routeName) !== -1
|
||||
? window.__SYSTEM._routes[routeName]
|
||||
const routes = route.flattenProperties(window.__SYSTEM._routes);
|
||||
const routeUrl =
|
||||
Object.keys(routes).indexOf(routeName) !== -1
|
||||
? routes[routeName]
|
||||
: null;
|
||||
if (!route) {
|
||||
return route;
|
||||
if (!routeUrl) {
|
||||
return routeUrl;
|
||||
}
|
||||
// copy parameters
|
||||
const iParameters = Object.assign({}, parameters),
|
||||
origin = window.location.origin,
|
||||
hash = window.location.hash;
|
||||
let pathname = route.replace(origin, ""),
|
||||
let pathname = routeUrl.replace(origin, ""),
|
||||
search = window.location.search;
|
||||
|
||||
// Replace in path name.
|
||||
|
|
@ -55,4 +56,21 @@ export default {
|
|||
return `${origin}${pathname}${search}${hash}`;
|
||||
},
|
||||
},
|
||||
flattenProperties(
|
||||
obj: Record<PropertyKey, any>,
|
||||
parent: string | null = null,
|
||||
res: Record<PropertyKey, any> = {}
|
||||
): Record<PropertyKey, any> {
|
||||
for (const key of Object.keys(obj)) {
|
||||
const propName = parent ? parent + "." + key : key;
|
||||
if (typeof obj[key] === "object") {
|
||||
route.flattenProperties(obj[key], propName, res);
|
||||
} else {
|
||||
res[propName] = obj[key];
|
||||
}
|
||||
}
|
||||
return res;
|
||||
},
|
||||
};
|
||||
|
||||
export default route;
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="remember" id="remember" {{ old('remember') ? 'checked' : '' }}>
|
||||
|
||||
<label class="form-check-label" for="remember">
|
||||
<label class="form-check-label user-select-none" for="remember">
|
||||
{{ __('Se souvenir de moi') }}
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
@break
|
||||
@case(InputType::boolean()->value)
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<label class="user-select-none">
|
||||
<input type="checkbox" name="data[{{$k}}][value]" value="1" @if (( old("data.$k.value") ?? $input['value'] ?? 0 ))checked @endif>
|
||||
{{ $input['label'] }}*
|
||||
</label>
|
||||
|
|
|
|||
|
|
@ -8,5 +8,13 @@
|
|||
if(!window.__SYSTEM._routes) {
|
||||
window.__SYSTEM._routes = {}
|
||||
}
|
||||
window.__SYSTEM._routes['crud-policies.upload'] = '{{ route('crud-policies.upload') }}';
|
||||
window.__SYSTEM._routes['crud-policies'] = {
|
||||
upload: '{{ route('crud-policies.upload') }}'
|
||||
};
|
||||
window.__SYSTEM._routes.bo = {
|
||||
tags: {
|
||||
store: '{{ route('bo.tags.store') }}',
|
||||
jsonStore: '{{ route('bo.tags.jsonStore') }}'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
<div class="col-12 col-md-6 form-group">
|
||||
<label for="newsModelPublished" class="col-form-label"><b>{{ __('Publication') }}</b></label>
|
||||
@if(isset($newsModel) and $newsModel->published->equals(App\Enums\PublishedStatus::published()))
|
||||
<span class="col-form-label ms-3 fst-italic">{{ __('Publié le :date', ['date' => $newsModel->published_at->isoFormat('DD/MM/YYYY à HH:mm')]) }}</span>
|
||||
<span class="col-form-label ms-3 fst-italic">{{ __('Publié le :date', ['date' => $newsModel->published_at->formatLocalized('%d/%m/%Y à %I:%M')]) }}</span>
|
||||
@endif
|
||||
<select id="newsModelPublished" class="form-select" name="published" aria-describedby="newsModelPublishedHelp" required>
|
||||
<option value="" @if(is_null(old('published', $newsModel->published ?? null))) selected @endif disabled>{{ __('Séléctionnez un statut de publication') }}</option>
|
||||
|
|
@ -44,18 +44,29 @@
|
|||
<label for="newsModelSeoTitle" class="col-form-label"><b>{{ __('Titre seo') }}</b></label>
|
||||
<div class="word-counter" data-json='@json(['id' => 'newsModelSeoTitle'])'></div>
|
||||
<input name="seo_title" type="text" value="{{ old('seo_title', $newsModel->seo_title ?? '') }}" class="form-control"
|
||||
id="newsModelSeoTitle" aria-describedby="newsModelSeoTitleHelp" placeholder="{{ __('Un titre en 60 caractères maximum') }}" required>
|
||||
id="newsModelSeoTitle" aria-describedby="newsModelSeoTitleHelp" placeholder="{{ __('Un titre en 70 caractères maximum') }}" required>
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'seo_title', 'helperId' => 'newsModelSeoTitleHelp' , 'helper' => __('Doit décrire précisément l\'article, requiert entre 53 et 60 caratères, balise <title>')
|
||||
'inputName' => 'seo_title', 'helperId' => 'newsModelSeoTitleHelp' , 'helper' => __('Doit décrire précisément l\'article, recommandation pour un positionnement optimal dans les moteurs de recherche : entre 53 et 60 caractères, balise <title> ')
|
||||
])
|
||||
</div>
|
||||
<div class="col-12 col-md-6 form-group">
|
||||
<label for="newsModelSeoDescription" class="col-form-label"><b>{{ __('Description seo') }}</b></label>
|
||||
<div class="word-counter" data-json='@json(['id' => 'newsModelSeoDescription'])'></div>
|
||||
<input name="seo_description" type="text" value="{{ old('seo_description', $newsModel->seo_description ?? '') }}" class="form-control"
|
||||
id="newsModelSeoDescription" aria-describedby="newsModelSeoDescriptionHelp" placeholder="{{ __('Une description de l\'article pour les moteurs de recherche') }}" required>
|
||||
@php
|
||||
$data = [
|
||||
'id' => 'newsModelSeoDescription',
|
||||
'class' => 'form-control',
|
||||
'name' => 'seo_description',
|
||||
'placeholder' => __('Une description de l\'article pour les moteurs de recherche'),
|
||||
'ariaDescribedby' => 'newsModelSeoDescriptionHelp',
|
||||
'value' => old('tags', $newsModel->tags ?? []),
|
||||
'tags' => $tags,
|
||||
'required' => true,
|
||||
'value' => old('seo_description', $newsModel->seo_description ?? '')
|
||||
];
|
||||
@endphp
|
||||
<div id="input-text-auto" data-json='@json($data)'></div>
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'seo_description', 'helperId' => 'newsModelSeoDescriptionHelp' , 'helper' => __('Doit décrire précisément l\'article, requiert entre 50 et 160 caratères, balise <meta> keywords')
|
||||
'inputName' => 'seo_description', 'helperId' => 'newsModelSeoDescriptionHelp' , 'helper' => __(' Doit décrire précisément l\'article, requiert entre 50 et 160 caractères, balise <meta> description')
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="input-group justify-content-end">
|
||||
<a href="{{ route('bo.news.create') }}" class="btn btn-primary" title="{{ __('Ajouter nouveau :model', [
|
||||
<a href="{{ route('bo.news.create') }}" class="btn btn-primary" title="{{ __('Ajouter une :model', [
|
||||
'model' => Str::singular(__('models.classes.news'))
|
||||
]) }}" data-bs-toggle="tooltip"><i class="fa-solid fa-plus"></i></a>
|
||||
</div>
|
||||
|
|
@ -34,74 +34,76 @@
|
|||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table class="table">
|
||||
<thead>
|
||||
@php
|
||||
$cols = ['title' => __('Titre'), 'tags' => __('Tags'), 'published' => __('Publication'), 'updated_at' => __('Mis à jour le')];
|
||||
@endphp
|
||||
@include('back.modules.tableColSorter', [ 'cols' => $cols, 'ignore' => ['tags']])
|
||||
</thead>
|
||||
<tbody>
|
||||
@if($news->count())
|
||||
@foreach ($news as $newsModel)
|
||||
<tr>
|
||||
<td>
|
||||
<span>{{ $newsModel->title }}</span>
|
||||
</td>
|
||||
<td>
|
||||
@foreach($newsModel->tags as $tag)
|
||||
<span class="badge bg-primary text-wrap">{{ $tag->name }}</span>
|
||||
@endforeach
|
||||
</td>
|
||||
<td class="text-center">
|
||||
@switch($newsModel->published)
|
||||
@case(App\Enums\PublishedStatus::draft())
|
||||
<span class="badge bg-warning text-wrap">{{ $newsModel->published->label }}</span>
|
||||
@break
|
||||
@case(App\Enums\PublishedStatus::published())
|
||||
<span class="badge bg-success text-wrap">{{ $newsModel->published_at ? __(':status le :date', [
|
||||
'status' => $newsModel->published->label,
|
||||
'date' => $newsModel->published_at->isoFormat('DD/MM/YYYY à HH:mm')
|
||||
]) : '' }}</span>
|
||||
@break
|
||||
@case(App\Enums\PublishedStatus::archived())
|
||||
<span class="badge bg-secondary text-wrap">{{ $newsModel->published->label}}</span>
|
||||
@break
|
||||
@endswitch
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge text-dark text-wrap">{{ $newsModel->updated_at_date }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group">
|
||||
{{-- Protected Preview Page --}}
|
||||
@if($newsModel->published->equals(App\Enums\PublishedStatus::draft()))
|
||||
<a class="btn btn-sm btn-warning" href="" target="_blank" title="{{ __('Prévisualiser :model', [
|
||||
'model' => Str::singular(__('models.classes.news'))
|
||||
]) }}" data-bs-toggle="tooltip"><i class="fa-solid fa-user-lock"></i></a>
|
||||
@endif
|
||||
{{-- Public View Page --}}
|
||||
@if($newsModel->published->equals(App\Enums\PublishedStatus::published()))
|
||||
<a class="btn btn-sm btn-warning" href="" target="_blank" title="{{ __('Visualiser :model', [
|
||||
'model' => Str::singular(__('models.classes.news'))
|
||||
]) }}" data-bs-toggle="tooltip"><i class="fa-solid fa-eye"></i></a>
|
||||
@endif
|
||||
<a class="btn btn-sm btn-primary" href="{{ route('bo.news.edit', $newsModel->id) }}" title="{{ __('Modifier :model', [
|
||||
'model' => Str::singular(__('models.classes.news'))
|
||||
]) }}" data-bs-toggle="tooltip"><i class="fa-solid fa-pencil"></i></a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@else
|
||||
<tr>
|
||||
<td colspan="5" class="text-center"><span class="h3">{{
|
||||
__('Aucun résultat pour les :models', ['models' => __('models.classes.news')])
|
||||
}}</span></td>
|
||||
</tr>
|
||||
@endif
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="table-responsive">
|
||||
<table class="table fixAction">
|
||||
<thead>
|
||||
@php
|
||||
$cols = ['title' => __('Titre'), 'tags' => __('Tags'), 'published' => __('Publication'), 'updated_at' => __('Mis à jour le')];
|
||||
@endphp
|
||||
@include('back.modules.tableColSorter', [ 'cols' => $cols, 'ignore' => ['tags']])
|
||||
</thead>
|
||||
<tbody>
|
||||
@if($news->count())
|
||||
@foreach ($news as $newsModel)
|
||||
<tr>
|
||||
<td>
|
||||
<span>{{ $newsModel->title }}</span>
|
||||
</td>
|
||||
<td>
|
||||
@foreach($newsModel->tags as $tag)
|
||||
<span class="badge bg-primary text-wrap">{{ $tag->name }}</span>
|
||||
@endforeach
|
||||
</td>
|
||||
<td class="text-center">
|
||||
@switch($newsModel->published)
|
||||
@case(App\Enums\PublishedStatus::draft())
|
||||
<span class="badge bg-warning text-wrap">{{ $newsModel->published->label }}</span>
|
||||
@break
|
||||
@case(App\Enums\PublishedStatus::published())
|
||||
<span class="badge bg-success text-wrap">{{ $newsModel->published_at ? __(':status le :date', [
|
||||
'status' => $newsModel->published->label,
|
||||
'date' => $newsModel->published_at->formatLocalized('%d/%m/%Y à %I:%M')
|
||||
]) : '' }}</span>
|
||||
@break
|
||||
@case(App\Enums\PublishedStatus::archived())
|
||||
<span class="badge bg-secondary text-wrap">{{ $newsModel->published->label}}</span>
|
||||
@break
|
||||
@endswitch
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge text-dark text-wrap">{{ $newsModel->updated_at->formatLocalized('%d/%m/%Y à %I:%M') }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group">
|
||||
{{-- Protected Preview Page --}}
|
||||
@if($newsModel->published->equals(App\Enums\PublishedStatus::draft()))
|
||||
<a class="btn btn-sm btn-warning" href="" target="_blank" title="{{ __('Prévisualiser :model', [
|
||||
'model' => Str::singular(__('models.classes.news'))
|
||||
]) }}" data-bs-toggle="tooltip"><i class="fa-solid fa-user-lock"></i></a>
|
||||
@endif
|
||||
{{-- Public View Page --}}
|
||||
@if($newsModel->published->equals(App\Enums\PublishedStatus::published()))
|
||||
<a class="btn btn-sm btn-warning" href="" target="_blank" title="{{ __('Visualiser :model', [
|
||||
'model' => Str::singular(__('models.classes.news'))
|
||||
]) }}" data-bs-toggle="tooltip"><i class="fa-solid fa-eye"></i></a>
|
||||
@endif
|
||||
<a class="btn btn-sm btn-primary" href="{{ route('bo.news.edit', $newsModel->id) }}" title="{{ __('Modifier :model', [
|
||||
'model' => Str::singular(__('models.classes.news'))
|
||||
]) }}" data-bs-toggle="tooltip"><i class="fa-solid fa-pencil"></i></a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@else
|
||||
<tr>
|
||||
<td colspan="5" class="text-center"><span class="h3">{{
|
||||
__('Aucun résultat pour les :models', ['models' => __('models.classes.news')])
|
||||
}}</span></td>
|
||||
</tr>
|
||||
@endif
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-center">
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
<div class="col">
|
||||
<h1 class="col h4">
|
||||
<a href="{{ route('bo.sliders.index') }}" class="text-decoration-none btn btn-sm btn-primary" role="button" aria-roledescription="{{ _('Revenir en arrière') }}"><i class="fa-solid fa-arrow-left-long"></i></a>
|
||||
<span> {{ __('models.classes.sliders') }} {{ __('édition') }}</span>
|
||||
<span> {{ __('models.classes.sliders') }} {{ __('édition') }} -> {{ $slider->type->label }}</span>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="col text-end">
|
||||
|
|
|
|||
|
|
@ -1,9 +1,30 @@
|
|||
@if(!isset($slider))
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<fieldset class="border p-3 mb-3">
|
||||
<div class="col-12 col-md-6 form-group">
|
||||
<label for="sliderType" class="col-form-label"><b>{{ __('Type de slider') }}</b></label>
|
||||
<select id="sliderType" class="form-select" name="type" aria-describedby="sliderTypeHelp" required>
|
||||
<option value="" @if(is_null(old('type', $slider->type ?? null))) selected @endif disabled>{{ __('Séléctionnez un type') }}</option>
|
||||
@foreach(App\Enums\Sliders\SliderType::toArray() as $type)
|
||||
<option value="{{ $type->value }}" @if(old('type', optional($slider->type ?? null)->value) == $type->value) selected @endif>{{ $type->label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'type', 'helperId' => 'sliderTypeHelp' , 'helper' => __('Le type de slider à créer')
|
||||
])
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<fieldset class="border p-3 mb-3">
|
||||
<legend>{{ __('Titre et Sous-titre') }}</legend>
|
||||
<div class="row mb-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="col-12 col-md-6 mb-3">
|
||||
<div class="form-group">
|
||||
<label for="sliderTitle"><b>{{ __('Titre') }}</b></label>
|
||||
<div class="word-counter" data-json='@json(['id' => 'sliderTitle'])'></div>
|
||||
|
|
@ -14,56 +35,106 @@
|
|||
])
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="col-12 col-md-6 mb-3">
|
||||
<div class="form-group">
|
||||
<label for="sliderTitleColor"><b>{{ __('Couleur du titre') }}</b></label>
|
||||
<input name="title_color" type="color" class="form-control form-control-color" id="sliderTitleColor" value="{{ old('title_color', $slider->title_color ?? '') }}" title="{{ __('Choisi ta couleur') }}" aria-describedby="sliderTitleColorHelp">
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'title_color', 'helperId' => 'sliderTitleColorHelp' , 'helper' => __('Couleur du titre')
|
||||
])
|
||||
@php
|
||||
$data = [
|
||||
'id' => 'sliderTitleColor',
|
||||
'name' => 'title_color',
|
||||
'value' => old('title_color', optional($slider ?? null)->title_color),
|
||||
'title' => __('Choisi ta couleur'),
|
||||
'label' => __('Couleur du titre'),
|
||||
'rgbaMode' => true,
|
||||
'nullable' => true,
|
||||
'ariaDescribedby' => 'sliderTitleColorHelp'
|
||||
];
|
||||
@endphp
|
||||
@include('back.modules.input-error', [ 'inputName' => 'title_color'])
|
||||
<div class="color-picker" data-json='@json($data)'></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="col-12 col-md-6 mb-3">
|
||||
<div class="form-group">
|
||||
<label for="sliderTitleBgColor"><b>{{ __('Couleur de fond du titre') }}</b></label>
|
||||
<input name="title_color_bg" type="color" class="form-control form-control-color" id="sliderTitleBgColor" value="{{ old('title_color_bg', $slider->title_color_bg ?? '') }}" title="{{ __('Choisi ta couleur') }}" aria-describedby="sliderTitleBgColorHelp">
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'title_color_bg', 'helperId' => 'sliderTitleBgColorHelp' , 'helper' => __('Couleur de fond du titre')
|
||||
])
|
||||
@php
|
||||
$data = [
|
||||
'id' => 'sliderTitleBgColor',
|
||||
'name' => 'title_color_bg',
|
||||
'value' => old('title_color_bg', optional($slider ?? null)->title_color_bg),
|
||||
'title' => __('Choisi ta couleur'),
|
||||
'label' => __('Couleur de fond du titre'),
|
||||
'rgbaMode' => true,
|
||||
'nullable' => true,
|
||||
'ariaDescribedby' => 'sliderTitleBgColorHelp'
|
||||
];
|
||||
@endphp
|
||||
@include('back.modules.input-error', [ 'inputName' => 'title_color_bg'])
|
||||
<div class="color-picker" data-json='@json($data)'></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="col-12 col-md-6 mb-3">
|
||||
<div class="form-group">
|
||||
<label for="sliderSubTitle"><b>{{ __('Sous-titre') }}</b></label>
|
||||
<div class="word-counter" data-json='@json(['id' => 'sliderSubTitle'])'></div>
|
||||
<input name="subtitle" type="text" value="{{ old('subtitle', $slider->subtitle ?? '') }}" class="form-control"
|
||||
<input name="subtitle" type="text" value="{{ old('subtitle', optional($slider ?? null)->subtitle ?? '') }}" class="form-control"
|
||||
id="sliderSubTitle" aria-describedby="sliderSubTitleHelp" placeholder="{{ __('Un nom beau et explicite') }}" required>
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'subtitle', 'helperId' => 'sliderSubTitleHelp' , 'helper' => __('Doit décrire précisément le slider')
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="col-12 col-md-6 mb-3">
|
||||
<div class="form-group">
|
||||
<label for="sliderSubTitleColor"><b>{{ __('Couleur du titre') }}</b></label>
|
||||
<input name="subtitle_color" type="color" class="form-control form-control-color" id="sliderSubTitleColor" value="{{ old('subtitle_color', $slider->subtitle_color ?? '') }}" title="{{ __('Choisi ta couleur') }}" aria-describedby="sliderSubTitleColorHelp">
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'subtitle_color', 'helperId' => 'sliderSubTitleColorHelp' , 'helper' => __('Couleur du sous-titre')
|
||||
])
|
||||
@php
|
||||
$data = [
|
||||
'id' => 'sliderSubTitleColor',
|
||||
'name' => 'subtitle_color',
|
||||
'value' => old('subtitle_color', optional($slider ?? null)->subtitle_color),
|
||||
'title' => __('Choisi ta couleur'),
|
||||
'label' => __('Couleur du sous-titre'),
|
||||
'rgbaMode' => true,
|
||||
'nullable' => true,
|
||||
'ariaDescribedby' => 'sliderSubTitleColorHelp'
|
||||
];
|
||||
@endphp
|
||||
@include('back.modules.input-error', [ 'inputName' => 'subtitle_color'])
|
||||
<div class="color-picker" data-json='@json($data)'></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="col-12 col-md-6 mb-3">
|
||||
<div class="form-group">
|
||||
<label for="sliderSubTitleBgColor"><b>{{ __('Couleur de fond du titre') }}</b></label>
|
||||
<input name="subtitle_color_bg" type="color" class="form-control form-control-color" id="sliderSubTitleBgColor" value="{{ old('subtitle_color_bg', $slider->subtitle_color_bg ?? '') }}" title="{{ __('Choisi ta couleur') }}" aria-describedby="sliderSubTitleBgColorHelp">
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'subtitle_color_bg', 'helperId' => 'sliderSubTitleBgColorHelp' , 'helper' => __('Couleur de fond du sous-titre')
|
||||
])
|
||||
@php
|
||||
$data = [
|
||||
'id' => 'sliderSubTitleBgColor',
|
||||
'name' => 'subtitle_color_bg',
|
||||
'value' => old('subtitle_color_bg', optional($slider ?? null)->subtitle_color_bg),
|
||||
'title' => __('Choisi ta couleur'),
|
||||
'label' => __('Couleur de fond du sous-titre'),
|
||||
'rgbaMode' => true,
|
||||
'nullable' => true,
|
||||
'ariaDescribedby' => 'sliderSubTitleBgColorHelp'
|
||||
];
|
||||
@endphp
|
||||
@include('back.modules.input-error', [ 'inputName' => 'subtitle_color_bg'])
|
||||
<div class="color-picker" data-json='@json($data)'></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-12 col-md-6 form-group">
|
||||
<label for="sliderTextPosition" class="col-form-label"><b>{{ __('Position des textes') }}</b></label>
|
||||
<select id="sliderTextPosition" class="form-select" name="text_position_desktop" aria-describedby="sliderTextPositionHelp" required>
|
||||
<option value="" @if(is_null(old('text_position_desktop', $slider->text_position_desktop ?? null))) selected @endif disabled>{{ __('Séléctionnez une position de centrage') }}</option>
|
||||
@foreach(App\Enums\Sliders\SliderElementPosition::toArray() as $status)
|
||||
<option value="{{ $status->value }}" @if(old('text_position_desktop', optional($slider->text_position_desktop ?? null)->value) == $status->value) selected @endif>{{ $status->label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'text_position_desktop', 'helperId' => 'sliderTextPositionHelp' , 'helper' => __('Positionnement du titre/sous-titre et bouton sur l\'image (en desktop)')
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -72,18 +143,13 @@
|
|||
<div class="col">
|
||||
<fieldset class="border p-3 mb-3">
|
||||
<legend>{{ __('Publication') }}</legend>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<span class="h6 text-info">{{ __('La zone suivante à modifier la visibilité.') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<div class="form-group">
|
||||
<label for="sliderPublishedAt"><b>{{ __('Date de publication (Europe/Paris)') }}</b></label>
|
||||
<input id="sliderPublishedAt" name="publish_at" class="form-control publishTime" type="datetime-local" data-form-type="date" aria-describedby="sliderPublishAtHelp" value="{{ old('publish_at', ((isset($slider) and $slider->publish_at) ? $slider->publish_at->format('Y-m-d\TH:i') : '')) }}">
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'publish_at', 'helperId' => 'sliderPublishAtHelp' , 'helper' => __('Uniquement pour planifier la publication, le slider deviendra visible.')
|
||||
'inputName' => 'publish_at', 'helperId' => 'sliderPublishAtHelp' , 'helper' => (isset($slider) and $slider->publish_at) ? __('Laisser vide pour un affichage immédiat du slider.') : __('Vider pour un affichage immédiat du slider')
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -92,7 +158,7 @@
|
|||
<label for="sliderUnPublishedAt"><b>{{ __('Date de fin de publication (Europe/Paris)') }}</b></label>
|
||||
<input id="sliderUnPublishedAt" name="unpublish_at" class="form-control publishTime" type="datetime-local" data-form-type="date" aria-describedby="sliderUnPublishAtHelp" value="{{ old('unpublish_at', ((isset($slider) and $slider->unpublish_at) ? $slider->unpublish_at->format('Y-m-d\TH:i') : '')) }}">
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'unpublish_at', 'helperId' => 'sliderUnPublishAtHelp' , 'helper' => __('Uniquement pour planifier la fin de la publication, le slider deviendra invisible.')
|
||||
'inputName' => 'unpublish_at', 'helperId' => 'sliderUnPublishAtHelp' , 'helper' => (isset($slider) and $slider->unpublish_at) ? __('Laisser vide pour un affichage permanent du slider.') : __('Vider pour un affichage permanent du slider')
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -112,14 +178,14 @@
|
|||
<div class="row mb-3">
|
||||
<div class="col-12 col-md-6 form-group">
|
||||
<label for="sliderPicturePosition" class="col-form-label"><b>{{ __('Position de centrage') }}</b></label>
|
||||
<select id="sliderPicturePosition" class="form-select" name="position" aria-describedby="sliderPicturePositionHelp" required>
|
||||
<option value="" @if(is_null(old('position', $slider->position ?? null))) selected @endif disabled>{{ __('Séléctionnez un position de centrage') }}</option>
|
||||
@foreach(App\Enums\Sliders\SliderPosition::toArray() as $status)
|
||||
<option value="{{ $status->value }}" @if(old('published', optional($slider->position ?? null)->value) == $status->value) selected @endif>{{ $status->label }}</option>
|
||||
<select id="sliderPicturePosition" class="form-select" name="picture_position_mobile" aria-describedby="sliderPicturePositionHelp" required>
|
||||
<option value="" @if(is_null(old('picture_position_mobile', $slider->picture_position_mobile ?? null))) selected @endif disabled>{{ __('Séléctionnez une position de centrage') }}</option>
|
||||
@foreach(App\Enums\Sliders\SliderElementPosition::toArray() as $status)
|
||||
<option value="{{ $status->value }}" @if(old('picture_position_mobile', optional($slider->picture_position_mobile ?? null)->value) == $status->value) selected @endif>{{ $status->label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'published', 'helperId' => 'sliderPicturePositionHelp' , 'helper' => __('Le centrage de l\'image en version mobile (responsive)')
|
||||
'inputName' => 'picture_position_mobile', 'helperId' => 'sliderPicturePositionHelp' , 'helper' => __('Le centrage de l\'image en version mobile (responsive)')
|
||||
])
|
||||
</div>
|
||||
|
||||
|
|
@ -129,7 +195,7 @@
|
|||
$data = [
|
||||
'id' => 'sliderImageInput',
|
||||
'name' => 'picture',
|
||||
'helper' => __('L\'image qui sera affiché sur le slide'),
|
||||
'helper' => __('L\'image qui sera affiché sur le slide, doit être de 630px (hauteur) par 1400px (largeur)'),
|
||||
'width' => 1400,
|
||||
'height' => 630,
|
||||
'value' => $slider->picture ?? '',
|
||||
|
|
@ -150,7 +216,7 @@
|
|||
<fieldset class="border p-3 mb-3">
|
||||
<legend>{{ __('Lien') }}</legend>
|
||||
<div class="row mb-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="col-12 col-md-6 mb-3">
|
||||
<div class="form-group">
|
||||
<label for="sliderLink"><b>{{ __('Lien') }}</b></label>
|
||||
<input name="link" type="text" value="{{ old('link', $slider->link ?? '') }}" class="form-control"
|
||||
|
|
@ -160,7 +226,7 @@
|
|||
])
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="col-12 col-md-6 mb-3">
|
||||
<div class="form-group">
|
||||
<label for="sliderLinkLabel"><b>{{ __('Titre du lien') }}</b></label>
|
||||
<div class="word-counter" data-json='@json(['id' => 'sliderLinkLabel'])'></div>
|
||||
|
|
@ -171,28 +237,82 @@
|
|||
])
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="col-12 col-md-6 mb-3">
|
||||
<div class="form-group">
|
||||
<label for="sliderLinkColor"><b>{{ __('Couleur du lien') }}</b></label>
|
||||
<input name="link_color" type="color" class="form-control form-control-color" id="sliderLinkColor" value="{{ old('link_color', $slider->link_color ?? '') }}" title="{{ __('Choisi ta couleur') }}" aria-describedby="sliderLinkColorHelp">
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'link_color', 'helperId' => 'sliderLinkColorHelp' , 'helper' => __('Couleur du lien du slider')
|
||||
])
|
||||
@php
|
||||
$data = [
|
||||
'id' => 'sliderLinkColor',
|
||||
'name' => 'link_color',
|
||||
'value' => old('link_color', optional($slider ?? null)->link_color),
|
||||
'title' => __('Choisi ta couleur'),
|
||||
'label' => __('Couleur du lien'),
|
||||
'rgbaMode' => true,
|
||||
'nullable' => true,
|
||||
'ariaDescribedby' => 'sliderLinkColorHelp'
|
||||
];
|
||||
@endphp
|
||||
@include('back.modules.input-error', [ 'inputName' => 'link_color'])
|
||||
<div class="color-picker" data-json='@json($data)'></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="col-12 col-md-6 mb-3">
|
||||
<div class="form-group">
|
||||
<label for="sliderLinkBGColor"><b>{{ __('Couleur de fond du lien') }}</b></label>
|
||||
<input name="link_color_bg" type="color" class="form-control form-control-color" id="sliderLinkBGColor" value="{{ old('link_color_bg', $slider->link_color_bg ?? '') }}" title="{{ __('Choisi ta couleur') }}" aria-describedby="sliderLinkBGColorHelp">
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'link_color_bg', 'helperId' => 'sliderLinkBGColorHelp' , 'helper' => __('Couleur de fond du lien du slider')
|
||||
])
|
||||
@php
|
||||
$data = [
|
||||
'id' => 'sliderLinkBGColor',
|
||||
'name' => 'link_color_bg',
|
||||
'value' => old('link_color_bg', optional($slider ?? null)->link_color_bg),
|
||||
'title' => __('Choisi ta couleur'),
|
||||
'label' => __('Couleur de fond du lien'),
|
||||
'rgbaMode' => true,
|
||||
'nullable' => true,
|
||||
'ariaDescribedby' => 'sliderLinkBGColorHelp'
|
||||
];
|
||||
@endphp
|
||||
@include('back.modules.input-error', [ 'inputName' => 'link_color_bg'])
|
||||
<div class="color-picker" data-json='@json($data)'></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="col-12 col-md-6 mb-3">
|
||||
<div class="form-group">
|
||||
@php
|
||||
$data = [
|
||||
'id' => 'sliderLinkHoverColor',
|
||||
'name' => 'link_color_hover',
|
||||
'value' => old('link_color_hover', optional($slider ?? null)->link_color_hover),
|
||||
'title' => __('Choisi ta couleur'),
|
||||
'label' => __('Couleur du lien (au survol)'),
|
||||
'rgbaMode' => true,
|
||||
'nullable' => true,
|
||||
'ariaDescribedby' => 'sliderLinkHoverColorHelp'
|
||||
];
|
||||
@endphp
|
||||
@include('back.modules.input-error', [ 'inputName' => 'link_color_hover'])
|
||||
<div class="color-picker" data-json='@json($data)'></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 mb-3">
|
||||
<div class="form-group">
|
||||
@php
|
||||
$data = [
|
||||
'id' => 'sliderLinkBGHoverColor',
|
||||
'name' => 'link_color_bg_hover',
|
||||
'value' => old('link_color_bg_hover', optional($slider ?? null)->link_color_bg_hover),
|
||||
'title' => __('Choisi ta couleur'),
|
||||
'label' => __('Couleur de fond du lien (au survol)'),
|
||||
'rgbaMode' => true,
|
||||
'nullable' => true,
|
||||
'ariaDescribedby' => 'sliderLinkBGHoverColorHelp'
|
||||
];
|
||||
@endphp
|
||||
@include('back.modules.input-error', [ 'inputName' => 'link_color_bg_hover'])
|
||||
<div class="color-picker" data-json='@json($data)'></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 mb-3">
|
||||
<div class="form-check">
|
||||
<input name="link_new_tab" class="form-check-input" type="checkbox" id="sliderLinkNewtab" aria-describedby="sliderLinkColorHelp" value="1" @if(old('link_new_tab', $slider->link_new_tab ?? ''))checked @endif>
|
||||
<label class="form-check-label" for="sliderLinkNewtab"><b>{{ __('Nouvel onglet') }}</b></label><br>
|
||||
<input name="link_new_tab" class="form-check-input mt-2" type="checkbox" id="sliderLinkNewtab" aria-describedby="sliderLinkColorHelp" value="1" @if(old('link_new_tab', $slider->link_new_tab ?? ''))checked @endif>
|
||||
<label class="form-check-label user-select-none pt-1" for="sliderLinkNewtab"><b>{{ __('Nouvel onglet') }}</b></label><br>
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'link_new_tab', 'helperId' => 'sliderLinkColorHelp' , 'helper' => __('Le lien doit s\'ouvrir dans un nouvel onglet')
|
||||
])
|
||||
|
|
@ -206,8 +326,8 @@
|
|||
<div class="row mb-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="form-check">
|
||||
<input name="status" class="form-check-input" type="checkbox" id="sliderStatus" aria-describedby="sliderStatusHelp" value="1" @if(old('status', $slider->status ?? ''))checked @endif>
|
||||
<label class="form-check-label" for="sliderStatus"><b>{{ __('Publié') }}</b></label><br>
|
||||
<input name="status" class="form-check-input mt-2" type="checkbox" id="sliderStatus" aria-describedby="sliderStatusHelp" value="1" @if(old('status', $slider->status ?? ''))checked @endif>
|
||||
<label class="form-check-label user-select-none pt-1" for="sliderStatus"><b>{{ __('Publié') }}</b></label><br>
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'status', 'helperId' => 'sliderStatusHelp' , 'helper' => __('Le slider est visible ou non')
|
||||
])
|
||||
|
|
@ -250,11 +370,17 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
}
|
||||
if (isPlanned || getEuropeParisTime().valueOf() < (new Date(input.value)).valueOf()) {
|
||||
let date = new Date(input.value);
|
||||
small.textContent = "{{ __('Planifié pour le ') }}" + date.toLocaleString();
|
||||
} else {
|
||||
// small.textContent = "{{ __('Planifié pour le ') }}" + date.toLocaleString();
|
||||
small.textContent = input.id === 'sliderPublishedAt' ?
|
||||
"{{ __('Uniquement pour planifier la publication') }}" :
|
||||
"{{ __('Uniquement pour planifier la fin de la publication') }}";
|
||||
"{{ __('Vider pour un affichage immédiat du slider.') }}" :
|
||||
"{{ __('Vider pour un affichage permanent du slider.') }}";
|
||||
} else {
|
||||
// small.textContent = input.id === 'sliderPublishedAt' ?
|
||||
// "{{ __('Uniquement pour planifier la publication') }}" :
|
||||
// "{{ __('Uniquement pour planifier la fin de la publication') }}";
|
||||
small.textContent = input.id === 'sliderPublishedAt' ?
|
||||
"{{ __('Laisser vide pour un affichage immédiat du slider.') }}" :
|
||||
"{{ __('Laisser vide pour un affichage permanent du slider.') }}";
|
||||
}
|
||||
};
|
||||
setDateHelpers(input, parent);
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@
|
|||
document.getElementById('typeFilter').submit();
|
||||
});</script>
|
||||
@endpush
|
||||
<a href="{{ route('bo.sliders.create') }}" class="btn btn-primary" title="{{ __('Ajouter nouveau :model', [
|
||||
'model' => Str::singular(__('models.classes.news'))
|
||||
<a href="{{ route('bo.sliders.create') }}" class="btn btn-primary" title="{{ __('Ajouter un :model', [
|
||||
'model' => Str::singular(__('models.classes.sliders'))
|
||||
]) }}" data-bs-toggle="tooltip"><i class="fa-solid fa-plus"></i></a>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -49,74 +49,76 @@
|
|||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table class="table">
|
||||
<thead>
|
||||
@php
|
||||
$rst = !is_null(request()->rst);
|
||||
$routeName = request()->route()->getName();
|
||||
$noOrder = Session::get("$routeName.sort_col") !== 'order' and (Session::has("$routeName.sort_col") or Session::has("$routeName.sort_way"));
|
||||
$noOrder = Session::get("$routeName.sort_col") !== '' and (Session::has("$routeName.sort_col") or Session::has("$routeName.sort_way"));
|
||||
$cols = ['title' => __('Titre'), 'order' => __('Ordre'), 'status' => __('Publication'), 'updated_at' => __('Mis à jour')];
|
||||
@endphp
|
||||
@include('back.modules.tableColSorter', [ 'cols' => $cols, 'mobileHide' => ['status']])
|
||||
</thead>
|
||||
<tbody>
|
||||
@if($sliders->count())
|
||||
@foreach ($sliders as $slider)
|
||||
<tr>
|
||||
<td><b>{{ $slider->title }}</b></td>
|
||||
@if(!$noOrder or $rst)
|
||||
<td class="d-flex flex-column">
|
||||
@if(!($loop->first and $sliders->onFirstPage()))
|
||||
<form action="{{ route('bo.sliders.change-order', ['slider' => $slider, 'action' => 'up']) }}" method="POST">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-sm" title="{{ __('Modifier l\'ordre d\'apparition vers le haut') }}" data-bs-toggle="tooltip">
|
||||
<i name="up_arrow" class="icon-size far fa-arrow-alt-circle-up"></i>
|
||||
</button>
|
||||
</form>
|
||||
<div class="table-responsive">
|
||||
<table class="table fixAction">
|
||||
<thead>
|
||||
@php
|
||||
$rst = !is_null(request()->rst);
|
||||
$routeName = request()->route()->getName();
|
||||
$noOrder = Session::get("$routeName.sort_col") !== 'order' and (Session::has("$routeName.sort_col") or Session::has("$routeName.sort_way"));
|
||||
$noOrder = Session::get("$routeName.sort_col") !== '' and (Session::has("$routeName.sort_col") or Session::has("$routeName.sort_way"));
|
||||
$cols = ['title' => __('Titre'), 'order' => __('Ordre'), 'status' => __('Publication'), 'updated_at' => __('Mis à jour')];
|
||||
@endphp
|
||||
@include('back.modules.tableColSorter', [ 'cols' => $cols, 'mobileHide' => ['status']])
|
||||
</thead>
|
||||
<tbody>
|
||||
@if($sliders->count())
|
||||
@foreach ($sliders as $slider)
|
||||
<tr>
|
||||
<td><b>{{ $slider->title }}</b></td>
|
||||
@if(!$noOrder or $rst)
|
||||
<td class="d-flex flex-column">
|
||||
@if(!($loop->first and $sliders->onFirstPage()))
|
||||
<form action="{{ route('bo.sliders.change-order', ['slider' => $slider, 'action' => 'up']) }}" method="POST">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-sm" title="{{ __('Modifier l\'ordre d\'apparition vers le haut') }}" data-bs-toggle="tooltip">
|
||||
<i name="up_arrow" class="icon-size far fa-arrow-alt-circle-up"></i>
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
@if (!($loop->last and $sliders->currentPage() === $sliders->lastPage()))
|
||||
<form action="{{ route('bo.sliders.change-order', ['slider' => $slider, 'action' => 'down']) }}" method="POST">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-sm" title="{{ __('Modifier l\'ordre d\'apparition vers le bas') }}" data-bs-toggle="tooltip">
|
||||
<i name="down_arrow" class="icon-size far fa-arrow-alt-circle-down"></i>
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</td>
|
||||
@endif
|
||||
@if (!($loop->last and $sliders->currentPage() === $sliders->lastPage()))
|
||||
<form action="{{ route('bo.sliders.change-order', ['slider' => $slider, 'action' => 'down']) }}" method="POST">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-sm" title="{{ __('Modifier l\'ordre d\'apparition vers le bas') }}" data-bs-toggle="tooltip">
|
||||
<i name="down_arrow" class="icon-size far fa-arrow-alt-circle-down"></i>
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</td>
|
||||
<td class="d-none d-md-table-cell">
|
||||
<form action="{{ route('bo.sliders.change-published', $slider->id) }}" method="POST">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-sm" title="{{ __(':action :model', [
|
||||
'action' => $slider->status ? __('Déplublier') : __('Publier'),
|
||||
'model' => Str::singular(__('models.classes.sliders'))
|
||||
]) }}" data-bs-toggle="tooltip">
|
||||
<i class="fas @if($slider->status)fa-check check-green @else fa-times cross-red @endif"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge text-dark text-wrap">{{ $slider->updated_at->formatLocalized('%d/%m/%Y à %I:%M') }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-sm btn-primary" href="{{ route('bo.sliders.edit', $slider->id) }}" title="{{ __('Modifier :model', [
|
||||
'model' => Str::singular(__('models.classes.sliders'))
|
||||
]) }}" data-bs-toggle="tooltip"><i class="fa-solid fa-pencil"></i></a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@else
|
||||
<tr>
|
||||
<td colspan="5" class="text-center"><span class="h3">{{
|
||||
__('Aucun résultat pour les :models', ['models' => __('models.classes.sliders') ])
|
||||
}}</span></td>
|
||||
</tr>
|
||||
@endif
|
||||
<td class="d-none d-md-table-cell">
|
||||
<form action="{{ route('bo.sliders.change-published', $slider->id) }}" method="POST">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-sm" title="{{ __(':action :model', [
|
||||
'action' => $slider->status ? __('Déplublier') : __('Publier'),
|
||||
'model' => Str::singular(__('models.classes.sliders'))
|
||||
]) }}" data-bs-toggle="tooltip">
|
||||
<i class="fas @if($slider->status)fa-check check-green @else fa-times cross-red @endif"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge text-dark text-wrap">{{ $slider->updated_at_date }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-sm btn-primary" href="{{ route('bo.sliders.edit', $slider->id) }}" title="{{ __('Modifier :model', [
|
||||
'model' => Str::singular(__('models.classes.sliders'))
|
||||
]) }}" data-bs-toggle="tooltip"><i class="fa-solid fa-pencil"></i></a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@else
|
||||
<tr>
|
||||
<td colspan="5" class="text-center"><span class="h3">{{
|
||||
__('Aucun résultat pour les :models', ['models' => __('models.classes.sliders') ])
|
||||
}}</span></td>
|
||||
</tr>
|
||||
@endif
|
||||
</tbody>
|
||||
</table>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-center">
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="input-group justify-content-end">
|
||||
<a href="{{ route('bo.tags.create') }}" class="btn btn-primary" title="{{ __('Ajouter nouveau :model', [
|
||||
<a href="{{ route('bo.tags.create') }}" class="btn btn-primary" title="{{ __('Ajouter un :model', [
|
||||
'model' => Str::singular(__('models.classes.tags'))
|
||||
]) }}" data-bs-toggle="tooltip"><i class="fa-solid fa-plus"></i></a>
|
||||
</div>
|
||||
|
|
@ -34,41 +34,43 @@
|
|||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table class="table">
|
||||
<thead>
|
||||
@php
|
||||
$cols = ['name' => __('Nom')];
|
||||
@endphp
|
||||
@include('back.modules.tableColSorter', [ 'cols' => $cols, 'ignore' => []])
|
||||
</thead>
|
||||
<tbody>
|
||||
@if($tags->count())
|
||||
@foreach ($tags as $tag)
|
||||
<tr>
|
||||
<td>
|
||||
<span>{{ $tag->name }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<form action="{{ route('bo.tags.destroy', $tag->id) }}" method="POST" class="btn-group confirmDeleteJS" novalidate>
|
||||
<a class="btn btn-sm btn-primary" href="{{ route('bo.tags.edit', $tag->id) }}" title="{{ __('Modifier :model', [
|
||||
'model' => Str::singular(__('models.classes.tags'))
|
||||
]) }}" data-bs-toggle="tooltip"><i class="fa-solid fa-pencil"></i></a>
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-danger"><i class="fa-solid fa-trash"></i></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@else
|
||||
<tr>
|
||||
<td colspan="5" class="text-center"><span class="h3">{{
|
||||
__('Aucun résultat pour les :models', ['models' => __('models.classes.tags')])
|
||||
}}</span></td>
|
||||
</tr>
|
||||
@endif
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="table-responsive">
|
||||
<table class="table fixAction">
|
||||
<thead>
|
||||
@php
|
||||
$cols = ['name' => __('Nom')];
|
||||
@endphp
|
||||
@include('back.modules.tableColSorter', [ 'cols' => $cols, 'ignore' => []])
|
||||
</thead>
|
||||
<tbody>
|
||||
@if($tags->count())
|
||||
@foreach ($tags as $tag)
|
||||
<tr>
|
||||
<td>
|
||||
<span>{{ $tag->name }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<form action="{{ route('bo.tags.destroy', $tag->id) }}" method="POST" class="btn-group confirmDeleteJS" novalidate>
|
||||
<a class="btn btn-sm btn-primary" href="{{ route('bo.tags.edit', $tag->id) }}" title="{{ __('Modifier :model', [
|
||||
'model' => Str::singular(__('models.classes.tags'))
|
||||
]) }}" data-bs-toggle="tooltip"><i class="fa-solid fa-pencil"></i></a>
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-danger"><i class="fa-solid fa-trash"></i></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@else
|
||||
<tr>
|
||||
<td colspan="5" class="text-center"><span class="h3">{{
|
||||
__('Aucun résultat pour les :models', ['models' => __('models.classes.tags')])
|
||||
}}</span></td>
|
||||
</tr>
|
||||
@endif
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-center">
|
||||
|
|
|
|||
|
|
@ -26,9 +26,10 @@ Route::prefix('bo')->name('bo.')->group(function () {
|
|||
|
||||
Route::resource('sliders', SliderController::class)->except(['show']);
|
||||
Route::resource('tags', TagController::class)->except(['show']);
|
||||
Route::post('/tags/store', [TagController::class, 'jsonStore'])->name('tags.jsonStore');
|
||||
Route::resource('news', NewsController::class)
|
||||
->except(['show'])
|
||||
->withoutMiddleware('xss')
|
||||
->middleware('xss:content');
|
||||
->withoutMiddleware('htmlsanitize')
|
||||
->middleware('htmlsanitize:content');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue