add CRUD concept

This commit is contained in:
Alexis Gousseau 2023-03-24 15:39:11 +00:00 • committed by Vincent BRIDIER
parent ec5f2f8bd1
commit 9fdc2260f0
62 changed files with 1539 additions and 129 deletions

View file

@ -9,25 +9,39 @@ enum ColorsEnum : int {
case lightgreen = 4;
case brown = 5;
public function label(): string {
return static::getLabel($this);
}
public function color(): string {
return static::getColor($this);
}
public static function getLabel(self $value): string {
return match ($value) {
self::redwine => __('Redwine'),
self::orange => __('Orange'),
self::blue => __('Blue'),
self::lightgreen => __('Lightgreen'),
self::brown => __('Brown'),
};
}
public static function getColor(self $value): string {
return match ($value) {
self::redwine => rgb(165 25 60),
self::orange => rgb(240 165 0),
self::blue => rgb(95 155 210),
self::lightgreen => rgb(180 190 0),
self::brown => rgb(95 40 45),
self::redwine => "rgb(165 25 60)",
self::orange => "rgb(240 165 0)",
self::blue => "rgb(95 155 210)",
self::lightgreen => "rgb(180 190 0)",
self::brown => "rgb(95 40 45)",
};
}
public static function getColors(): array {
public static function getData(): array {
$arrayEnum = [];
foreach (self::cases() as $key => $value) {
$arrayEnum = array_merge($arrayEnum, [[
"value" => $value->value,
"label" => $value->label(),
"color" => $value->color()
]]);
}

18
app/Enums/InputType.php Normal file
View file

@ -0,0 +1,18 @@
<?php
namespace App\Enums;
enum InputType : int {
case boolean = 1;
case number = 2;
case string = 3;
case text = 4;
case url = 5;
case videoVimeo = 6;
case image = 7;
case imageArray = 8;
case labelArray = 9;
case dropdownArray = 10;
case miniblocArray = 11;
case selectEnum = 12;
}

View file

@ -0,0 +1,164 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Concepts\StoreConceptRequest;
use App\Http\Requests\Bo\Concepts\UpdateConceptRequest;
use App\Models\Concept;
use App\Traits\Controllers\UpdateModelStatus;
use App\Traits\Models\ChangesModelOrder;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class ConceptController extends Controller
{
use ChangesModelOrder;
use UpdateModelStatus;
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
/** @var \Illuminate\Database\Eloquent\Builder $concepts */
$concepts = Concept::query();
if ($request->search) {
$this->searchQuery(
$concepts,
$request->search,
null,
'seo_title',
'name',
'title',
'subtitle',
);
}
$this->sortQuery($concepts);
$searchFields = trans('Name');
$conceptModels = $concepts->paginate(10);
return view('bo.pages.concepts.index', compact('conceptModels', 'searchFields'));
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
$conceptData = Concept::getEmptyData();
return view('bo.pages.concepts.create', compact('conceptData'));
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Bo\Concepts\StoreConceptRequest $request
* @return \Illuminate\Http\Response
*/
public function store(StoreConceptRequest $request)
{
return DB::transaction(function () use ($request) {
$concept = new Concept();
$concept->data = $concept->getEmptyData();
$concept->fill(array_replace_recursive(
\json_decode(\json_encode($concept->toArray()), true),
$request->validated()
));
if ($concept->saveOrFail()) {
return redirect()
->route('bo.concepts.edit', $concept)
->with('success', trans(
':model has been created',
['model' => Str::singular(trans('models.classes.concepts'))]
));
}
return redirect()
->back()
->with('error', trans(
':model cannot be created',
['model' => Str::singular(trans('models.classes.concepts'))]
));
});
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\Concept $concept
* @return \Illuminate\Http\Response
*/
public function edit(Concept $concept)
{
return view('bo.pages.concepts.edit', [
'conceptModel' => $concept
]);
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\Bo\Concepts\UpdateConceptRequest $request
* @param \App\Models\Concept $concept
* @return \Illuminate\Http\Response
*/
public function update(UpdateConceptRequest $request, Concept $concept)
{
return DB::transaction(function () use ($request, $concept) {
$concept->fill(array_replace_recursive(
\json_decode(\json_encode($concept->toArray()), true),
$request->validated()
));
if ($concept->saveOrFail()) {
return redirect()
->route('bo.concepts.edit', $concept)
->with('success', trans(
'Modification(s) from :model saved',
['model' => Str::singular(trans('models.classes.concepts'))]
));
}
return redirect()
->back()
->with('error', trans(
'Modification(s) from :model cannot be saved',
['model' => Str::singular(trans('models.classes.concepts'))]
));
});
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Concept $concept
* @return \Illuminate\Http\Response
*/
public function destroy(Concept $concept)
{
if ($concept->deleteOrFail()) {
return redirect()
->route('bo.concepts.index')
->with('success', trans(
':model has been deleted',
['model' => Str::singular(trans('models.classes.concepts'))]
));
}
return redirect()
->back()
->with('error', trans(
':model cannot be deleted',
['model' => Str::singular(trans('models.classes.concepts'))]
));
}
}

View file

@ -44,11 +44,14 @@ class HomeController extends Controller
return redirect()
->route('bo.homes.edit', $home)
->with('success', trans(
':model a été mis à jour',
'Modification(s) from :model saved',
['model' => Str::singular(trans('models.classes.homes'))]
));
} //end if
return back();
return back()->with('success', trans(
'Modification(s) from :model cannot be saved',
['model' => Str::singular(trans('models.classes.homes'))]
));
});
}
}

View file

@ -0,0 +1,87 @@
<?php
namespace App\Http\Requests\Bo\Concepts;
use App\Http\Requests\Bo\UpdateMiniCrudRequest;
use App\Traits\Requests\HasPicture;
use Illuminate\Support\Str;
class StoreConceptRequest extends UpdateMiniCrudRequest
{
use HasPicture;
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize()
{
return auth('backend')->check();
}
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation()
{
$this->merge([
'slug' => Str::slug(strip_tags($this->name))
]);
$this->mergePicture('picture');
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
$rules = [
'seo_title' => 'required|min:3|max:70',
'seo_description' => 'required|min:50|max:160',
'name' => 'required|min:3|max:70',
'title' => 'required|min:3|max:70',
'subtitle' => 'required|min:3|max:70',
'description' => 'required|string|min:15',
'data' => 'present|array',
'data.*' => 'required|array',
'picture_alt' => 'required|min:3|max:255',
'picture_title' => 'required|min:3|max:255',
];
return \array_merge(
$rules,
$this->pictureRules('picture', true, 1512, 354, 1512, 354),
);
}
/**
* Get custom attributes for validator errors.
*
* @return array
*/
public function attributes()
{
return [
'seo_title' => trans('titre') . ' seo',
'seo_description' => trans('description') . ' seo',
'name' => trans('name of the concept'),
'slug' => trans('slug of the concept'),
'title' => trans('title of the concept'),
'subtitle' => trans('subtitle of the concept'),
'description' => trans('description of the concept'),
'data' => trans('values of the data'),
'data.*.value' => trans('value of the data'),
'picture' => trans('illustration'),
'picture_alt' => trans('alt attribute of the image'),
'picture_title' => trans('title attribute of the image'),
];
}
}

View file

@ -0,0 +1,9 @@
<?php
namespace App\Http\Requests\Bo\Concepts;
use App\Http\Requests\Bo\Concepts\StoreConceptRequest;
class UpdateConceptRequest extends StoreConceptRequest
{
}

View file

@ -41,13 +41,13 @@ class UpdateHomeRequest extends FormRequest
'seo_description' => 'required|min:50|max:160',
'part1_title' => 'required|min:3|max:70',
'part1_title_span' => 'required|min:3|max:70',
'part1_subtitle' => 'required|min:3|max:70',
'part1_text_1' => 'required|string|min:15',
'part1_text_2' => 'required|string|min:15',
'part1_text_3' => 'required|string|min:15',
'part2_title' => 'required|min:3|max:70',
'part2_title_span' => 'required|min:3|max:70',
'part2_subtitle' => 'required|min:3|max:70',
'part2_text' => 'required|string|min:15',
'part2_list' => 'present|array',
'part2_list.*' => 'required|array',
@ -76,13 +76,13 @@ class UpdateHomeRequest extends FormRequest
'seo_description' => trans('description') . ' seo',
'part1_title' => trans('title part.1'),
'part1_title_span' => trans('subtitle part.1'),
'part1_subtitle' => trans('subtitle part.1'),
'part1_text_1' => trans('text 1 part.1'),
'part1_text_2' => trans('text 2 part.1'),
'part1_text_3' => trans('text 3 part.1'),
'part2_title' => trans('title part.2'),
'part2_title_span' => trans('subtitle part.2'),
'part2_subtitle' => trans('subtitle part.2'),
'part2_text' => trans('text part.2'),
'part2_list' => trans('values of the list part.2'),
'part2_list.*.value' => trans('value of the list part.2'),

View file

@ -0,0 +1,156 @@
<?php
namespace App\Http\Requests\Bo;
use App\Enums\InputType;
use App\Lib\Helpers\ToolboxHelper;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\ValidationException;
abstract class UpdateMiniCrudRequest extends FormRequest
{
/**
* The model parameter name
*
* @var string
*/
protected $modelName = null;
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize()
{
return auth('backend')->check();
}
/**
* Get the validation rules that apply to the request.
*
* @return array
* @phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh
* @phpcs:disable Generic.Metrics.NestingLevel.TooHigh
*/
public function rules()
{
// phpcs:enable
$rules = [];
foreach ($this->{$this->modelName}->data as $iptName => $input) {
$input = \json_decode(\json_encode($input), true);
switch ($input['inputType']['value']) {
case InputType::number()->value:
$rules = array_merge($rules, [
"data.$iptName.value" => $this->compileValidate('required|numeric', $input)
]);
break;
case InputType::string()->value:
$rules = array_merge($rules, [
"data.$iptName.value" => $this->compileValidate('required|string|min:3', $input)
]);
break;
case InputType::text()->value:
$mergeRules = $this->compileValidate('required|string|min:15', $input);
$mergeRules = collect(\explode('|', $mergeRules))
->map(function ($rule) {
if (\strpos($rule, 'max:') !== false) {
$max = \intval(str_replace('max:', '', $rule));
return function ($attribute, $value, $fail) use ($max) {
if (\strlen(\strip_tags($value)) > $max) {
$fail("$attribute ne doit pas excéder $max caractères");
}
};
}
return $rule;
});
$rules = array_merge($rules, [
"data.$iptName.text" => $mergeRules
]);
break;
case InputType::image()->value:
$ipt = $this->data[$iptName];
$max = isset($ipt['dimensionsMax']);
$dim = collect(explode(
',',
$ipt['dimensionsMax'] ?? $input['dimensions']
))->mapWithKeys(function ($v) {
$v = explode(' ', $v);
return [$v[0] => $v[1]];
});
$rules = array_merge($rules, [
"data.$iptName.value" => $this->compileValidate(
implode('|', [
'required',
'image',
'mimes:png,jpg,jpeg',
sprintf(
'dimensions:%s=%d,%s=%d',
$max ? 'max_height' : 'height',
$dim['hauteur'],
$max ? 'max_width' : 'width',
$dim['largeur']
)
]),
$input
),
"data.$iptName.alt" => $this->compileValidate('required|string|min:5', $input),
"data.$iptName.title" => $this->compileValidate('required|string|min:5', $input)
]);
break;
case InputType::url()->value:
$rules = array_merge($rules, [
"data.$iptName.value" => [
'required', 'url',
// @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed
function ($attribute, $value, $fail) {
try {
ToolboxHelper::assertNoInternalUrls($attribute, $value);
ToolboxHelper::assertNoInternalBoUrls($attribute, $value);
} catch (ValidationException $e) {
$fail(collect($e->errors())->first()[0]);
}
}
]
]);
break;
case InputType::boolean()->value:
$all = $this->all();
$nested = ToolboxHelper::dotStringToNestedArray(
"data.$iptName.value",
$this->input("data.$iptName.value") ? true : false
);
$all = array_merge_recursive_distinct($all, $nested);
$this->merge($all);
$rules = array_merge($rules, [
"data.$iptName.value" => 'boolean'
]);
break;
} //end switch
} //end foreach
return $rules;
}
/**
* Undocumented function
*
* @param string $inputTypeValidate
* @param array $input
* @return string
*/
public function compileValidate(string $inputTypeValidate, array $input): string
{
if (!isset($input['validate']) or !\is_string($input['validate'])) {
return $inputTypeValidate;
}
/** @var array */
$inputValidate = explode('|', $input['validate']);
$out = explode('|', $inputTypeValidate);
foreach ($inputValidate as $validate) {
if (!\in_array($validate, $out)) {
$out[] = $validate;
}
}
return \implode('|', $out);
}
}

View file

@ -234,4 +234,24 @@ class ToolboxHelper
throw new \RuntimeException('cannot handle $input of type ' . \gettype($input));
}
}
/**
* Generates an array from a dot separated string
*
* @param string $path
* @param mixed $value
* @param string $separator
* @return array
*/
public static function dotStringToNestedArray(string $path, $value, string $separator = '.'): array
{
$arr = $value;
$keys = array_reverse(explode($separator, $path));
foreach ($keys as $key) {
$arr = [$key => $arr];
}
return $arr;
}
}

View file

@ -2,10 +2,15 @@
namespace App\Models;
use App\Enums\Concepts\ColorsEnum;
use App\Enums\InputType;
use App\Lib\Helpers\FileStorageHelper;
use App\Lib\Helpers\ToolboxHelper;
use App\Traits\Models\MiniCrudModel;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules\Enum;
/**
* @property integer $id
@ -14,16 +19,19 @@ use Illuminate\Database\Eloquent\Model;
* @property string $name
* @property string $slug
* @property string $title
* @property string $title_span
* @property string $subtitle
* @property string $description
* @property string $content
* @property string $picture
* @property string $picture_alt
* @property string $picture_title
* @property string $created_at
* @property string $updated_at
*/
class Concept extends Model
{
use HasFactory;
use MiniCrudModel;
/**
* @var array
@ -34,14 +42,26 @@ class Concept extends Model
'name',
'slug',
'title',
'title_span',
'subtitle',
'description',
'content',
'data',
'picture',
'picture_alt',
'picture_title',
'order',
'created_at',
'updated_at'
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'data' => 'array',
];
// * EVENTS
/**
@ -52,9 +72,13 @@ class Concept extends Model
protected static function booted()
{
static::creating(function (self $model) {
self::setSlug($model);
self::assertFieldIsUnique('name', $model->name);
self::manageFields($model);
self::setOrder($model);
});
static::updating(function (self $model) {
self::setSlug($model);
self::manageFields($model);
});
static::updated(function (self $model) {
@ -68,17 +92,41 @@ class Concept extends Model
// * METHODS
/**
* Undocumented function
* Set the slug.
*
* @param \Illuminate\Database\Eloquent\Model $model
*
* @return void
*/
private static function setSlug(Model $model)
{
$model->slug = Str::slug($model->name);
}
/**
* Common methods function.
*
* @param Model $model
* @return void
*/
private static function manageFields(Model $model)
{
self::assertFieldIsUnique('name', $model->name);
ToolboxHelper::assertNoInternalBoUrls('description', $model->description);
$model->description = clean($model->description);
$model->picture = FileStorageHelper::storeFile($model, $model->picture, true);
}
/**
* Set order after the last element of the list.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return void
*/
private static function setOrder(Model $model): void
{
$model->order = \intval(self::query()->max('order')) + 1;
}
/**
* Asserts using validation that the field is unique.
*
@ -92,4 +140,130 @@ class Concept extends Model
$table = (new self())->getTable();
ToolboxHelper::assertFieldIsUnique($table, $nameField, $valueField, $id);
}
/**
* Return the structure necessary of data.
*
* @param Model $model
* @return void
*/
public static function getEmptyData(
string $title = null,
int $color = null,
string $text = null,
string $img = null,
string $imgAlt = null,
string $imgTitle = null
) {
return [
// * BLOC 1
"bloc_1_title" => [
"label" => "Bloc 1 - Title",
"inputType" => InputType::string,
"value" => $title ?? "",
"validate" => "required|min:4|max:100"
],
"bloc_1_color" => [
"label" => "Bloc 1 - Color",
"inputType" => InputType::selectEnum,
"value" => $color ?? "",
"validate" => ["required", "integer", new Enum(ColorsEnum::class)]
],
"bloc_1_text" => [
"label" => "Bloc 1 - Text",
"inputType" => InputType::text,
"value" => $text ?? "",
"validate" => "string|min:10"
],
"bloc_1_image" => [
"label" => "Bloc 1 - Image",
"dimensions" => ["width" => 1140, "height" => 795],
"inputType" => InputType::image,
"alt" => $imgAlt ?? "",
"title" => $imgTitle ?? "",
"value" => $img ?? "",
],
// * BLOC 2
"bloc_2_title" => [
"label" => "Bloc 2 - Title",
"inputType" => InputType::string,
"value" => $title ?? "",
"validate" => "required|min:4|max:100"
],
"bloc_2_color" => [
"label" => "Bloc 2 - Color",
"inputType" => InputType::selectEnum,
"value" => $color ?? "",
"validate" => ["required", "integer", new Enum(ColorsEnum::class)]
],
"bloc_2_text" => [
"label" => "Bloc 2 - Text",
"inputType" => InputType::text,
"value" => $text ?? "",
"validate" => "string|min:10"
],
"bloc_2_image" => [
"label" => "Bloc 2 - Image",
"dimensions" => ["width" => 1140, "height" => 795],
"inputType" => InputType::image,
"alt" => $imgAlt ?? "",
"title" => $imgTitle ?? "",
"value" => $img ?? "",
],
// * BLOC 3
"bloc_3_title" => [
"label" => "Bloc 3 - Title",
"inputType" => InputType::string,
"value" => $title ?? "",
"validate" => "required|min:4|max:100"
],
"bloc_3_color" => [
"label" => "Bloc 3 - Color",
"inputType" => InputType::selectEnum,
"value" => $color ?? "",
"validate" => ["required", "integer", new Enum(ColorsEnum::class)]
],
"bloc_3_text" => [
"label" => "Bloc 3 - Text",
"inputType" => InputType::text,
"value" => $text ?? "",
"validate" => "string|min:10"
],
"bloc_3_image" => [
"label" => "Bloc 3 - Image",
"dimensions" => ["width" => 1140, "height" => 795],
"inputType" => InputType::image,
"alt" => $imgAlt ?? "",
"title" => $imgTitle ?? "",
"value" => $img ?? "",
],
// * BLOC 4
"bloc_4_title" => [
"label" => "Bloc 4 - Title",
"inputType" => InputType::string,
"value" => $title ?? "",
"validate" => "required|min:4|max:100"
],
"bloc_4_color" => [
"label" => "Bloc 4 - Color",
"inputType" => InputType::selectEnum,
"value" => $color ?? "",
"validate" => ["required", "integer", new Enum(ColorsEnum::class)]
],
"bloc_4_text" => [
"label" => "Bloc 4 - Text",
"inputType" => InputType::text,
"value" => $text ?? "",
"validate" => "string|min:10"
],
"bloc_4_image" => [
"label" => "Bloc 4 - Image",
"dimensions" => ["width" => 1140, "height" => 795],
"inputType" => InputType::image,
"alt" => $imgAlt ?? "",
"title" => $imgTitle ?? "",
"value" => $img ?? "",
],
];
}
}

View file

@ -14,12 +14,12 @@ use Illuminate\Database\Eloquent\Model;
* @property string $seo_title
* @property string $seo_description
* @property string $part1_title
* @property string $part1_title_span
* @property string $part1_subtitle
* @property string $part1_text_1
* @property string $part1_text_2
* @property string $part1_text_3
* @property string $part2_title
* @property string $part2_title_span
* @property string $part2_subtitle
* @property string $part2_text
* @property string $part2_list
* @property string $part3_picture
@ -41,12 +41,12 @@ class Home extends Model
'seo_title',
'seo_description',
'part1_title',
'part1_title_span',
'part1_subtitle',
'part1_text_1',
'part1_text_2',
'part1_text_3',
'part2_title',
'part2_title_span',
'part2_subtitle',
'part2_text',
'part2_list',
'part3_picture',

View file

@ -2,6 +2,7 @@
namespace App\Providers;
use App\Models\Concept;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
@ -39,6 +40,8 @@ class RouteServiceProvider extends ServiceProvider
->namespace($this->namespace)
->group(base_path('routes/web-fo.php'));
});
Route::model('concept', Concept::class);
}
/**

View file

@ -6,6 +6,7 @@ use App\Models\Home;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
@ -22,9 +23,9 @@ class ViewServiceProvider extends ServiceProvider
// * FORCE BOOTSTRAP PAGINATOR (or custom if in front)
// Early boot, request wont be filled as expected.
if (collect(explode('/', \request()->getPathInfo()))->get(1) === 'bo') {
Paginator::useBootstrap();
Paginator::defaultview('bo.modules.pagination');
} else {
Paginator::defaultview('fo.partials.pagination');
Paginator::defaultview('fo.modules.pagination');
}
}
@ -32,10 +33,14 @@ class ViewServiceProvider extends ServiceProvider
return json_decode(File::get(\app_path('../composer.json')));
});
// Share composer.json infos.
View::share('cpsr_name', $appInfos->name);
View::share('cpsr_version', $appInfos->version);
View::share('cpsr_license', $appInfos->license);
View::share('cpsr_time', now()->parse($appInfos->time)->isoFormat('Y'));
View::share('globalHome', Home::firstOrFail());
if (isset($appInfos->name) and isset($appInfos->version) and isset($appInfos->license) and isset($appInfos->time)) {
View::share('cpsr_name', $appInfos->name);
View::share('cpsr_version', $appInfos->version);
View::share('cpsr_license', $appInfos->license);
View::share('cpsr_time', now()->parse($appInfos->time)->isoFormat('Y'));
}
if (Schema::hasTable('homes')) {
View::share('globalHome', Home::first());
}
}
}

View file

@ -0,0 +1,94 @@
<?php
namespace App\Traits\Models;
use App\Enums\InputType;
use App\Lib\Helpers\FileStorageHelper;
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.
*/
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'] ?? json_encode($this->getEmptyData())) ?? '[]';
}
/**
* 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'] ?? $input['inputType']) {
case InputType::image->value:
$data[$o]['value'] = FileStorageHelper::storeFile($this, $input['value'], true);
break;
case InputType::boolean->value:
case InputType::text->value:
case InputType::number->value:
case InputType::string->value:
case InputType::videoVimeo->value:
case InputType::url->value:
}
} //end foreach
return json_encode($data);
}
}

View file

@ -1,5 +1,5 @@
{
"name": "Wenn dann mit respekt",
"name": "ldcweb/wenn-dann-mit-respekt",
"version": "1.0.0",
"type": "project",
"time": "2023-03-16",
@ -52,6 +52,14 @@
}
},
"scripts": {
"refresh": [
"@php artisan optimize:clear",
"@php artisan clear-compiled",
"@php artisan view:clear",
"@php artisan route:clear",
"@php artisan config:clear",
"@php artisan cache:clear"
],
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"

2
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "eee884d4ea0363ba9b1f0f62410bb38f",
"content-hash": "0bc4ba3bc4ba2da26748340ed29c18bc",
"packages": [
{
"name": "brick/math",

View file

@ -23,7 +23,7 @@ return [
|
*/
'view' => 'breadcrumbs/breadrcumb-layout',
'view' => 'modules/breadcrumbs/breadcrumb-layout',
/*
|--------------------------------------------------------------------------

View file

@ -26,15 +26,17 @@ final class ConceptFactory extends Factory
public function definition(): array
{
$name = fake()->unique()->firstname;
return [
'seo_title' => fake()->word,
'seo_description' => fake()->text,
'name' => $name,
'slug' => Str::slug($name),
'title' => fake()->word,
'title_span' => fake()->word,
'subtitle' => fake()->word,
'description' => fake()->text,
'content' => fake()->text,
'picture_alt' => fake()->word,
'picture_title' => fake()->word,
];
}
}

View file

@ -36,12 +36,12 @@ final class HomeFactory extends Factory
'seo_title' => fake()->word,
'seo_description' => fake()->text,
'part1_title' => fake()->word,
'part1_title_span' => fake()->word,
'part1_subtitle' => fake()->word,
'part1_text_1' => fake()->text,
'part1_text_2' => fake()->text,
'part1_text_3' => fake()->text,
'part2_title' => fake()->word,
'part2_title_span' => fake()->word,
'part2_subtitle' => fake()->word,
'part2_text' => fake()->text,
'part2_list' => $part2_list,
'part2_product1_id' => fake()->randomElement(Product::pluck('id')),

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 508 KiB

View file

@ -20,10 +20,13 @@ return new class extends Migration
$table->string('name')->comment('Nom du concept');
$table->string('slug')->unique()->comment('Slug du nom de la page');
$table->string('title')->comment('Titre de la page');
$table->string('title_span')->comment('Seconde partie du titre de la page');
$table->string('description')->comment('Description du concept');
$table->mediumText('content')->comment('Tableau d\'objets');
$table->string('subtitle')->comment('Seconde partie du titre de la page');
$table->text('description')->comment('Description du concept');
$table->json('data')->comment('Tableau d\'objets');
$table->string('picture')->comment('Path de l\'image, 1512px x 354px');
$table->string('picture_alt')->comment('Attribut alt de l\'image');
$table->string('picture_title')->comment('Titre de l\'image');
$table->integer('order')->comment('Pour ordonner les concepts');
$table->timestamps();
});
}

View file

@ -18,12 +18,12 @@ return new class extends Migration
$table->string('seo_title')->comment('Utilisé pour la balise title de la page');
$table->string('seo_description')->comment('Utilisé pour la balise méta description de la page');
$table->string('part1_title')->comment('Titre de la partie 1');
$table->string('part1_title_span')->comment('Seconde partie du titre de la partie 1');
$table->string('part1_subtitle')->comment('Seconde partie du titre de la partie 1');
$table->string('part1_text_1')->comment('Premier paragraphe de la partie 1');
$table->string('part1_text_2')->comment('Second paragraphe de la partie 1');
$table->string('part1_text_3')->comment('Troisième paragraphe de la partie 1');
$table->string('part2_title')->comment('Titre de la partie 2');
$table->string('part2_title_span')->comment('Seconde partie du titre de la partie 2');
$table->string('part2_subtitle')->comment('Seconde partie du titre de la partie 2');
$table->string('part2_text')->comment('Paragraphe de la partie 2');
$table->json('part2_list')->comment('Tableau JSON de string');
$table->unsignedBigInteger('part2_product1_id')->comment('Produit lié 1, FK');

View file

@ -2,6 +2,7 @@
namespace Database\Seeders;
use App\Enums\Concepts\ColorsEnum;
use App\Lib\Helpers\FileStorageHelper;
use App\Models\Concept;
use Illuminate\Database\Seeder;
@ -18,8 +19,20 @@ class ConceptSeeder extends Seeder
$jj = 1;
Concept::factory(20)->make()->each(function (Concept $concept) use (&$jj) {
$concept->picture = FileStorageHelper::storeFile($concept, new \SplFileInfo(\resource_path(
'../database/factories/assets/concepts/default-picture-1512x3540.png'
'../database/factories/assets/concepts/default-picture-1512x354.png'
)));
$dataImg = FileStorageHelper::storeFile($concept, new \SplFileInfo(\resource_path(
'../database/factories/assets/concepts/default-picture-1140x795.png'
)));
$concept->data = $concept->getEmptyData(
fake()->words(2, true),
\collect(ColorsEnum::cases())->random()->value,
fake()->text(),
$dataImg,
fake()->words(4, true),
fake()->words(4, true)
);
$concept->order = $jj;
$concept->saveOrFail();
$jj = $jj + 1;
});

View file

@ -4,5 +4,6 @@ return [
'classes' => [
'homes' => 'Homepage',
'products' => 'Products',
'concepts' => 'Concepts',
]
];

View file

@ -1,4 +1,6 @@
.need-word-counter {
position: relative;
z-index: 1;
border-radius: 0 0 0.375rem 0.375rem !important;
}

View file

@ -37,6 +37,8 @@
@import "~/bootstrap/scss/close";
@import "~/bootstrap/scss/card";
@import "~/bootstrap/scss/offcanvas";
@import "~/bootstrap/scss/badge";
@import "~/bootstrap/scss/breadcrumb";
// 6. Optionally include utilities API last to generate classes based on the Sass map in `_utilities.scss`
@import "~/bootstrap/scss/utilities/api";

View file

@ -482,7 +482,7 @@ export default defineComponent({
return {
intId: "",
intValue: "",
intValueFileName: this.__("Sélectionnez une image"),
intValueFileName: this.__("Choose a file"),
intHelper: "",
intName: "",
intRequired: true,
@ -703,7 +703,7 @@ export default defineComponent({
removeFile() {
const actualImage = this.$refs.actualImage as HTMLInputElement;
actualImage.value = "";
this.intValueFileName = this.__("Sélectionnez une image");
this.intValueFileName = this.__("Choose a file");
this.intValue = "";
},
/**

View file

@ -3,13 +3,11 @@
class="word-counter-varchar position-relative d-flex flex-row justify-content-around align-items-center w-100"
>
<span class="words">
{{ __("Words") }}
:&nbsp;
{{ __("Words") }}:
{{ intWords }}
</span>
<span class="chars">
{{ __("Characters") }}
:&nbsp;
{{ __("Characters") }}:
{{ intChars }}
</span>
</div>

View file

@ -13,8 +13,8 @@ window.addEventListener("DOMContentLoaded", () => {
);
}
confirm.methods.confirm(
"Confirmez",
"Voulez-vous vraiment supprimer cet élément ?",
"Confirm",
"Do you really want to remove this element ?",
el as HTMLFormElement,
function () {
(el as HTMLFormElement).submit();
@ -35,8 +35,8 @@ window.addEventListener("DOMContentLoaded", () => {
);
}
confirm.methods.confirm(
"Confirmez",
"Voulez-vous vraiment marquer cet élément comme traité ?",
"Confirm",
"Do you really want to mark this item as treated ?",
el as HTMLFormElement,
function () {
(el as HTMLFormElement).submit();
@ -57,8 +57,8 @@ window.addEventListener("DOMContentLoaded", () => {
);
}
confirm.methods.confirm(
"Confirmez",
"Voulez-vous enregistrer cet élément tel-quel ?",
"Confirm",
"Do you want to save this element as it is ?",
el as HTMLFormElement,
function () {
(el as HTMLFormElement).submit();
@ -85,8 +85,8 @@ window.addEventListener("DOMContentLoaded", () => {
const promise = new Promise(
(resolve: (value: boolean) => void) => {
confirm.methods.confirm(
"Confirmez",
"Êtes vous sure ?",
"Confirm",
"Are you sure ?",
el,
function (response: SweetAlertResult<any>) {
resolve(response.isConfirmed);

View file

@ -27,10 +27,10 @@ export default {
showCancelButton: true,
confirmButtonText:
options.confirmButtonText ??
trans.default.methods.__("Oui"),
trans.default.methods.__("Yes"),
cancelButtonText:
options.cancelButtonText ??
trans.default.methods.__("Annuler"),
trans.default.methods.__("Cancel"),
reverseButtons: true,
// * Bootstrap Styling
customClass: {
@ -49,7 +49,7 @@ export default {
) {
Swal.fire({
title: trans.default.methods.__(
"L'opération n'as pas été effectuée"
"The operation has not been performed"
),
confirmButtonText: trans.default.methods.__("Ok"),
// * Bootstrap Styling

View file

@ -1,6 +1,7 @@
@extends('bo.layout')
@section('title', __('Authentification'))
@section('description', __('Formulaire d\'authentification au back-office.'))
@section('breadcrumb', 'no')
@section('content')
<div class="container">

View file

@ -12,15 +12,8 @@
@else
@php $authLayout = 'col px-md-4'; @endphp
@endauth
@auth('backend')
<div class="row">
<div class="{{ $authLayout }}">
@include('modules.breadcrumb.breadcrumb-layout')
</div>
</div>
@endauth
<div class="row justify-content-center">
<div class="{{ $authLayout }} mt-3">
<div class="{{ $authLayout }}">
<div class="container mx-auto m-0 p-0">
<div class="row justify-content-center">
<div class="col col-xl-7">
@ -32,6 +25,9 @@
</div>
<div class="row justify-content-center">
<div class="{{ $authLayout }}">
@auth('backend')
@include('modules.breadcrumbs.breadcrumb-body')
@endauth
@yield('content')
</div>
</div>

View file

@ -31,7 +31,7 @@
<meta name="msapplication-TileColor" content="#da532c">
<meta name="theme-color" content="#ffffff">
@include('modules.breadcrumb.breadcrumb-head')
@include('modules.breadcrumbs.breadcrumb-head')
<!-- STYLES -->
@vite(['resources/sass/bo/bo.scss'])

View file

@ -16,7 +16,7 @@
<h5 class="offcanvas-title">{{ __('Users menu') }}</h5>
<button type="button" class="btn-close btn-close-dark" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body pt-2">
<div class="offcanvas-body">
<ul class="navbar-nav w-100">
<!-- Model Links -->
@include('bo.layouts.nav-menu')

View file

@ -11,4 +11,10 @@
{{ __('models.classes.homes') }}
</a>
</li>
<li class="nav-item">
<a class="nav-link @if (Route::is('bo.concepts.*'))active @endif" href="{{ route('bo.concepts.index') }}"
role="button" data-bs-toggle="tooltip" title="{{ __('List all :model', ['model' => __('models.classes.concepts')]) }}">
{{ __('models.classes.concepts') }}
</a>
</li>
@endauth

View file

@ -0,0 +1,140 @@
{{-- MINI CRUD EDIT --}}
@php
$data = (isset($model->data)) ? $model->getEmptyData() : $conceptData;
@endphp
@foreach ($data as $k => $input)
@php
$input = json_decode(json_encode($input), true);
@endphp
@switch(intval($input['inputType']))
@case(intval(App\Enums\InputType::url->value))
@case(intval(App\Enums\InputType::string->value))
<div class="col-12 col-md-6 mb-3">
<fieldset class="border h-100 bg-white p-3">
<legend>{{ $input['label'] }} *</legend>
<div class="row">
<div class="col-12 mb-3">
@include('bo.partials.inputs.varchar', [
'id' => Str::slug($input["label"]),
'label' => $input['label'] . ' of the ' . __('models.classes.concepts'),
'hasWordCounter' => true,
'name' => 'data['.$k.'][value]',
'value' => $model->data->{$k}->value ?? null,
'required' => true,
'model' => $model ?? null,
'helper' => __('Must be in 3 characters minimum, 255 maximum.')
])
</div>
</div>
</fieldset>
</div>
@break
@case(intval(App\Enums\InputType::number->value))
<div class="form-group">
<label for="{{ $k }}"><small>{{ $input['label'] }}*</small></label>
<input id="{{ $k }}" type="number" step="0.01" class="form-control"
name="data[{{ $k }}][value]" placeholder="{{ $input['label'] }}"
value="{{ old("data.$k.value") ?? ($input['value'] ?? '') }}" required>
</div>
@break
@case(intval(App\Enums\InputType::text->value))
<div class="col-12 mb-3">
<fieldset class="border bg-white p-3">
<legend>{{ $input['label'] }} *</legend>
<div class="row mb-3">
<div class="col-12">
@include('bo.partials.inputs.text-html', [
'id' => Str::slug($input["label"]),
'name' => 'data['.$k.'][value]',
'value' => $model->data->{$k}->value ?? null,
'model' => $model ?? null,
])
</div>
</div>
</fieldset>
</div>
@include('modules.input-error', ['inputName' => 'data['.$k.'][value]'])
@break
@case(intval(App\Enums\InputType::boolean->value))
@case(intval(App\Enums\InputType::image->value))
<div class="col-12 mb-3">
<fieldset class="border h-100 bg-white p-3">
<legend>{{ $input['label'] }} *</legend>
<div class="row">
{{-- PICTURE_ALT --}}
<div class="col-12 col-md-6 mb-3">
@include('bo.partials.inputs.varchar', [
'id' => Str::slug($input["label"]) . '-alt',
'label' => __('Alt attribute of the image'),
'hasWordCounter' => true,
'name' => 'data['.$k.'][alt]',
'value' => $model->data->{$k}->alt ?? null,
'required' => true,
'model' => $model ?? null,
'helper' => __('Must describe your image for search engines and blind people (5 to 6 words, minimum 3 characters).'),
])
</div>
{{-- PICTURE_TITLE --}}
<div class="col-12 col-md-6 mb-3">
@include('bo.partials.inputs.varchar', [
'id' => Str::slug($input["label"]) . '-title',
'label' => __('Title attribute of the image'),
'hasWordCounter' => true,
'name' => 'data['.$k.'][title]',
'value' => $model->data->{$k}->title ?? null,
'required' => true,
'model' => $model ?? null,
'helper' => __('Useful for users, displayed by moving the mouse over the image (minimum 3 characters).'),
])
</div>
{{-- PICTURE --}}
<div class="col-12 mb-3">
@include('bo.partials.inputs.varchar-picture', [
'id' => Str::slug($input["label"]),
'name' => 'data['.$k.'][value]',
'width' => $input['dimensions']['width'],
'height' => $input['dimensions']['height'],
'model' => $model ?? null,
'value' => $model->data->{$k}->value ?? null,
'helper' => __('Must be :widthpx (width) by :heightpx (height).', [
'width' => $input['dimensions']['width'],
'height' => $input['dimensions']['height'],
]),
'required' => true,
'preview' => true,
])
</div>
</div>
</fieldset>
</div>
@break
@case(intval(App\Enums\InputType::imageArray->value))
@case(intval(App\Enums\InputType::labelArray->value))
@case(intval(App\Enums\InputType::selectEnum->value))
<div class="col-12 col-md-6 mb-3">
<fieldset class="border h-100 bg-white p-3">
<legend>{{ $input['label'] }} *</legend>
<div class="row">
<div class="col-12 mb-3">
@include('bo.partials.inputs.select-enum', [
'id' => Str::slug($input["label"]),
'label' => $input['label'] . ' associated',
'name' => 'data['.$k.'][value]',
'model' => $model ?? null,
'require' => true,
'associatedModels' => $selectedEnum ?? [],
'helper' => trans('Selectionnez un :enum parmis la liste.', ['enum' => $input['label']]),
])
</div>
</div>
</fieldset>
</div>
@break
@default
@php throw new Exception('Input type not handled'); @endphp
@endswitch
@endforeach

View file

@ -0,0 +1,60 @@
@if ($paginator->hasPages())
<nav>
<ul class="pagination d-flex justify-content-center align-items-center my-3">
{{-- Previous Page Link --}}
@if ($paginator->onFirstPage())
<li class="page-item disabled" data-bs-toggle="tooltip" data-bs-placement="top"
title="{{ __('Previous page') }}" aria-disabled="true" aria-label="@lang('Previous page')">
<span class="page-link" aria-hidden="true">&lsaquo;</span>
</li>
@else
<li class="page-item">
<a class="page-link" data-bs-toggle="tooltip" data-bs-placement="top"
title="{{ __('Previous page') }}" href="{{ $paginator->previousPageUrl() }}"
rel="prev" aria-label="@lang('Previous page')">&lsaquo;</a>
</li>
@endif
{{-- Pagination Elements --}}
@foreach ($elements as $element)
{{-- "Three Dots" Separator --}}
@if (is_string($element))
<li class="page-item disabled" aria-disabled="true"><span
class="page-link">{{ $element }}</span></li>
@endif
{{-- Array Of Links --}}
@if (is_array($element))
@foreach ($element as $page => $url)
@if ($page == $paginator->currentPage())
<li class="page-item active" data-bs-toggle="tooltip" data-bs-placement="top"
title="{{ __('Actual page') }}" aria-current="page">
<span class="page-link">{{ $page }}</span>
</li>
@else
<li class="page-item">
<a class="page-link" data-bs-toggle="tooltip" data-bs-placement="top"
title="{{ __('Page n°:id', ['id' => $page]) }}"
href="{{ $url }}">{{ $page }}</a>
</li>
@endif
@endforeach
@endif
@endforeach
{{-- Next Page Link --}}
@if ($paginator->hasMorePages())
<li class="page-item">
<a class="page-link" data-bs-toggle="tooltip" data-bs-placement="top" title="{{ __('Next page') }}"
href="{{ $paginator->nextPageUrl() }}" rel="next"
aria-label="@lang('Next page')">&rsaquo;</a>
</li>
@else
<li class="page-item disabled" data-bs-toggle="tooltip" data-bs-placement="top"
title="{{ __('Next page') }}" aria-disabled="true" aria-label="@lang('Next page')">
<span class="page-link" aria-hidden="true">&rsaquo;</span>
</li>
@endif
</ul>
</nav>
@endif

View file

@ -8,7 +8,7 @@
$iArgs['search'] = request()->sort_way;
}
@endphp
<div class="row mb-3">
<div class="row">
<div class="col-12">
<form id="searchForm" action="{{ request()->url() }}">
<div class="input-group">
@ -25,11 +25,13 @@
@if(request()->group)
<input class="d-none" type="text" name="group" value="{{ request()->group }}">
@endif
<input type="search" id="searchField" name="search" class="form-control" value="{{ request()->search }}" placeholder="{{ __('Vos mots clés ici...') }}"/>
<button type="submit" class="btn btn-primary" data-bs-toggle="tooltip" title="{{ __('Lancer la recherche') }}">
<input type="search" id="searchField" name="search" class="form-control"
value="{{ request()->search }}" placeholder="{{ __('Your keywords here...') }}"/>
<button type="submit" class="btn btn-primary" data-bs-toggle="tooltip" title="{{ __('Start search') }}">
<i class="fas fa-search"></i>
</button>
<button id="searchDelete" type="button" class="btn btn-danger" data-bs-toggle="tooltip" title="{{ __('Effacer la recherche') }}">
<button id="searchDelete" type="button" class="btn btn-danger" data-bs-toggle="tooltip"
title="{{ __('Delete search') }}">
<i class="fas fa-backspace"></i>
</button>
@push('scripts')

View file

@ -30,7 +30,7 @@ $ignore = (isset($ignore) and is_array($ignore)) ? $ignore : [];
@if(!session("$routeName.sort_way") or session("$routeName.sort_col") !== $col or (session("$routeName.sort_way") === 'desc' and session("$routeName.sort_col") === $col))
<a class="col_sort text-info lead asc"
href="{{ request()->fullUrlWithQuery(array_merge($iArgs, ['sort_way' => 'asc', 'rst' => null])) }}"
title="{{ __('Trier :name en ordre ascendant', ['name' => Str::lower($colname)]) }}"
title="{{ __('Sort :name in ascending order', ['name' => Str::lower($colname)]) }}"
data-bs-toggle="tooltip">
&nbsp;&darr;&nbsp;
</a>
@ -38,7 +38,7 @@ $ignore = (isset($ignore) and is_array($ignore)) ? $ignore : [];
@if(!session("$routeName.sort_way") or session("$routeName.sort_col") !== $col or (session("$routeName.sort_way") === 'asc' and session("$routeName.sort_col") === $col))
<a class="col_sort text-info lead desc"
href="{{ request()->fullUrlWithQuery(array_merge($iArgs, ['sort_way' => 'desc', 'rst' => null])) }}"
title="{{ __('Trier :name en ordre descendant', ['name' => Str::lower($colname)]) }}"
title="{{ __('Sort :name in descending order', ['name' => Str::lower($colname)]) }}"
data-bs-toggle="tooltip">
&nbsp;&uarr;&nbsp;
</a>
@ -52,11 +52,11 @@ $ignore = (isset($ignore) and is_array($ignore)) ? $ignore : [];
<a class="btn btn-small btn-danger"
href="{{ request()->fullUrlWithQuery(['sort_col' => null, 'sort_way' => null, 'rst' => true, 'search' => null]) }}"
data-bs-toggle="tooltip"
title="{{ __('Effacer le tri') }}">
title="{{ __('Delete the sorting') }}">
<i class="fa fa-eraser"></i>
</a>
@else
<span data-bs-toggle="tooltip" data-bs-placement="top" title="{{ __('Ici, les flèches servent à trier et non pas à filtrer') }}">
<span data-bs-toggle="tooltip" data-bs-placement="top" title="{{ __('Here, the arrows are used to sort and not to filter') }}">
<i class="fas fa-question-circle"></i>
</span>
@endif

View file

@ -0,0 +1,43 @@
@extends('bo.layout')
@section('title', sprintf('%s - %s', Str::singular(__('models.classes.concepts')), Str::ucfirst(__('create'))))
@section('description', __('Creation of a new concept.'))
@section('breadcrumb', request()->route()->getName())
@section('content')
<div class="card">
<div class="card-header">
<div class="row">
<div class="col">
<h1 class="col h2">
<a href="{{ route('bo.concepts.index') }}"
class="btn btn-primary text-decoration-none m-0"
data-bs="tooltip"
data-bs-placement="top"
title="{{ __('form.return_list') }}">
<i class="fa-solid fa-arrow-left"></i>
</a>
<span class="fw-bold">{{ Str::singular(__('models.classes.concepts')) }}</span>&nbsp;<small class="text-muted">{{ __('create') }}</small>
</h1>
</div>
</div>
</div>
<div class="card-body bg-light">
<form id="formSubmit" action="{{ route('bo.concepts.store') }}" method="POST" enctype="multipart/form-data">
@csrf
@method('PUT')
@include('bo.pages.concepts.form-inputs')
<div class="row">
<div class="col text-center">
<button type="submit" class="btn btn-primary"
title="{{ __('Save :model', ['model' => Str::singular(__('models.classes.concepts'))]) }}"
data-bs-toggle="tooltip">
{{ __('Save') }}
</button>
</div>
</div>
</form>
</div>
</div>
@endsection

View file

@ -0,0 +1,43 @@
@extends('bo.layout', ['brParam' => $conceptModel])
@section('title', sprintf('%s - %s %s', Str::singular(__('models.classes.concepts')), Str::ucfirst(__('edit')), $conceptModel->seo_title))
@section('description', $conceptModel->seo_description)
@section('breadcrumb', request()->route()->getName())
@section('content')
<div class="card">
<div class="card-header">
<div class="row">
<div class="col">
<h1 class="col h2">
<a href="{{ route('bo.concepts.index') }}"
class="btn btn-primary text-decoration-none m-0"
data-bs="tooltip"
data-bs-placement="top"
title="{{ __('form.return_list') }}">
<i class="fa-solid fa-arrow-left"></i>
</a>
<span class="fw-bold">{{ Str::singular(__('models.classes.concepts')) }}</span>&nbsp;<small class="text-muted">{{ __('edit') }}</small>
</h1>
</div>
</div>
</div>
<div class="card-body bg-light">
<form id="formSubmit" action="{{ route('bo.concepts.update', $conceptModel->id) }}" method="POST" enctype="multipart/form-data">
@csrf
@method('PUT')
@include('bo.pages.concepts.form-inputs', ['model' => $conceptModel])
<div class="row">
<div class="col text-center">
<button type="submit" class="btn btn-primary"
title="{{ __('Save :model', ['model' => Str::singular(__('models.classes.concepts'))]) }}"
data-bs-toggle="tooltip">
{{ __('Save') }}
</button>
</div>
</div>
</form>
</div>
</div>
@endsection

View file

@ -0,0 +1,178 @@
<div class="row">
{{-- SEO_TITLE --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="border h-100 bg-white p-3">
<legend>{{ __('SEO Title') }} *</legend>
<div class="row">
<div class="col-12 mb-3">
@include('bo.partials.inputs.varchar', [
'id' => 'conceptSeoTitle',
'label' => __('SEO title of the :model', ['model' => Str::singular(__('models.classes.concepts'))]),
'hasWordCounter' => true,
'name' => 'seo_title',
'required' => true,
'model' => $conceptModel ?? null,
'helper' => __('Must be in 3 characters minimum, 255 maximum.'),
'placeholder' => __('A beautiful title'),
])
</div>
</div>
</fieldset>
</div>
{{-- SEO_DESCRIPTION --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="border h-100 bg-white p-3">
<legend>{{ __('SEO Description') }} *</legend>
<div class="row">
<div class="col-12">
@include('bo.partials.inputs.varchar-multiline', [
'id' => 'conceptSeoDescription',
'label' => __('SEO description of the :model', ['model' => Str::singular(__('models.classes.concepts'))]),
'name' => 'seo_description',
'model' => $conceptModel ?? null,
'required' => true,
'helper' => __('Must be in 50 characters minimum, 160 maximum.'),
'placeholder' => __('A description of the content for search engines.'),
])
</div>
</div>
</fieldset>
</div>
{{-- TITLE --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="border h-100 bg-white p-3">
<legend>{{ __('Title') }} *</legend>
<div class="row">
<div class="col-12 mb-3">
@include('bo.partials.inputs.varchar', [
'id' => 'conceptTitle',
'label' => __('Title of the :model', ['model' => Str::singular(__('models.classes.concepts'))]),
'hasWordCounter' => true,
'name' => 'title',
'required' => true,
'model' => $conceptModel ?? null,
'helper' => __('Must be in 3 characters minimum, 255 maximum.'),
'placeholder' => __('A beautiful title'),
])
</div>
</div>
</fieldset>
</div>
{{-- SUBTITLE --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="border h-100 bg-white p-3">
<legend>{{ __('Subtitle') }} *</legend>
<div class="row">
<div class="col-12 mb-3">
@include('bo.partials.inputs.varchar', [
'id' => 'conceptSubtitle',
'label' => __('Subtitle of the :model', ['model' => Str::singular(__('models.classes.concepts'))]),
'hasWordCounter' => true,
'name' => 'subtitle',
'required' => true,
'model' => $conceptModel ?? null,
'helper' => __('Must be in 3 characters minimum, 255 maximum.'),
'placeholder' => __('A beautiful subtitle'),
])
</div>
</div>
</fieldset>
</div>
{{-- NAME --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="border h-100 bg-white p-3">
<legend>{{ __('Name') }} *</legend>
<div class="row">
<div class="col-12 mb-3">
@include('bo.partials.inputs.varchar', [
'id' => 'conceptName',
'label' => __('Name of the :model', ['model' => Str::singular(__('models.classes.concepts'))]),
'hasWordCounter' => true,
'name' => 'name',
'required' => true,
'model' => $conceptModel ?? null,
'helper' => __('Must be in 3 characters minimum, 255 maximum.'),
'placeholder' => __('A beautiful name'),
])
</div>
</div>
</fieldset>
</div>
{{-- DESCRIPTION --}}
<div class="col-12 mb-3">
<fieldset class="border bg-white p-3">
<legend>{{ __('Description') }} *</legend>
<div class="row mb-3">
<div class="col-12">
@include('bo.partials.inputs.text-html', [
'id' => 'conceptDescription',
'name' => 'description',
'model' => $conceptModel ?? null,
'helper' => __('Proofread your content before publishing it (minimum 15 characters).'),
])
</div>
</div>
</fieldset>
</div>
{{-- MINICRUD --}}
<div class="col-12 mb-3">
<button class="btn btn-primary col-form-label rounded-0 dropdown-toggle w-100" type="button" data-bs-toggle="collapse" data-bs-target="#collapseExample" aria-expanded="false" aria-controls="collapseExample">
<b>{{ __('ALL BLOCS') }}</b>
</button>
<div class="collapse" id="collapseExample">
<div class="card card-body bg-light rounded-0">
<div class="row">
@include('bo.modules.minicrud-form-inputs', [
'model' => $conceptModel ?? $conceptData,
'selectedEnum' => \App\Enums\Concepts\ColorsEnum::getData()
])
</div>
</div>
</div>
</div>
{{-- PICTURE --}}
<div class="col-12 mb-3">
<fieldset class="border h-100 bg-white p-3">
<legend>{{ __('Illustration') }} *</legend>
<div class="row">
{{-- PICTURE_ALT --}}
<div class="col-12 col-md-6 mb-3">
@include('bo.partials.inputs.varchar', [
'id' => 'homePart3PictureAlt',
'label' => __('Alt attribute of the :model', ['model' => Str::singular(__('models.classes.concepts'))]),
'hasWordCounter' => true,
'name' => 'picture_alt',
'required' => true,
'model' => $conceptModel ?? null,
'helper' => __('Must describe your image for search engines and blind people (5 to 6 words, minimum 3 characters).'),
])
</div>
{{-- PICTURE_TITLE --}}
<div class="col-12 col-md-6 mb-3">
@include('bo.partials.inputs.varchar', [
'id' => 'homePart3PictureTitle',
'label' => __('Title attribute of the :model', ['model' => Str::singular(__('models.classes.concepts'))]),
'hasWordCounter' => true,
'name' => 'picture_title',
'required' => true,
'model' => $conceptModel ?? null,
'helper' => __('Useful for users, displayed by moving the mouse over the image (minimum 3 characters).'),
])
</div>
{{-- PICTURE --}}
<div class="col-12 mb-3">
@include('bo.partials.inputs.varchar-picture', [
'id' => 'homePart3Picture',
'name' => 'picture',
'width' => 1512,
'height' => 354,
'model' => $conceptModel ?? null,
'helper' => __('Must be 1512px (width) by 354px (height).'),
'required' => true,
'preview' => true,
])
</div>
</div>
</fieldset>
</div>
</div>

View file

@ -0,0 +1,122 @@
@extends('bo.layout')
@section('title', sprintf('%s - %s', __('models.classes.concepts'), Str::ucfirst(__('liste'))))
@section('content')
<div class="card">
<div class="card-header">
<div class="row">
<div class="col">
<h1 class="col h2">
<span class="fw-bold">{{ __('models.classes.concepts') }}</span>&nbsp;<small class="text-muted">{{ Str::ucfirst(__('list')) }}</small>
</h1>
</div>
<div class="col-md-3 d-flex align-items-center">
<div class="input-group justify-content-end">
<a href="{{ route('bo.concepts.create') }}"
class="btn btn-primary"
title="{{ __('Create a :model', ['model' => Str::singular(__('models.classes.concepts'))]) }}"
data-bs-toggle="tooltip">
<i class="fa-solid fa-plus"></i>
</a>
</div>
</div>
</div>
</div>
<div class="card-body">
@include('bo.modules.search-bar')
<div class="row justify-content-center">
<div class="col-12 col-md-6 p-0">
{{ $conceptModels->withQueryString()->onEachSide(1)->links() }}
</div>
</div>
<div class="row">
<div class="col">
<div class="table-responsive">
<table class="table fix-action m-0">
<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"));
$cols = ['name' => __('Name'), 'title' => __('Title'), 'subtitle' => __('Subtitle'), 'order' => __('Order'), 'updated_at' => __('Updated at')];
@endphp
@include('bo.modules.table-col-sorter', [ 'cols' => $cols, 'mobileHide' => ['status']])
</thead>
<tbody>
@if($conceptModels->count())
@foreach ($conceptModels as $conceptModel)
<tr>
<td class="align-middle">{{ $conceptModel->name }}</td>
<td class="align-middle">{{ $conceptModel->title }}</td>
<td class="align-middle">{{ $conceptModel->subtitle }}</td>
@if(!$noOrder or $rst)
<td class="d-flex">
<form action="{{ route('bo.concepts.change-order', ['concept' => $conceptModel, 'action' => 'up']) }}"
method="POST"
class="text-center @if($loop->first and $conceptModels->onFirstPage()) invisible @endif">
@csrf
<button type="submit"
class="btn btn-sm @if(($loop->first and $conceptModels->onFirstPage())) disabled @endif"
title="{{ __('Change the order of appearance upwards') }}"
data-bs-toggle="tooltip">
<i class="fas fa-arrow-alt-circle-up"></i>
</button>
</form>
<form action="{{ route('bo.concepts.change-order', ['concept' => $conceptModel, 'action' => 'down']) }}"
method="POST"
class="text-center @if($loop->last and $conceptModels->currentPage() === $conceptModels->lastPage()) invisible @endif">
@csrf
<button type="submit"
class="btn btn-sm @if($loop->last and $conceptModels->currentPage() === $conceptModels->lastPage()) disabled @endif"
title="{{ __('Change the order of appearance downwards') }}"
data-bs-toggle="tooltip">
<i class="fas fa-arrow-alt-circle-down"></i>
</button>
</form>
</td>
@endif
<td class="align-middle">
<span class="badge bg-warning text-dark text-wrap">{{ $conceptModel->updated_at->formatLocalized('%d/%m/%Y à %H:%M') }}</span>
</td>
<td class="align-middle">
<form action="{{ route('bo.concepts.destroy', $conceptModel->id) }}" method="POST" class="btn-group confirmDeleteJS" novalidate>
<a class="btn btn-sm btn-primary"
href="{{ route('bo.concepts.edit', $conceptModel->id) }}"
title="{{ __('Edit this :model', ['model' => Str::singular(__('models.classes.concepts'))]) }}"
data-bs-toggle="tooltip">
<i class="fa-solid fa-pencil"></i>
</a>
@method('DELETE')
@csrf
<div class="btn-group" role="group">
<button class="btn btn-sm btn-danger"
title="{{ __('Delete this :model', ['model' => Str::singular(__('models.classes.concepts'))]) }}"
data-bs-toggle="tooltip">
<i class="fa-solid fa-trash"></i>
</button>
</div>
</form>
</td>
</tr>
@endforeach
@else
<tr>
<td colspan="5" class="text-center">
<span class="h3">{{ __('No results for :models', ['models' => __('models.classes.concepts') ])}}</span>
</td>
</tr>
@endif
</tbody>
</table>
</div>
</div>
</div>
<div class="row justify-content-center">
<div class="col-12 col-md-6 p-0">
{{ $conceptModels->withQueryString()->onEachSide(1)->links() }}
</div>
</div>
</div>
</div>
@endsection

View file

@ -10,7 +10,7 @@
<div class="col">
<div class="card text-center">
<div class="card-header fw-bold">
<h1 class="h4 fw-bold">{{ __('Dashboard') }}</h1>
<h1 class="h2 fw-bold">{{ __('Dashboard') }}</h1>
</div>
<div class="card-body">
<h5 class="card-title">

View file

@ -10,8 +10,8 @@
<div class="card-header">
<div class="row">
<div class="col">
<h1 class="col h4">
<span class="fw-bold">{{ __('models.classes.homes') }}</span>&nbsp;<small class="text-muted">{{ __('edit') }}</small>
<h1 class="col h2">
<span class="fw-bold">{{ __('models.classes.homes') }}</span>&nbsp;<small class="text-muted">{{ Str::ucfirst(__('edit')) }}</small>
</h1>
</div>
</div>
@ -22,7 +22,7 @@
@method('PUT')
@include('bo.pages.homes.form-inputs', compact('homeModel'))
<div class="row">
<div class="col text-center text-md-end">
<div class="col text-center">
<button type="submit" class="btn btn-primary"
title="{{ __('Save :model', ['model' => Str::singular(__('models.classes.homes'))]) }}"
data-bs-toggle="tooltip">

View file

@ -61,17 +61,17 @@
</div>
</fieldset>
</div>
{{-- PART.1 TITLE SPAN --}}
{{-- PART.1 SUBTITLE --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="border h-100 bg-white p-3">
<legend>{{ __('Subtitle') }} *</legend>
<div class="row">
<div class="col-12 mb-3">
@include('bo.partials.inputs.varchar', [
'id' => 'homePart1TitleSpan',
'id' => 'homePart1Subtitle',
'label' => __('Subtitle of the part.1'),
'hasWordCounter' => true,
'name' => 'part1_title_span',
'name' => 'part1_subtitle',
'required' => true,
'model' => $homeModel,
'helper' => __('Must be in 3 characters minimum, 255 maximum.'),
@ -154,17 +154,17 @@
</div>
</fieldset>
</div>
{{-- PART.2 TITLE SPAN --}}
{{-- PART.2 SUBTITLE --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="border h-100 bg-white p-3">
<legend>{{ __('Subtitle') }} *</legend>
<div class="row">
<div class="col-12 mb-3">
@include('bo.partials.inputs.varchar', [
'id' => 'homePart2TitleSpan',
'id' => 'homePart2Subtitle',
'label' => __('Subtitle of the part.2'),
'hasWordCounter' => true,
'name' => 'part2_title_span',
'name' => 'part2_subtitle',
'required' => true,
'model' => $homeModel,
'helper' => __('Must be in 3 characters minimum, 255 maximum.'),

View file

@ -3,7 +3,7 @@
@endif
<select id="{{ $name }}" class="form-select" name="{{ $name }}" aria-describedby="{{ $id }}Help" @if(!empty($require))required @endif>
@foreach ($associatedModels as $associatedModel)
<option value="{{ $associatedModel['value'] }}" @if (old($name, $model->{$name}->value ?? 1) == $associatedModel['value']) selected @endif>
<option value="{{ $associatedModel['value'] }}" @if (old($name, $model->{$name}->value ?? (isset($model->data)) ? intval($model->data->{$k}->value) : '' ?? 1) == $associatedModel['value']) selected @endif>
{{ $associatedModel['label'] }}
</option>
@endforeach

View file

@ -1,12 +1,12 @@
@php
$data = [
'name' => $name,
'text' => old($name, $model->{$name} ?? '')
'text' => old($name, $model->{$name} ?? $value ?? '')
];
@endphp
<div class="ck-editor" data-json='@json($data)'></div>
@include('modules.input-error', [
'inputName' => $name,
'helperId' => "{$id}Help",
'helper' => $helper ?? __('Relisez bien votre contenu avant de le publier (minimum 15 caractères).'),
'helper' => $helper ?? __('Proofread your content before publishing it (minimum 15 characters).'),
])

View file

@ -5,11 +5,11 @@ $data = [
'helper' => $helper,
'width' => $width,
'height' => $height,
'value' => $model->{$name} ?? '',
'value' => $model->{$name} ?? $value ?? '',
'preview' => $preview ?? false,
'browseEvent' => $browseEvent ?? false,
'required' => $required ?? false,
];
@endphp
@include('modules.input-error', ['inputName' => $name, 'helper' => null])
<div class="image-input" data-json='@json($data)'></div>
@include('modules.input-error', ['inputName' => $name, 'helper' => null])

View file

@ -8,19 +8,19 @@
@endif
<input name="{{ $name }}"
type="text"
value="{{ old($name, $model->{$name} ?? '') }}"
value="{{ old($name, $model->{$name} ?? $value ?? '') }}"
class="form-control
@if(!empty($hasWordCounter ?? false))
need-word-counter
@endif"
id="{{ $id }}"
aria-describedby="{{ $id }}Help"
placeholder="{{ $placeholder ?? __('Un nom beau et explicite') }}"
placeholder="{{ $placeholder ?? __('A beautiful title') }}"
@if(!empty($required))
required
@endif>
@include('modules.input-error', [
'inputName' => $name,
'helperId' => '{{ $id }}Help',
'helperId' => $id.'Help',
'helper' => $helper
])

View file

@ -14,7 +14,7 @@
</div>
<div class="row justify-content-center">
<div class="col-12 col-md-10 col-lg-8 mt-3">
@include('modules.breadcrumb.breadcrumb-body')
@include('modules.breadcrumbs.breadcrumb-body')
</div>
</div>
<div class="row p-md-3 justify-content-center">

View file

@ -1,10 +0,0 @@
@hasSection('breadcrumb')
@if(Breadcrumbs::exists(app()->view->getSections()['breadcrumb']))
{{ call_user_func_array('Breadcrumbs::view', array_merge(
['breadcrumbs::json-ld', app()->view->getSections()['breadcrumb']],
!empty($brParam) ? (is_array($brParam) ? $brParam : [$brParam]) : []
)) }}
@endif
@elseif(Breadcrumbs::exists(request()->route()->getName()))
{{ Breadcrumbs::view('breadcrumbs::json-ld', request()->route()->getName()) }}
@endif

View file

@ -1,15 +0,0 @@
@if (isset($breadcrumbs))
@unless ($breadcrumbs->isEmpty())
<nav aria-label="breadcrumb" class="d-inline">
<ol class="breadcrumb d-inline-flex">
@foreach ($breadcrumbs as $breadcrumb)
@if (!is_null($breadcrumb->url) && !$loop->last)
<li class="breadcrumb-item"><a href="{{ $breadcrumb->url }}" rel="nofollow">{{ $breadcrumb->title }}</a></li>
@else
<li class="breadcrumb-item active" aria-current="page">{{ $breadcrumb->title }}</li>
@endif
@endforeach
</ol>
</nav>
@endunless
@endif

View file

@ -5,6 +5,8 @@
!empty($brParam) ? (is_array($brParam) ? $brParam : [$brParam]) : []
)) }}
@endif
@elseif(Breadcrumbs::exists(request()->route()->getName()))
@elseif(request()->route() != null)
@if(Breadcrumbs::exists(request()->route()->getName()))
{{ Breadcrumbs::render(request()->route()->getName()) }}
@endif
@endif

View file

@ -0,0 +1,9 @@
@hasSection('breadcrumb')
@if(Breadcrumbs::exists(app()->view->getSections()['breadcrumb']))
{{ Breadcrumbs::view('breadcrumbs::json-ld', app()->view->getSections()['breadcrumb'], $brParam ?? null) }}
@endif
@elseif(request()->route() != null)
@if (Breadcrumbs::exists(request()->route()->getName()))
{{ Breadcrumbs::view('breadcrumbs::json-ld', request()->route()->getName()) }}
@endif
@endif

View file

@ -0,0 +1,17 @@
@if (count($breadcrumbs))
<ol class="breadcrumb flex-nowrap @if(!Request::is('bo/*')) w-100 m-0 @else mb-3 @endif">
@foreach ($breadcrumbs as $breadcrumb)
@if ($breadcrumb->url && !$loop->last)
<li class="breadcrumb-item d-flex">
<a class="text-decoration-none @if(!Request::is('bo/*')) text-white py-3 @endif" href="{{ $breadcrumb->url }}">
{{ $breadcrumb->title }}
</a>
</li>
@else
<li class="breadcrumb-item btn-games d-flex align-items-center flex-grow-1 active">
{{ $breadcrumb->title }}
</li>
@endif
@endforeach
</ol>
@endif

View file

@ -0,0 +1,30 @@
<?php
use App\Models\Concept;
use App\Models\Home;
use Diglactic\Breadcrumbs\Breadcrumbs;
use Diglactic\Breadcrumbs\Generator;
// * DASHBOARD
Breadcrumbs::for('bo.dashboard', function (Generator $trail) {
$trail->push(trans('Dashboard'), route('bo.dashboard'));
});
// * HOMEPAGE
Breadcrumbs::for('bo.homes.edit', function (Generator $trail, Home $homeModel) {
$trail->push(trans('Homepage'), route('bo.homes.edit', $homeModel));
$trail->push(trans('Edit'));
});
// * CONCEPTS
Breadcrumbs::for('bo.concepts.index', function (Generator $trail) {
$trail->push(trans('Concepts'), route('bo.concepts.index'));
});
Breadcrumbs::for('bo.concepts.create', function (Generator $trail) {
$trail->parent('bo.concepts.index');
$trail->push(trans('Create'), route('bo.concepts.create'));
});
Breadcrumbs::for('bo.concepts.edit', function (Generator $trail, Concept $concept) {
$trail->parent('bo.concepts.index');
$trail->push(trans('Edit'), route('bo.concepts.edit', $concept->slug));
});

View file

@ -1,5 +1,6 @@
<?php
use App\Http\Controllers\Bo\ConceptController;
use App\Http\Controllers\Bo\DashboardController;
use App\Http\Controllers\Bo\HomeController;
use Illuminate\Support\Facades\Auth;
@ -26,12 +27,21 @@ Route::prefix('bo')
}
);
Route::middleware('auth:backend')->group(function () {
Route::get('/', [DashboardController::class, 'index'])->name('dashboard');
Route::middleware('auth:backend')->group(function () {
Route::get('/', [DashboardController::class, 'index'])->name('dashboard');
// * HOMEPAGE
Route::resource('homes', HomeController::class)->except(['show'])
->withoutMiddleware('htmlsanitize')
->middleware(['htmlsanitize:part1_text_1,part1_text_2,part1_text_3,part2_text']);
});
});
// * HOMEPAGE
Route::resource('homes', HomeController::class)->except(['show'])
->withoutMiddleware('htmlsanitize')
->middleware(['htmlsanitize:part1_text_1,part1_text_2,part1_text_3,part2_text']);
// * CONCEPTS
Route::resource('concepts', ConceptController::class)->except(['show'])
->withoutMiddleware('htmlsanitize')
->middleware(['htmlsanitize:description,data']);
Route::put('/concepts/create/new', [ConceptController::class, 'store'])->name('concepts.store');
Route::post('/concepts/{concept}/change-order', [ConceptController::class, 'changeOrder'])
->name('concepts.change-order');
});
}
);