Merge branch 'issue-33' into 'develop'

feat: add team slide in BO

See merge request ldc-web/projets/volena!30
This commit is contained in:
noah.lancien 2025-02-20 13:23:46 +00:00
commit 546f955171
16 changed files with 668 additions and 478 deletions

View file

@ -8,9 +8,8 @@ enum CkEditorConfigName: int
{
use BaseEnum;
case minimal = 1;
case base = 2;
case advanced = 3;
case minimal = 1;
case basic = 2;
/**
* Optionnal labels definition.
@ -19,7 +18,6 @@ enum CkEditorConfigName: int
*/
private const LABELS = [
self::minimal->name => 'minimal',
self::base->name => 'base',
self::advanced->name => 'advanced',
self::basic->name => 'basic',
];
}

View file

@ -11,6 +11,7 @@ enum SliderTypeEnum: int
case home = 1;
case better_life = 2;
case respect_earth = 3;
case team = 4;
/**
* Optionnal labels definition.
@ -21,5 +22,6 @@ enum SliderTypeEnum: int
self::home->name => 'Accueil',
self::better_life->name => 'Mieux vivre',
self::respect_earth->name => 'Respecter la terre',
self::team->name => 'Nous rejoindre',
];
}

View file

@ -48,6 +48,7 @@ class StaticPageController extends Controller
$ambitionSection = $sectionModels->where('section', PageSectionEnum::home_ambition)->first();
$teamSection = $sectionModels->where('section', PageSectionEnum::home_team)->first();
$lifeSection = $sectionModels->where('section', PageSectionEnum::home_life)->first();
$slideTeams = Slide::query()->where('type', SliderTypeEnum::team)->get();
return view('fo.pages.home', compact(
'staticPageModel',
@ -59,6 +60,7 @@ class StaticPageController extends Controller
'ambitionSection',
'teamSection',
'lifeSection',
'slideTeams',
));
}

View file

@ -3,6 +3,7 @@
namespace App\Http\Requests\Bo\Slides;
use App\Enums\Slides\SliderTypeEnum;
use App\Rules\WysiwygLength;
use App\Traits\Requests\HasPicture;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Enum;
@ -30,7 +31,7 @@ class StoreSlideRequest extends FormRequest
{
$this->merge([
'button_status' => $this->boolean('button_status'),
'button_new_tab' => $this->boolean('button_new_tab')
'button_new_tab' => $this->boolean('button_new_tab'),
]);
$this->mergePicture('picture');
}
@ -54,19 +55,31 @@ class StoreSlideRequest extends FormRequest
'between:3,255'
],
'content' => [
'required_if:type,' . SliderTypeEnum::home->value(),
'required_if:type,' . SliderTypeEnum::home->value() . ', ' . SliderTypeEnum::team->value(),
'nullable',
'string',
new WysiwygLength(3, 275),
'max:500'
],
'video_id' => 'nullable|string|regex:%([^"&?\/\s]{11})%i',
'picture_alt' => [
'required_unless:type,' . SliderTypeEnum::team->value(),
'nullable',
'string',
'between:3,255'
],
'picture_title' => [
'required_unless:type,' . SliderTypeEnum::team->value(),
'nullable',
'string',
'between:3,255'
],
'video_id' => 'nullable|string|regex:%([^"&?\/\s]{11})%i',
'picture_alt' => 'required|string|between:3,255',
'picture_title' => 'required|string|between:3,255',
'button_label' => 'required_if:button_status,true|nullable|string|between:3,35',
'button_url' => 'required_if:button_status,true|nullable|string|between:3,255',
'button_status' => [
'required_unless:type,' . SliderTypeEnum::home->value(),
'nullable', 'boolean'
'required_unless:type,' . SliderTypeEnum::home->value() . ', ' . SliderTypeEnum::team->value(),
'nullable',
'boolean'
],
'button_new_tab' => 'required|boolean',
];
@ -106,18 +119,22 @@ class StoreSlideRequest extends FormRequest
public function messages(): array
{
return [
'title' => [
'required_unless' => __('Le champ :attribute est obligatoire si le slider n\'est pas :name.', [
'attribute' => trans('validation.attributes.title'),
'name' => SliderTypeEnum::home->label()
]),
],
'content' => [
'required_unless' => __('Le champ :attribute est obligatoire si le slider n\'est pas :name.', [
'title.required_unless' => __('Le champ :attribute est obligatoire si le slider n\'est pas :name.', [
'attribute' => trans('validation.attributes.title'),
'name' => SliderTypeEnum::home->label()
]),
'content.required_if' => __('Le champ :attribute est obligatoire si le slider est :name1 ou :name2.', [
'attribute' => trans('validation.attributes.content'),
'name1' => SliderTypeEnum::home->label(),
'name2' => SliderTypeEnum::team->label()
]),
'content.max' => __(
'Le champ :attribute ne peut pas contenir plus de :max caractères (balises incluses).',
[
'attribute' => trans('validation.attributes.content'),
'name' => SliderTypeEnum::home->label()
]),
]
'max' => '500',
]
),
];
}
}

View file

@ -7,6 +7,7 @@ use App\Lib\Helpers\FileStorageHelper;
use App\Traits\Models\ActivityLog;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Validation\ValidationException;
/**
* @property \App\Enums\Slides\SliderTypeEnum $type Slide type.
@ -30,6 +31,12 @@ class Slide extends Model
use ActivityLog;
use HasFactory;
/** @var integer Minimum number of published slides */
private static int $min = 1;
/** @var integer Maximum number of published slides */
private static int $max = 3;
/**
* The attributes that are mass assignable.
*
@ -68,6 +75,14 @@ class Slide extends Model
protected static function booted(): void
{
static::creating(function (self $model) {
if (
$model->type === SliderTypeEnum::team &&
self::query()->where('type', SliderTypeEnum::team)->count() + 1 > $model::getMaxPublished()
) {
throw ValidationException::withMessages([
'slide' => 'Il ne peut y avoir que 3 slides en même temps.'
]);
}
self::manageFields($model);
});
static::updating(function (self $model) {
@ -76,6 +91,16 @@ class Slide extends Model
static::updated(function (self $model) {
FileStorageHelper::removeOldFile($model, 'picture');
});
static::deleting(function (self $model) {
if (
$model->type === SliderTypeEnum::team &&
self::query()->where('type', SliderTypeEnum::team)->count() - 1 < $model::getMinPublished()
) {
throw ValidationException::withMessages([
'slide' => 'Il doit y avoir au mininmum 1 slide d\'active.'
]);
}
});
static::deleted(function (self $model) {
FileStorageHelper::removeFile($model, 'picture');
});
@ -93,4 +118,24 @@ class Slide extends Model
{
$model->picture = FileStorageHelper::storeFile($model, $model->picture, true);
}
/**
* Getter for the $min property.
*
* @return integer
*/
public static function getMinPublished(): int
{
return self::$min;
}
/**
* Getter for the $max property.
*
* @return integer
*/
public static function getMaxPublished(): int
{
return self::$max;
}
}

View file

@ -29,7 +29,6 @@
"require-dev": {
"fakerphp/faker": "^1.9.1",
"kitloong/laravel-migrations-generator": "^6.8",
"krlove/eloquent-model-generator": "dev-l10-compatibility",
"larastan/larastan": "^2.9",
"laravel-lang/common": "^4.0",
"laravel/pint": "^1.0",

90
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": "c06d9627a9b3a3aedb38f936fcf50b9a",
"content-hash": "7e2effeafd1cbae360198092b1705a40",
"packages": [
{
"name": "axlon/laravel-postal-code-validation",
@ -8759,90 +8759,6 @@
],
"time": "2024-02-06T14:42:33+00:00"
},
{
"name": "krlove/code-generator",
"version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/krlove/code-generator.git",
"reference": "1ac521f5ef79a376282e3315ba6a13a83c63fb9a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/krlove/code-generator/zipball/1ac521f5ef79a376282e3315ba6a13a83c63fb9a",
"reference": "1ac521f5ef79a376282e3315ba6a13a83c63fb9a",
"shasum": ""
},
"type": "library",
"autoload": {
"psr-4": {
"Krlove\\CodeGenerator\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Code Generator",
"support": {
"issues": "https://github.com/krlove/code-generator/issues",
"source": "https://github.com/krlove/code-generator/tree/1.0.1"
},
"time": "2022-02-20T12:24:22+00:00"
},
{
"name": "krlove/eloquent-model-generator",
"version": "dev-l10-compatibility",
"source": {
"type": "git",
"url": "https://github.com/laravel-shift/eloquent-model-generator.git",
"reference": "d8f0280de04d85cf2991e5a5dd317f5dea239d95"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel-shift/eloquent-model-generator/zipball/d8f0280de04d85cf2991e5a5dd317f5dea239d95",
"reference": "d8f0280de04d85cf2991e5a5dd317f5dea239d95",
"shasum": ""
},
"require": {
"doctrine/dbal": "^3.5",
"illuminate/config": "^10.0",
"illuminate/console": "^10.0",
"illuminate/database": "^10.0",
"illuminate/support": "^10.0",
"krlove/code-generator": "^1.0",
"php": "^8.1"
},
"require-dev": {
"phpunit/phpunit": "^9.5.10"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Krlove\\EloquentModelGenerator\\Provider\\GeneratorServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Krlove\\EloquentModelGenerator\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Krlove\\Tests\\Integration\\": "tests/integration"
}
},
"license": [
"MIT"
],
"description": "Eloquent Model Generator",
"support": {
"source": "https://github.com/laravel-shift/eloquent-model-generator/tree/l10-compatibility"
},
"time": "2023-01-30T20:15:06+00:00"
},
{
"name": "larastan/larastan",
"version": "v2.9.12",
@ -12374,9 +12290,7 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {
"krlove/eloquent-model-generator": 20
},
"stability-flags": [],
"prefer-stable": true,
"prefer-lowest": false,
"platform": {

View file

@ -0,0 +1,72 @@
<?php
use App\Enums\Slides\SliderTypeEnum;
use App\Models\Slide;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::table('slides', function (Blueprint $table) {
$table->string('content', 500)->nullable()->comment('Slide content.')->change();
$table->string('picture')->nullable()->comment('Slide picture path.')->change();
$table->string('picture_alt')->nullable()->comment('Slide picture alt attribute.')->change();
$table->string('picture_title')->nullable()->comment('Slide picture title attribute.')->change();
$table->boolean('button_status')->default(0)->comment('Slide button status.')->change();
$table->boolean('button_new_tab')->default(0)->comment('Open the slide button url in a new tab.')->change();
});
if (Slide::query()->where('type', SliderTypeEnum::team)->count() === 0) {
// phpcs:disable Generic.Files.LineLength.TooLong
$datas = [
[
'title' => 'Culture d’entreprise inclusive et collaborative :',
'content' => 'Notre entreprise valorise la diversité des perspectives et favorise un environnement où chacun se sent écouté et respecté. Nous croyons en la force de la collaboration et encourageons l’innovation à tous les niveaux.',
],
[
'title' => 'Opportunités de développement professionnel :',
'content' => 'En nous rejoignant, vous aurez accès à des programmes de formation et de développement pour renforcer vos compétences et progresser dans votre carrière. Nous encourageons la croissance personnelle et professionnelle de nos employés.',
],
[
'title' => 'Implication dans des projets innovants :',
'content' => 'Nous travaillons sur des projets stimulants et innovants qui offre l’opportunité de faire une réelle différence pour les consommateurs. Rejoindre notre équipe vous permettra de contribuer à des initiatives passionnantes et impactantes.<br><br>Et en plus, on est sympas.',
]
];
//phpcs:enable
foreach ($datas as $data) {
$slide = new Slide();
$slide->type = SliderTypeEnum::team;
$slide->title = $data['title'];
$slide->content = $data['content'];
$slide->button_status = 0;
$slide->saveOrFail();
}
} //end if
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Slide::query()->where('type', SliderTypeEnum::team)->delete();
Schema::table('slides', function (Blueprint $table) {
$table->string('content', 255)->nullable()->comment('Slide content.')->change();
$table->string('picture')->comment('Slide picture path.')->change();
$table->string('picture_alt')->comment('Slide picture alt attribute.')->change();
$table->string('picture_title')->comment('Slide picture title attribute.')->change();
});
}
};

View file

@ -0,0 +1,61 @@
<?php
namespace Database\Seeders\Mandatory;
use App\Enums\Slides\SliderTypeEnum;
use App\Models\Slide;
use Illuminate\Database\Seeder;
/**
* @package Database\Seeders\Development
*/
class TeamSlideSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
Slide::query()->updateOrCreate(
[
'type' => SliderTypeEnum::team,
'title' => 'Culture d’entreprise inclusive et collaborative :',
],
[
'title' => 'Culture d’entreprise inclusive et collaborative :',
'content' => 'Notre entreprise valorise la diversité des perspectives et favorise un environnement où
chacun se sent écouté et respecté. Nous croyons en la force de la collaboration et encourageons
l’innovation à tous les niveaux.',
]
);
Slide::query()->updateOrCreate(
[
'type' => SliderTypeEnum::team,
'title' => 'Opportunités de développement professionnel :',
],
[
'title' => 'Culture d’entreprise inclusive et collaborative :',
'content' => 'En nous rejoignant, vous aurez accès à des programmes de formation et de développement
pour renforcer vos compétences et progresser dans votre carrière. Nous encourageons la croissance
personnelle et professionnelle de nos employés.',
]
);
Slide::query()->updateOrCreate(
[
'type' => SliderTypeEnum::team,
'title' => 'Implication dans des projets innovants :',
],
[
'title' => 'Culture d’entreprise inclusive et collaborative :',
'content' => 'Nous travaillons sur des projets stimulants et innovants qui offre l’opportunité de faire
une réelle différence pour les consommateurs. Rejoindre notre équipe vous permettra de contribuer à des
initiatives passionnantes et impactantes.
<br>
<br>
Et en plus, on est sympas.',
]
);
}
}

View file

@ -1,342 +1,390 @@
<template>
<div class="ck-editor-component">
<div
ref="ckEditorComponent"
class="ck-editor-component"
>
<div ref="wordCounter" />
<div ref="ckeditor" />
<div ref="ckEditor" />
<input
:id="internalId"
:id="id"
class="form-control"
type="hidden"
:name="intName"
:value="internalText"
:required="internalRequired"
:name="name"
:value="text"
:required="required"
>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
<script lang="ts" setup>
import { computed, defineOptions, onMounted, ref, useAttrs, type PropType } from "vue";
import "../../../js/vendor/ckeditor";
import parseLaravelErrorMessages from "../../modules/parseLaravelErrorMessages";
import trans from "../../modules/trans";
// @ts-ignore
import { CkUploadAdapterPlugin } from "../../../js/CKUploadAdapter";
import "./../../../js/vendor/ckeditor";
import trans from "./../../modules/trans";
export default defineComponent({
name: "CkEditorComponent",
emits: ["input"],
mixins: [trans],
props: {
id: {
type: String,
default: String(Math.pow(10, 16) / Math.random()),
},
required: {
type: Boolean,
default: false,
},
name: {
type: String,
default: "",
},
dbtext: {
type: String,
default: "",
},
defineOptions({
name: "CkEditorComponent"
});
// * EMITS
const emits = defineEmits<{
input: [string: string],
}>();
// * ATTRIBUTES
const attrs = useAttrs();
// * REFS
const ckEditorComponent = ref<HTMLDivElement|null>(null);
const ckEditor = ref<HTMLDivElement|null>(null);
const wordCounter = ref<HTMLDivElement|null>(null);
// * PROPS
const props = defineProps({
id: {
type: String,
default: String(Math.pow(10, 16) / Math.random())
},
mounted() {
const json = String(this.$attrs.json ?? "{}"),
data = JSON.parse(json);
this.intName = data.name;
this.internalText = data.text ? String(data.text) : "";
this.internalId = data.id ? data.id : "";
this.internalRequired = Boolean(data.required);
this.makeSuperBuild();
name: {
type: String,
default: ""
},
data(): {
intName: string;
internalText: string;
internalId: string;
internalRequired: boolean;
// @ts-ignore
editor: ClassicEditor | null;
words: number;
chars: number;
} {
return {
intName: "",
internalText: "",
internalId: "",
internalRequired: false,
editor: null,
words: 0,
chars: 0,
};
dbtext: {
type: String,
default: ""
},
watch: {
id() {
this.internalId = this.id;
},
name() {
this.intName = this.name;
},
required() {
this.internalRequired = this.required;
},
dbtext() {
if (this.internalText != this.dbtext) {
this.internalText = this.dbtext ? this.dbtext : "";
if (this.editor) {
const text = this.internalText;
Object.freeze(text);
this.editor.setData(text);
}
}
},
placeholder: {
type: String,
default: ""
},
methods: {
makeSuperBuild() {
const self = this,
ckeditor = this.$refs.ckeditor as HTMLElement,
wordCounter = this.$refs.wordCounter as HTMLElement,
// * Top Menu
verticalTopMenuOffset = document.querySelector(
"body > #top-navbar"
) as HTMLElement | null;
if (!verticalTopMenuOffset) {
console.warn("Could not find bo #top-navbar");
}
const ui = verticalTopMenuOffset ? {
viewportOffset: {
top: verticalTopMenuOffset.getBoundingClientRect().height,
right: 0,
bottom: 0,
left: 0,
},
} : undefined;
// @ts-ignore
ClassicEditor.create(ckeditor, {
// https://github.com/ckeditor/ckeditor5-react/issues/267
extraPlugins: [ CkUploadAdapterPlugin ],
wordCount: {
container: wordCounter,
// @ts-ignore
onUpdate: (stats) => {
this.chars = stats.characters;
this.words = stats.words;
},
},
ui: ui,
link: {
// Automatically add target="_blank" and rel="noopener noreferrer" to all external links.
addTargetToExternalLinks: true,
// Let the users control the "download" attribute of each link.
decorators: {
openInNewTab: {
mode: "manual",
label: "Ouvrir dans un nouvel onglet",
attributes: {
target: "_blank",
}
}
}
},
typing: {
transformations: {
extra: [
{ from: ":)", to: "😊" },
{ from: ":-P", to: "😛" },
{ from: "8-)", to: "😎" },
{ from: ";-)", to: "😉" },
{ from: ":-D", to: "😄" },
{ from: ":twisted:", to: "😈" },
{ from: ":mrgreen:", to: "😁" },
{ from: ":lol:", to: "😆" },
{ from: ":roll:", to: "🙄" },
{ from: ":-?", to: "😕" },
{ from: ":cry:", to: "😢" },
{ from: ":-o", to: "😮" },
{ from: ":evil:", to: "😡" },
{ from: ":-|", to: "😐" },
{ from: ":oops:", to: "😳" },
{ from: ":-x", to: "😠" },
{ from: "8-O", to: "😲" },
{ from: ":-(", to: "😧" },
{ from: ":arrow:", to: "➡" },
{ from: ":idea:", to: "💡" },
],
},
},
mediaEmbed: {
previewsInData: false,
removeProviders: [
"dailymotion",
"spotify",
"youtube",
"vimeo",
"instagram",
"twitter",
"googleMaps",
"flickr",
"facebook",
],
// // see https://github.com/ckeditor/ckeditor5/blob/master/packages/ckeditor5-media-embed/src/mediaembedediting.js
// providers: [
// {
// // display local videos in oembed tags
// name: "localVideo",
// url: /^(\/(?:content|storage|uploads)\/.*)/,
// html: (match: string[]) => {
// const path = match[1];
// return (
// "<video controls allowfullscreen style=\"max-width: 90%; margin: 0 auto; display: block;\">" +
// `<source src="${path}" type="video/mp4">` +
// `<p>Votre navigateur ne prend pas en charge les vidéos HTML5. Voici <a href="${path}">un lien pour télécharger la vidéo</a>.</p>` +
// "</video>"
// );
// },
// },
// ],
},
toolbar: {
items: [
"heading",
"|",
"findAndReplace",
"|",
"bold",
"italic",
"underline",
"strikethrough",
"link",
"bulletedList",
"numberedList",
// "code",
// "codeBlock",
"|",
"outdent",
"indent",
"alignment",
"|",
"undo",
"redo",
"selectAll",
"|",
"imageUpload",
// "laravelFileManager",
"blockQuote",
"|",
// "insertTable",
// "tableColumn",
// "tableRow",
// "mergeTableCells",
"|",
// "highlight",
// "removeHighlight",
// "fontColor",
// "fontBackgroundColor",
// "fontFamily",
"fontSize",
"horizontalLine",
// "specialCharacters",
"|",
"removeFormat",
"|",
"sourceEditing",
],
},
// table: {
// contentToolbar: [
// "tableColumn",
// "tableRow",
// "mergeTableCells",
// "tableProperties",
// "tableCellProperties",
// "toggleTableCaption",
// ],
// Configuration of the TableProperties plugin.
// tableProperties: {
// ...
// },
// Configuration of the TableCellProperties plugin.
// tableCellProperties: {
// ...
// },
// },
width: "auto",
language: "fr",
image: {
toolbar: [
"toggleImageCaption",
"imageTextAlternative",
"imageStyle:inline",
"imageStyle:alignLeft",
"imageStyle:alignRight",
"imageStyle:alignCenter",
"imageStyle:side",
"imageResize",
"linkImage",
],
},
})
// @ts-ignore
.then((editor: ClassicEditor) => {
let timer = null as number|null;
const updateContent = () => {
const data = editor.getData();
self.internalText = data;
self.$emit("input", data);
timer = null;
};
// Display all toolbar elements
// console.log(Array.from(editor.ui.componentFactory.names()));
self.editor = Object.freeze(editor);
editor.setData(this.internalText);
editor.model.document.on("change:data", () => {
if(timer) {
clearTimeout(timer);
}
// * Keep track of internal editor
timer = setTimeout(updateContent, 1000) as never as number;
});
})
// @ts-ignore
.catch((error) => {
console.error("Oops, something went wrong!");
console.error(
"Please, report the following error on https://github.com/ckeditor/ckeditor5/issues with the build id and the error stack trace:"
);
console.warn("Build id: x0tjc6omqq4d-iydt761tuj51");
console.error(error);
});
},
required: {
type: Boolean,
default: false
},
errors: {
type: Object as PropType<LaravelValidationErrors>,
default: () => { return []; }
},
editorConfig: {
type: String,
default: "basic"
},
});
// * DATA
const id = ref<string>(props.id);
const name = ref<string>(props.name);
const text = ref<string>(props.dbtext);
const placeholder = ref<string>(props.placeholder);
const required = ref<boolean>(props.required);
const allErrors = ref<LaravelValidationErrors>(props.errors);
const editorConfig = ref<string>(props.editorConfig);
// * COMPUTED
const isUsedWithProps = computed<boolean>(() => attrs.json === undefined);
// * LIFECYCLE
onMounted(() => {
const json = String(attrs.json ?? "{}"),
data = JSON.parse(json);
if (!isUsedWithProps.value) {
(data.id) ? id.value = String(data.id) : "";
name.value = String(data.name);
text.value = String(data.text);
placeholder.value = String(data.placeholder ?? trans.methods.__("Votre texte ici."));
allErrors.value = data.errors;
(data.required) ? required.value = Boolean(data.required) : "";
editorConfig.value = String(data.editorConfig);
}
setTimeout(() => {
customNodes();
}, 100);
// Top Menu
const verticalTopMenuOffset = document.querySelector(
"body > #top-navbar"
) as HTMLElement | null;
if (!verticalTopMenuOffset) {
console.warn("Could not find bo #top-navbar");
}
// * CkEditor Global Parts.
// WordCounter
const ckEditorGlobals = {
wordCount: {
container: wordCounter.value,
// @ts-ignore
onUpdate: (stats) => {
/** Hack to stylize words/caracters counter */
/** Add word counter */
const wordCounterNode = wordCounter.value?.querySelector(".ck-word-count__words");
const spanWords = document.createElement("span");
spanWords.classList.add("fw-bold");
spanWords.innerHTML = stats.words;
wordCounterNode!.textContent = "Mots : ";
wordCounterNode!.appendChild(spanWords);
/** Add character counter */
const characterCounterNode = wordCounter.value?.querySelector(".ck-word-count__characters");
const spanCharacters = document.createElement("span");
spanCharacters.classList.add("fw-bold");
spanCharacters.innerHTML = stats.characters;
characterCounterNode!.textContent = "Caractères : ";
characterCounterNode!.appendChild(spanCharacters);
},
},
ui: verticalTopMenuOffset ? {
viewportOffset: {
top: verticalTopMenuOffset.getBoundingClientRect().height,
right: 0,
bottom: 0,
left: 0,
},
} : undefined,
typing: {
transformations: {
extra: [
{ from: ":)", to: "😊" },
{ from: ":-P", to: "😛" },
{ from: "8-)", to: "😎" },
{ from: ";-)", to: "😉" },
{ from: ":-D", to: "😄" },
{ from: ":twisted:", to: "😈" },
{ from: ":mrgreen:", to: "😁" },
{ from: ":lol:", to: "😆" },
{ from: ":roll:", to: "🙄" },
{ from: ":-?", to: "😕" },
{ from: ":cry:", to: "😢" },
{ from: ":-o", to: "😮" },
{ from: ":evil:", to: "😡" },
{ from: ":-|", to: "😐" },
{ from: ":oops:", to: "😳" },
{ from: ":-x", to: "😠" },
{ from: "8-O", to: "😲" },
{ from: ":-(", to: "😧" },
{ from: ":arrow:", to: "➡" },
{ from: ":idea:", to: "💡" },
],
},
},
link: {
// Automatically add target="_blank" and rel="noopener noreferrer" to all external links.
addTargetToExternalLinks: true,
// Let the users control the "download" attribute of each link.
decorators: {
openInNewTab: {
mode: "manual",
label: "Ouvrir dans un nouvel onglet",
attributes: {
target: "_blank",
}
}
}
}
};
// * Differents CkEditor configs.
const minimalConfig = {
wordCount: ckEditorGlobals.wordCount,
ui: ckEditorGlobals.ui,
placeholder: placeholder.value,
link: ckEditorGlobals.link,
typing: ckEditorGlobals.typing,
mediaEmbed: {
previewsInData: false,
removeProviders: [
"dailymotion",
"spotify",
"vimeo",
"youtube",
"instagram",
"twitter",
"googleMaps",
"flickr",
"facebook",
],
},
toolbar: {
items: [
"findAndReplace",
"|",
"bold",
"italic",
"underline",
"strikethrough",
"|",
"undo",
"redo",
"selectAll",
"|",
"removeFormat",
],
},
image: {
toolbar: []
},
width: "auto",
language: "fr",
};
const basicConfig = {
placeholder: placeholder.value,
wordCount: ckEditorGlobals.wordCount,
ui: ckEditorGlobals.ui,
link: ckEditorGlobals.link,
typing: ckEditorGlobals.typing,
mediaEmbed: {
previewsInData: false,
removeProviders: [
"dailymotion",
"spotify",
"youtube",
"vimeo",
"instagram",
"twitter",
"googleMaps",
"flickr",
"facebook",
],
},
toolbar: {
items: [
"heading",
"|",
"findAndReplace",
"|",
"bold",
"italic",
"underline",
"strikethrough",
"link",
"bulletedList",
"numberedList",
"|",
"outdent",
"indent",
"alignment",
"|",
"undo",
"redo",
"selectAll",
"|",
"imageUpload",
"blockQuote",
"|",
"fontSize",
"horizontalLine",
"|",
"removeFormat",
"|",
"sourceEditing",
],
},
width: "auto",
language: "fr",
image: {
toolbar: [
"toggleImageCaption",
"imageTextAlternative",
"imageStyle:inline",
"imageStyle:alignLeft",
"imageStyle:alignRight",
"imageStyle:alignCenter",
"imageStyle:side",
"imageResize",
"linkImage",
],
},
};
let config = basicConfig;
switch (editorConfig.value) {
case "minimal":
config = minimalConfig;
break;
case "basic":
config = basicConfig;
break;
default:
console.warn(editorConfig.value + " is not a correct configuration. The basic config is loaded.");
}
// Generate Editor.
makeEditorBuild(config);
});
// * METHODS
// @ts-ignore
function makeEditorBuild(config: Config<EditorConfig>): void {
// @ts-ignore
ClassicEditor.create(ckEditor.value, config)
// @ts-ignore
.then((editor: ClassicEditor) => {
let timer = null as number|null;
const updateContent = () => {
const data = editor.getData();
text.value = data;
emits("input", data);
timer = null;
};
// Display all toolbar elements
ckEditor.value = Object.freeze(editor);
editor.setData(text.value);
editor.model.document.on("change:data", () => {
if(timer) {
clearTimeout(timer);
}
// * Keep track of internal editor
timer = setTimeout(updateContent, 300) as never as number;
});
})
// @ts-ignore
.catch((error) => {
console.error("Oops, something went wrong!");
console.error(
"Please, report the following error on https://github.com/ckeditor/ckeditor5/issues with the build id and the error stack trace:"
);
console.warn("Build id: x0tjc6omqq4d-iydt761tuj51");
console.error(error);
});
}
/** Customize input. */
function customNodes(): void {
if (getErrorMessage() !== null) {
const fakeInput = ckEditorComponent.value?.querySelector(".ck.ck-editor__main") as HTMLDivElement|null;
fakeInput!.classList.add("ck-invalid");
}
}
/** Return error message or null. */
function getErrorMessage(): string|null {
return parseLaravelErrorMessages.methods.parseValidationInput(
`${name.value}`,
allErrors.value
);
}
</script>
<style lang="scss">
<style lang="scss" scopped>
@import url("./../../../js/vendor/ckeditor/build/styles.css");
.ck-editor-component{
// 2. Include functions first (so you can manipulate colors, SVGs, calc, etc)
@import "bootstrap/scss/functions";
@import "bootstrap/scss/functions";
@import "bootstrap/scss/variables";
// 3. Include remainder of required Bootstrap stylesheets
@import "bootstrap/scss/variables";
.ck-editor-component{
.ck-rounded-corners {
border-radius: 0 0 0.375rem 0.375rem !important;
}
.ck-word-count {
background-color: #fafafa;
border: 1px solid #c4c4c4;
background-color: rgb(var(--bs-secondary-bg-rgb));
border: var(--bs-border-width) solid var(--bs-border-color);
border-bottom: 0px;
border-top-left-radius: 0.375rem;
border-top-right-radius: 0.375rem;
color: initial;
color: var(--bs-body-color);
display: flex;
flex-direction: row;
justify-content: space-around !important;
@ -345,13 +393,26 @@ export default defineComponent({
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.ck-focused {
border-color: #86b7fe !important;
box-shadow: 0 0 0 0.25rem rgb(13 110 253 / 25%) !important;
border-color: var(--bs-primary-border-subtle) !important;
box-shadow: 0 0 0 .25rem rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity)) !important;
outline: 0 !important;
}
.ck-editor__main{
color: $body-color;
}
.ck.ck-editor__main.ck-invalid .ck-blurred, .ck.ck-editor__main.ck-invalid .ck-focused {
border-color: var(--bs-form-invalid-border-color) !important;
}
.ck.ck-editor__main.ck-invalid .ck-focused {
box-shadow: 0 0 0 .25rem rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity)) !important;
}
}
.ck.ck-reset.ck-rounded-corners .ck-toolbar {
border-top-left-radius: 0 !important;
border-top-right-radius: 0 !important;
border: var(--bs-border-width) solid var(--bs-border-color);
}
.ck.ck-powered-by-balloon {
display: none !important;
}
</style>

View file

@ -70,6 +70,7 @@ declare global {
type LaravelPaginator = Paginator;
type LaravelPaginatorLink = PaginatorLink;
type ValueOf<T> = T[keyof T];
type LaravelValidationErrors = Record<string, string[]>;
type Enum = EnumInterface;

View file

@ -1,21 +1,21 @@
@extends('bo.layout', ['brParam' => $slideModel])
@section('title', __('Création d\'un :model', ['model' => trans_choice('models.classes.site', 1)]))
@section('description', __('Création d\'un tout nouveau :model', ['model' => trans_choice('models.classes.site', 1)]))
@section('title', __('Création d\'un :model', ['model' => trans_choice('models.classes.slide', 1)]))
@section('description', __('Création d\'un tout nouveau :model', ['model' => trans_choice('models.classes.slide', 1)]))
@section('breadcrumb', request()->route()->getName())
@section('content')
<div class="d-flex justify-content-between flex-md-nowrap align-items-center flex-wrap pb-3">
<div class="d-flex align-items-start flex-row">
<a class="btn btn-primary text-decoration-none m-0" data-bs-toggle="tooltip" href="{{ route('bo.slides.index') }}"
title="{{ __('crud.helpers.list_all', ['model' => trans_choice('models.classes.site', 1)]) }}">
title="{{ __('crud.helpers.list_all', ['model' => trans_choice('models.classes.slide', 1)]) }}">
<i class="fa-solid fa-arrow-left"></i>
</a>
@include('modules.breadcrumbs.breadcrumb-body', ['brParam' => $slideModel])
</div>
<div class="mb-md-0 mb-2">
<button class="btn btn-primary" id="formSubmitClone" data-bs-toggle="tooltip" type="submit"
title="{{ __('crud.helpers.save_model', ['model' => trans_choice('models.classes.site', 1)]) }}">
title="{{ __('crud.helpers.save_model', ['model' => trans_choice('models.classes.slide', 1)]) }}">
{{ Str::ucfirst(__('crud.actions.save')) }}
</button>
</div>
@ -27,7 +27,7 @@
<div class="row mt-3">
<div class="col text-center">
<button class="btn btn-primary" id="formSubmit" data-bs-toggle="tooltip" type="submit"
title="{{ __('crud.helpers.save_model', ['model' => trans_choice('models.classes.site', 1)]) }}">
title="{{ __('crud.helpers.save_model', ['model' => trans_choice('models.classes.slide', 1)]) }}">
{{ Str::ucfirst(__('crud.actions.save')) }}
</button>
</div>

View file

@ -29,7 +29,7 @@
'label' => __(':field :inter:model', [
'field' => Str::ucfirst(__('validation.attributes.title')),
'inter' => __('validation.custom.of_the.female'),
'model' => trans_choice('models.classes.site', 1),
'model' => trans_choice('models.classes.slide', 1),
]),
'hasWordCounter' => true,
'name' => 'title',
@ -39,7 +39,7 @@
'attribute' => __(':field :inter:model', [
'field' => __('validation.attributes.title'),
'inter' => __('validation.custom.of_the.female'),
'model' => trans_choice('models.classes.site', 1),
'model' => trans_choice('models.classes.slide', 1),
]),
'min' => 3,
'max' => 100,
@ -51,30 +51,30 @@
</fieldset>
</div>
{{-- CONTENT --}}
<div class="col-12 col-md-6 mb-3" id="divContent">
<div class="col-12 mb-3" id="divContent">
<fieldset class="h-100 border p-3">
<legend>{{ Str::ucfirst(__('validation.attributes.content')) }}</legend>
<div class="row">
<div class="col-12">
@include('bo.partials.inputs.varchar-multiline', [
@include('bo.partials.inputs.text-html', [
'id' => 'slideContent',
'label' => __(':field :inter:model', [
'field' => Str::ucfirst(__('validation.attributes.content')),
'inter' => __('validation.custom.of_the.female'),
'model' => trans_choice('models.classes.site', 1),
'model' => trans_choice('models.classes.slide', 1),
]),
'hasWordCounter' => true,
'name' => 'content',
'required' => false,
'model' => $slideModel ?? null,
'required' => false,
'editorConfig' => \App\Enums\CkEditorConfigName::minimal->label(),
'helper' => __('validation.between.string', [
'attribute' => __(':field :inter:model', [
'field' => __('validation.attributes.content'),
'inter' => __('validation.custom.of_the.female'),
'model' => trans_choice('models.classes.site', 1),
'model' => trans_choice('models.classes.slide', 1),
]),
'min' => 3,
'max' => 255,
'max' => 275,
]),
'placeholder' => __('Contenu textuel de la slide'),
])
@ -93,7 +93,7 @@
'label' => __(':field :inter:model', [
'field' => Str::ucfirst(__('validation.custom.video_id')),
'inter' => __('validation.custom.of_the.female'),
'model' => trans_choice('models.classes.site', 1),
'model' => trans_choice('models.classes.slide', 1),
]),
'hasWordCounter' => true,
'name' => 'video_id',
@ -104,7 +104,7 @@
'attribute' => __(':field :inter:model', [
'field' => __('validation.custom.video_id'),
'inter' => __('validation.custom.of_the.female'),
'model' => trans_choice('models.classes.site', 1),
'model' => trans_choice('models.classes.slide', 1),
]),
'min' => 3,
'max' => 11,
@ -116,7 +116,7 @@
</fieldset>
</div>
{{-- PICTURE --}}
<div class="col-12 pb-3">
<div class="col-12 pb-3" id="divPicture">
<fieldset class="h-100 border p-3">
<legend>{{ Str::ucfirst(__('validation.attributes.image')) }}</legend>
<div class="row mb-3">
@ -129,11 +129,11 @@
'inter1' => __('validation.custom.of_the.male'),
'field2' => __('validation.attributes.image'),
'inter2' => __('validation.custom.of_the.male'),
'model' => trans_choice('models.classes.site', 1),
'model' => trans_choice('models.classes.slide', 1),
]),
'hasWordCounter' => true,
'name' => 'picture_alt',
'required' => true,
'required' => false,
'model' => $slideModel ?? null,
'helper' => __('crud.minicrud.picture_alt_helper'),
'placeholder' => __(':field1 :inter1:field2 :inter2:model', [
@ -141,7 +141,7 @@
'inter1' => __('validation.custom.of_the.male'),
'field2' => __('validation.attributes.image'),
'inter2' => __('validation.custom.of_the.male'),
'model' => trans_choice('models.classes.site', 1),
'model' => trans_choice('models.classes.slide', 1),
]),
])
</div>
@ -154,11 +154,11 @@
'inter1' => __('validation.custom.of_the.male'),
'field2' => __('validation.attributes.image'),
'inter2' => __('validation.custom.of_the.male'),
'model' => trans_choice('models.classes.site', 1),
'model' => trans_choice('models.classes.slide', 1),
]),
'hasWordCounter' => true,
'name' => 'picture_title',
'required' => true,
'required' => false,
'model' => $slideModel ?? null,
'helper' => __('crud.minicrud.picture_title_helper'),
'placeholder' => __(':field1 :inter1:field2 :inter2:model', [
@ -166,7 +166,7 @@
'inter1' => __('validation.custom.of_the.male'),
'field2' => __('validation.attributes.image'),
'inter2' => __('validation.custom.of_the.male'),
'model' => trans_choice('models.classes.site', 1),
'model' => trans_choice('models.classes.slide', 1),
]),
])
</div>
@ -290,6 +290,7 @@
const fieldsetVideo = document.getElementById('divVideo');
const fieldsetTitle = document.getElementById('divTitle');
const fieldsetContent = document.getElementById('divContent');
const fieldsetPicture = document.getElementById('divPicture');
const fieldsetButton = document.getElementById('divButton');
checkSelectValue()
sliderSelectType.addEventListener('input', checkSelectValue);
@ -299,11 +300,19 @@
fieldsetVideo.hidden = false;
fieldsetTitle.required = false;
fieldsetContent.hidden = false;
fieldsetPicture.hidden = false;
fieldsetButton.hidden = false;
} else if (sliderSelectType.value === "{{ \App\Enums\Slides\SliderTypeEnum::team->value() }}") {
fieldsetVideo.hidden = true;
fieldsetTitle.required = true;
fieldsetContent.hidden = false;
fieldsetPicture.hidden = true;
fieldsetButton.hidden = true;
} else {
fieldsetVideo.hidden = true;
fieldsetTitle.required = true;
fieldsetContent.hidden = true;
fieldsetPicture.hidden = false;
fieldsetButton.hidden = true;
}
}

View file

@ -19,6 +19,9 @@
$data = [
'name' => $name,
'text' => old($name, $model->{$name} ?? ($value ?? '')),
'placeholder' => $placeholder ?? __('Un text riche'),
'errors' => $errors->getBag('default')->getMessages() ?? [],
'editorConfig' => $editorConfig ?? \App\Enums\CkEditorConfigName::basic->label(),
];
@endphp
<div class="ck-editor" data-json='@json($data)'></div>

View file

@ -141,26 +141,16 @@
<div class="col-12 rounded-3 mt-2 bg-white p-4">
<div class="swiper swiper-teams p-0" id="swiper-teams">
<div class="swiper-wrapper">
@php
$homeTeamData = [
'Culture d’entreprise inclusive et collaborative&nbsp;:' =>
'Notre entreprise valorise la diversité des perspectives et favorise un environnement où chacun se sent écouté et respecté. Nous croyons en la force de la collaboration et encourageons l’innovation à tous les niveaux.',
'Opportunités de développement professionnel&nbsp;:' =>
'En nous rejoignant, vous aurez accès à des programmes de formation et de développement pour renforcer vos compétences et progresser dans votre carrière. Nous encourageons la croissance personnelle et professionnelle de nos employés.',
'Implication dans des projets innovants&nbsp;:' =>
'Nous travaillons sur des projets stimulants et innovants qui offre l’opportunité de faire une réelle différence pour les consommateurs. Rejoindre notre équipe vous permettra de contribuer à des initiatives passionnantes et impactantes.<br><br>Et en plus, on est sympas.',
];
@endphp
@foreach ($homeTeamData as $title => $text)
@foreach ($slideTeams as $slide)
<div class="swiper-slide">
<p class="text-primary fw-bolder fs-3">{!! $title !!}</p>
<p class="text-primary fs-4">{!! $text !!}</p>
<p class="text-primary fw-bolder fs-3">{{ $slide->title }}</p>
<div class="text-primary fs-4 ck-content">{!! clean($slide->content) !!}</div>
</div>
@endforeach
</div>
<div class="d-flex justify-content-center align-items-center">
<div
class="swiper-pagination swiper-teams-pagination d-flex justify-content-between align-items-center w-100">
class="swiper-pagination swiper-teams-pagination d-flex justify-content-evenly align-items-center w-100">
</div>
<button class="swiper-pagination-playpause btn btn-primary btn-sm m-1 ms-3">
<i class="fa-solid fa-pause"></i>

View file

@ -41,10 +41,12 @@ Route::prefix('bo')
// * USERS.
Route::resource('users', UserController::class)->except('show');
Route::patch('/users/{user}/change-published', [
UserController::class, 'changePublished'
UserController::class,
'changePublished'
])->name('users.change-published');
Route::get('/users/{user}/duplicate', [
UserController::class, 'duplicate'
UserController::class,
'duplicate'
])->name('users.duplicate');
// * STATIC PAGES.
@ -63,13 +65,16 @@ Route::prefix('bo')
->withoutMiddleware('htmlsanitize')
->middleware('htmlsanitize:content,introduction');
Route::patch('/news/{news}/change-published', [
NewsController::class, 'changePublished'
NewsController::class,
'changePublished'
])->name('news.change-published');
Route::patch('/news/{news}/change-order/{direction}', [
NewsController::class, 'changeOrder'
NewsController::class,
'changeOrder'
])->where('direction', 'up|down')->name('news.change-order');
Route::get('/news/{news}/duplicate', [
NewsController::class, 'duplicate'
NewsController::class,
'duplicate'
])->name('news.duplicate');
// * SITES.
@ -77,29 +82,36 @@ Route::prefix('bo')
->withoutMiddleware('htmlsanitize')
->middleware('htmlsanitize:director_speach,employee_speach,highlights,content');
Route::patch('/sites/{site}/change-order/{direction}', [
SiteController::class, 'changeOrder'
SiteController::class,
'changeOrder'
])->where('direction', 'up|down')->name('sites.change-order');
Route::get('/sites/{site}/duplicate', [
SiteController::class, 'duplicate'
SiteController::class,
'duplicate'
])->name('sites.duplicate');
// * LOGOS.
Route::resource('logos', LogoController::class)->except('show');
Route::get('/logos/{logo}/duplicate', [
LogoController::class, 'duplicate'
LogoController::class,
'duplicate'
])->name('logos.duplicate');
// * HISTORICAL MILESTONES.
Route::resource('historicalmilestones', HistoricalMilestoneController::class)->except('show')
->parameters(['historicalmilestones' => 'historical_milestone']);
Route::get('/historicalmilestones/{historical_milestone}/duplicate', [
HistoricalMilestoneController::class, 'duplicate'
HistoricalMilestoneController::class,
'duplicate'
])->name('historicalmilestones.duplicate');
// * SLIDES.
Route::resource('slides', SlideController::class)->except('show');
Route::resource('slides', SlideController::class)->except('show')
->withoutMiddleware('htmlsanitize')
->middleware('htmlsanitize:content');
Route::get('/slides/{slide}/duplicate', [
SlideController::class, 'duplicate'
SlideController::class,
'duplicate'
])->name('slides.duplicate');
// * STRIPS.
@ -107,10 +119,12 @@ Route::prefix('bo')
->withoutMiddleware('htmlsanitize')
->middleware('htmlsanitize:text');
Route::patch('/strips/{strip}/change-published', [
StripController::class, 'changePublished'
StripController::class,
'changePublished'
])->name('strips.change-published');
Route::get('/strips/{strip}/duplicate', [
StripController::class, 'duplicate'
StripController::class,
'duplicate'
])->name('strips.duplicate');
// * SECTIONS.
@ -128,7 +142,8 @@ Route::prefix('bo')
Route::resource('jobvideos', JobVideoController::class)->except('show')
->parameters(['jobvideos' => 'job_video']);
Route::get('/jobvideos/{job_video}/duplicate', [
JobVideoController::class, 'duplicate'
JobVideoController::class,
'duplicate'
])->name('jobvideos.duplicate');
// * TESTIMONIALS.
@ -136,7 +151,8 @@ Route::prefix('bo')
->withoutMiddleware('htmlsanitize')
->middleware('htmlsanitize:content');
Route::get('/testimonials/{testimonial}/duplicate', [
TestimonialController::class, 'duplicate'
TestimonialController::class,
'duplicate'
])->name('testimonials.duplicate');
Route::middleware('can:isConceptor')->group(function () {