Compare commits

...

No commits in common. "fixEslint/k" and "addBanner" have entirely different histories.

188 changed files with 28075 additions and 1807 deletions

View file

@ -56,6 +56,14 @@ MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
MAIL_LIST_BREEDER=
MAIL_LIST_COMMITMENTS=
MAIL_LIST_EXPORT=
MAIL_LIST_FINANCE=
MAIL_LIST_INFO=
MAIL_LIST_PRESS=
MAIL_LIST_RESTAURATION=
VITE_APP_NAME="${APP_NAME}"
# TELESCOPE
@ -80,3 +88,10 @@ MEILISEARCH_KEY=
UWIL_ADMIN_SECURE_CIDR_LIST=""
UWIL_ADMIN_SECURE_CIDR_PROJECT_ALLOWED_LIST=""
# MATOMO
MATOMO_SITE_ID=
# RECAPTCHA
GOOGLE_RECAPTCHA_PUBLIC=
GOOGLE_RECAPTCHA_PRIVATE=
GOOGLE_RECAPTCHA_VERSION=

3
.husky/commit-msg Normal file
View file

@ -0,0 +1,3 @@
#!/bin/sh
npx --no -- commitlint --config node_modules/@uwil/husky-config/commitlint.config.cjs --edit $1

View file

@ -29,14 +29,17 @@ COPY --from=vendor /app/vendor/ /app/vendor/
WORKDIR /app
RUN npm config set @ldc-web:registry https://lab.s-web.fr/api/v4/packages/npm/
RUN npm config set -- '//lab.s-web.fr/api/v4/packages/npm/:_authToken' "${GITLAB_PASSWORD}"
RUN npm config set @uwil:registry=https://lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/
RUN npm config set -- '//lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/:_authToken' "${GITLAB_PASSWORD}"
RUN npm ci && npm run prod
FROM registry.s-web.fr/ldc-web/projets/ldc-web-images-docker/uwil-production-8.4:170625-130159
FROM registry.s-web.fr/ldc-web/projets/ldc-web-images-docker/uwil-production-8.5:${PHP_8_5_TAG}
ARG USER=www-data
COPY --chown=${USER}:${USER} . /app/srcs
COPY --chown=${USER}:${USER} ./resources/favicon /app/srcs/public
COPY --chown=${USER}:${USER} ./resources/favicon/front/ /app/srcs/public/
COPY --chown=${USER}:${USER} ./resources/favicon/back/ /app/srcs/public/back/
COPY --from=vendor --chown=${USER}:${USER} /app/vendor/ /app/srcs/vendor/
COPY --from=frontend --chown=${USER}:${USER} /app/public/build/ /app/srcs/public/build/

View file

@ -13,12 +13,12 @@ class LocalDateTime implements CastsAttributes
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param string|integer|null $value
* @param mixed $value
* @param array $attributes
* @return \Carbon\Carbon|null
* @phpcs:disable
*/
public function get(Model $model, string $key, string|int|null $value, array $attributes): ?\Carbon\Carbon
public function get(Model $model, string $key, mixed $value, array $attributes): ?\Carbon\Carbon
{
// phpcs:enable
return $value
@ -31,12 +31,12 @@ class LocalDateTime implements CastsAttributes
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param string|integer|null $value
* @param mixed $value
* @param array $attributes
* @return \Carbon\Carbon|null
* @phpcs:disable
*/
public function set(Model $model, string $key, string|int|null $value, array $attributes): ?\Carbon\Carbon
public function set(Model $model, string $key, mixed $value, array $attributes): ?\Carbon\Carbon
{
// phpcs:enable
return $value

View file

@ -27,225 +27,20 @@ enum ContactFormType: int
* @phpstan-ignore-next-line
*/
private const LABELS = [
self::collaborator->name => 'enums.contactFormType.collaborator.label',
self::breeder->name => 'enums.contactFormType.breeder.label',
self::professional->name => 'enums.contactFormType.professional.label',
self::restauration->name => 'enums.contactFormType.restauration.label',
self::export->name => 'enums.contactFormType.export.label',
self::finance->name => 'enums.contactFormType.finance.label',
self::commitments->name => 'enums.contactFormType.commitments.label',
self::product->name => 'enums.contactFormType.product.label',
self::info->name => 'enums.contactFormType.info.label',
self::press->name => 'enums.contactFormType.press.label',
self::collaborator->name => 'enums.contactFormType.collaborator',
self::breeder->name => 'enums.contactFormType.breeder',
self::professional->name => 'enums.contactFormType.professional',
self::restauration->name => 'enums.contactFormType.restauration',
self::export->name => 'enums.contactFormType.export',
self::finance->name => 'enums.contactFormType.finance',
self::commitments->name => 'enums.contactFormType.commitments',
self::product->name => 'enums.contactFormType.product',
self::info->name => 'enums.contactFormType.info',
self::press->name => 'enums.contactFormType.press',
];
/**
* Optionnal SUBTYPES definition.
*
* @phpstan-ignore-next-line
*/
private const SUBTYPES = [
self::professional->name => [
self::restauration,
self::export,
],
self::collaborator->name => [],
self::breeder->name => [],
self::restauration->name => [],
self::export->name => [],
self::finance->name => [],
self::commitments->name => [],
self::product->name => [],
self::info->name => [],
self::press->name => [],
];
/**
* Get Class
*
* @return string|null
*/
public function isSubtype(): bool
{
foreach (self::SUBTYPES as $subtypes) {
if (in_array($this, $subtypes, true)) {
return true;
}
}
return false;
}
/**
* Optionnal TOPLEVELS definition.
*
* @phpstan-ignore-next-line
*/
private const TOPLEVELS = [
self::professional->name => true,
self::collaborator->name => true,
self::breeder->name => true,
self::restauration->name => false,
self::export->name => false,
self::finance->name => true,
self::commitments->name => true,
self::product->name => true,
self::info->name => true,
self::press->name => true,
];
/**
* Get Class
*
* @return string|null
*/
public function topLevel(): ?string
{
return !empty(self::TOPLEVELS[$this->name]) ?
self::TOPLEVELS[$this->name] : false;
}
/**
* Get the data-target
*
* @return string
*/
public function dataTarget(): string
{
return match ($this) {
self::professional => 'askPro',
};
}
/**
* Get the subtitle
*
* @return string
*/
public function subtitle(): string
{
return match (true) {
$this === self::restauration => __('enums.contactFormType.' . $this->name . '.subtitle'),
default => __('enums.contactFormType.' . $this->name . '.label'),
};
}
/**
* Get the slug for each type
*
* @param string|null $locale
* @return string|null
* @phpcs:disable Generic.Metrics.CyclomaticComplexity
*/
public function slug(?string $locale = null): ?string
{
// phpcs:enable
return match ($this) {
self::collaborator => Lang::get('enums.contactFormType.collaborator.slug', [], $locale),
self::breeder => Lang::get('enums.contactFormType.breeder.slug', [], $locale),
self::professional => null,
self::restauration => Lang::get('enums.contactFormType.restauration.slug', [], $locale),
self::export => Lang::get('enums.contactFormType.export.slug', [], $locale),
self::finance => Lang::get('enums.contactFormType.finance.slug', [], $locale),
self::commitments => Lang::get('enums.contactFormType.commitments.slug', [], $locale),
self::product => Lang::get('enums.contactFormType.product.slug', [], $locale),
self::info => Lang::get('enums.contactFormType.info.slug', [], $locale),
self::press => null,
};
}
/**
* Check if current lang slug match in specific lang slug
*
* @param string $slug
* @param string $locale
* @return self
*/
public static function fromSlug(string $slug, string $locale): ?self
{
foreach (self::cases() as $case) {
$caseSlug = $case->slug($locale);
if (
$caseSlug !== null &&
mb_strtolower($caseSlug) === mb_strtolower($slug)
) {
return $case;
}
}
return null;
}
/**
* Get the route
*
* @return string|null
*/
public function route(): ?string
{
$commonRoutes = [
self::collaborator,
self::breeder,
self::export,
self::finance,
self::commitments,
self::product,
self::info,
self::restauration
];
return match (true) {
in_array($this, $commonRoutes, true) => route('front.contact.show', ['slug' => $this->slug()]),
$this === self::professional => null,
$this === self::press =>
RouteHelper::routeNameChecker('front.dynamic-pages.espace-presse') . '#page-press-form',
default => null,
};
}
/**
* Get the route for the contact form type
*
* @return array
*/
public static function getLabelisedRoutes(): array
{
return collect(self::cases())->mapWithKeys(fn($enum) => [
$enum->name => (object) [
"label" => $enum->label(),
"route" => $enum->route(),
],
])->toArray();
}
/**
* Get the slug for each type
*
* @return array
*/
public function seo(): array
{
$commonRoutes = [
self::collaborator,
self::breeder,
self::export,
self::finance,
self::commitments,
self::professional,
self::product,
self::info,
self::press,
self::restauration
];
return match (true) {
in_array($this, $commonRoutes, true) => [
'title' => __('enums.contactFormType.' . $this->name . '.seo.title'),
'description' => __('enums.contactFormType.' . $this->name . '.seo.description'),
],
default => [],
};
}
/**
* Get the slug for each type
* Get the slug for each type.
*
* @return array
* @phpcs:disable Generic.Metrics.CyclomaticComplexity
@ -254,34 +49,17 @@ enum ContactFormType: int
{
// phpcs:enable
return match ($this) {
self::collaborator => [],
self::breeder => [
'marie-charlotte.garnier@ldc.fr',
'dylan.chevalier@ldc.fr',
],
self::professional => null,
self::restauration => [
'rhd@ldc.fr',
],
self::export => [
'alexis.lambert@ldc.fr',
'bruno.lafon@ldc.fr',
],
self::finance => [
'david.eluaud@ldc.fr',
'malaury.offe@ldc.fr',
],
self::commitments => [
'claire-marie.gesland@ldc.fr',
],
self::product => [],
self::info => [
'blandine.durand-gratian@ldc.fr',
'malaury.offe@ldc.fr',
],
self::press => [
'communicationgroupe@ldc.fr',
],
self::breeder => config('mail.to.breeder'),
self::restauration => config('mail.to.restauration'),
self::export => config('mail.to.export'),
self::finance => config('mail.to.finance'),
self::commitments => config('mail.to.commitments'),
self::info => config('mail.to.info'),
self::press => config('mail.to.press'),
self::collaborator,
self::product,
self::professional => null
// phpcs:ignore Squiz.Commenting.InlineComment
}; //end match
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace App\Enums\Contact;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum ExportAskingType: int
{
use BaseEnum;
case customer = 1;
case provider = 2;
case other = 3;
/**
* Optionnal labels definition.
*
* @phpstan-ignore-next-line
*/
private const LABELS = [
self::customer->name => 'enums.exportAskingType.customer',
self::provider->name => 'enums.exportAskingType.provider',
self::other->name => 'enums.exportAskingType.other',
];
}

View file

@ -8,7 +8,7 @@ enum DynamicFieldTypes: int
{
use BaseEnum;
// Name must be one word in lowercase.
// Name must be one word in lowercase.
case string = 1;
case textarea = 2;
case wysiwyg = 3;
@ -25,9 +25,11 @@ enum DynamicFieldTypes: int
case color = 14;
case model = 15;
case staticroute = 16;
case video = 17;
// Specific inputs.
case position = 101;
// Specific inputs.
case position = 101;
case contactform = 102;
/**
* Labels definition.
@ -51,9 +53,11 @@ enum DynamicFieldTypes: int
self::color->name => 'enums.dynamic-field-types.color',
self::model->name => 'enums.dynamic-field-types.model',
self::staticroute->name => 'enums.dynamic-field-types.staticroute',
self::video->name => 'enums.dynamic-field-types.video',
// Specific inputs.
self::position->name => 'enums.dynamic-field-types.position',
self::position->name => 'enums.dynamic-field-types.position',
self::contactform->name => 'enums.dynamic-field-types.contactform',
];
/**
@ -70,15 +74,17 @@ enum DynamicFieldTypes: int
self::image->name => 'required|file|mimes:jpg,jpeg,png,webp|max:5120|dimensions:width=100,height=100',
self::list->name => 'required|array|min:1|max:10',
self::file->name => 'required|file|mimes:pdf|max:5120',
self::vimeoid->name => 'required|string|min:8|max:10',
self::youtubeid->name => 'required|string|size:11',
self::vimeoid->name => 'required',
self::youtubeid->name => 'required',
self::date->name => 'required|date',
self::color->name => 'required|string|hex_color',
self::model->name => 'required|numeric|exists:dynamic_pages,id',
self::staticroute->name => 'required|numeric|between:1,10',
self::staticroute->name => 'required|numeric|between:1,99',
self::video->name => 'required',
// Specific inputs.
self::position->name => 'required|numeric|between:1,5',
self::contactform->name => 'required|numeric|between:1,10'
];
/**
@ -95,6 +101,7 @@ enum DynamicFieldTypes: int
* Is array definition.
*
* @return array
* @phpstan-ignore-next-line
*/
private const ISARRAY = [
self::string->name => false,
@ -113,8 +120,10 @@ enum DynamicFieldTypes: int
self::color->name => false,
self::model->name => false,
self::staticroute->name => false,
self::video->name => false,
// Specific inputs.
self::position->name => false,
self::position->name => false,
self::contactform->name => false,
];
}

View file

@ -21,18 +21,18 @@ enum OrdersEnum: int
self::desc->name => 'enums.orders.desc',
];
/**
* Get the orders to display using the app locale
* Get the orders to display using the app locale.
*
* @return boolean
*/
public function ordersWithLocal(): bool
{
$locale = config('app.locale');
return match (true) {
$this === self::asc => true,
$this === self::desc => true,
return match ($this) {
self::asc,
/** @phpstan-ignore-next-line */
self::desc => true,
default => $locale === "fr" ? true : false,
};
}

View file

@ -10,17 +10,6 @@ enum StaticRoutesEnum: int
use BaseEnum;
// Set all cases for each static routes.
case collaborator = 1;
case breeder = 2;
case professional = 3;
case restauration = 4;
case export = 5;
case finance = 6;
case commitments = 7;
case product = 8;
case info = 9;
case press = 10;
case search = 11;
/**
* Get array of each route with his matching value.
@ -28,29 +17,29 @@ enum StaticRoutesEnum: int
* @param callable|null $callback
* @return array
*/
public static function getRoutes(?callable $callback = null): array
public static function getRoutes(?callable $callback = null)
{
$dataKeyValue = array_merge(ContactFormType::getLabelisedRoutes(), [
self::search->name => (object) [
'label' => trans('front.search.title'),
'route' => '/recherche?texte=LDC',
]
]);
return collect($dataKeyValue)
->reject(fn($item) => is_null($item->label) || is_null($item->route))
->map(function ($item, $key) {
$targetEnum = collect(self::cases())
->first(fn($enum) => $enum->name === $key);
// $dataKeyValue = array_merge([], [
// self::search->name => (object) [
// 'label' => trans('front.search.title'),
// 'route' => '/recherche?texte=LDC',
// ]
// ]);
// return collect($dataKeyValue)
// ->reject(fn($item) => is_null($item->label) || is_null($item->route))
// ->map(function ($item, $key) {
// $targetEnum = collect(self::cases())
// ->first(fn($enum) => $enum->name === $key);
return (object) [
'label' => ucfirst($item->label),
'value' => $targetEnum->value,
'route' => $item->route,
];
})
->when(!is_null($callback), fn ($collection) => $collection->filter($callback))
->values()
->toArray();
// return (object) [
// 'label' => ucfirst($item->label),
// 'value' => $targetEnum->value,
// 'route' => $item->route,
// ];
// })
// ->when(!is_null($callback), fn ($collection) => $collection->filter($callback))
// ->values()
// ->toArray();
}
/**

View file

@ -0,0 +1,34 @@
<?php
namespace App\Enums;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum VideoProviderEnum: int
{
use BaseEnum;
case youtube = 1;
case vimeo = 2;
/**
* Optionnal labels definition.
*
* @phpstan-ignore-next-line
*/
private const LABELS = [
self::youtube->name => 'youtube',
self::vimeo->name => 'viméo',
];
/**
* Is array definition.
*
* @return array
* @phpstan-ignore-next-line
*/
private const COLOR = [
self::youtube->name => "#FF0000",
self::vimeo->name => "#00B2EA",
];
}

View file

@ -6,9 +6,9 @@ use App\Enums\DynamicFieldTypes;
use App\Enums\DynamicPages\PageStatusEnum;
use App\Models\DynamicPage;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Uwil\LaravelCommon\Helpers\FileHelper;
use Uwil\LaravelCommon\Helpers\ToolboxHelper;
@ -151,7 +151,7 @@ class DynamicPageHelper
? ''
: data_get($newContent, $key, '');
if ($old) {
FileHelper::removeOldModelFilesFromHtml($new, $old);
FileHelper::removeOldModelFilesFromHtml($new ?? '', $old);
}
});
}
@ -181,9 +181,9 @@ class DynamicPageHelper
/**
* Get all segments url for dynamic pages.
*
* @param \App\Models\DynamicPage $page
* @param \Illuminate\Support\Collection $pages
* @param string $locale
* @param \App\Models\DynamicPage $page
* @param \Illuminate\Database\Eloquent\Collection $pages
* @param string $locale
* @return array|null
*/
public static function getAllDynamicPageSegments(DynamicPage $page, Collection $pages, string $locale): ?array

View file

@ -2,7 +2,6 @@
namespace App\Helpers;
use App\Enums\Contact\ContactFormType;
use App\Models\DynamicPage;
use Illuminate\Routing\Route;
use Uwil\LaravelCommon\Controllers\Controller;
@ -63,7 +62,6 @@ class RouteHelper extends Controller
}
return self::resolve(
self::getDynamicPageLocalizedUrl($route, $localeCode),
self::getContactShowLocalizedUrl($route, $localeCode),
) ?? LaravelLocalization::getLocalizedURL($localeCode, null, [], false);
}
@ -83,48 +81,6 @@ class RouteHelper extends Controller
return null;
}
/**
* Return the localized url for contact page show.
*
* @param \Illuminate\Routing\Route $route
* @param string $localeCode
* @return string|false|null
*/
private static function getContactShowLocalizedUrl(Route $route, string $localeCode)
{
$contactPage = $route->parameter('contactPage');
if ($contactPage) {
$localizedUrl = LaravelLocalization::getLocalizedURL(
$localeCode,
null,
[],
false
);
$parsed = parse_url($localizedUrl);
if (!isset($parsed['path'])) {
return $localizedUrl;
}
$segments = explode('/', trim($parsed['path'], '/'));
$currentSlug = end($segments);
$enum = ContactFormType::fromSlug(
$currentSlug,
app()->getLocale()
);
if ($enum) {
$translatedSlug = $enum->slug($localeCode);
if ($translatedSlug) {
$segments[count($segments) - 1] = $translatedSlug;
return '/' . implode('/', $segments);
}
}
return $localizedUrl;
} //end if
}
/**
* Return the localized url for all dynamic pages.
*
@ -137,10 +93,10 @@ class RouteHelper extends Controller
/** @var \App\Models\DynamicPage|null $dynamicPage */
$dynamicPage = $route->parameter('dynamicPage');
if ($dynamicPage instanceof DynamicPage) {
$path = $dynamicPage->getLocalizedPath($localeCode);
if ($path) {
return LaravelLocalization::getLocalizedURL($localeCode, "/{$path}", [], false);
}
$path = $dynamicPage->getLocalizedPath($localeCode);
$defPath = $path ?? "";
return LaravelLocalization::getLocalizedURL($localeCode, "/{$defPath}", [], false);
}
return null;
}
}

View file

@ -64,29 +64,43 @@ class TranslationHelper
}
/**
* Assert value is unique in a translation field.
* Assert that a value is unique inside a JSON translation field.
*
* The uniqueness can optionally be scoped to another attribute/value pair.
* If $anotherDbAttribute and $anotherValue are provided, the uniqueness
* will be checked only within that scope. Otherwise, the value must be
* globally unique.
*
* @param \Illuminate\Database\Eloquent\Model $model The model instance being validated.
* @param string $value The translated value to check.
* @param string $dbAttribute The JSON column containing translations.
* @param string|null $anotherValue Optional value used to scope uniqueness.
* @param string|null $anotherDbAttribute Optional database column used to scope uniqueness.
* @param string|null $errorAttribute Optional attribute name used in the validation error.
* @param string|null $errorTranslation Optional translated attribute label for the error message.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $value
* @param string $dbAttribute
* @param string|null $errorAttribute
* @param string|null $errorTranslation
* @return void
* @throws \Illuminate\Validation\ValidationException Value is already used.
*
* @throws \Illuminate\Validation\ValidationException If the value is already used.
*/
public static function assertTranslationFieldIsUnique(
Model $model,
string $value,
string $dbAttribute,
?string $anotherValue = null,
?string $anotherDbAttribute = null,
?string $errorAttribute = null,
?string $errorTranslation = null,
): void {
$modelClass = get_class($model);
$exists = $modelClass::query()
$query = $modelClass::query()
->whereKeyNot($model->getKey())
->whereRaw("JSON_SEARCH($dbAttribute, 'one', ?) IS NOT NULL", [$value])
->exists();
if ($exists) {
->whereRaw("JSON_SEARCH(`{$dbAttribute}`, 'one', ?) IS NOT NULL", [$value]);
if ($anotherDbAttribute !== null) {
$query->where($anotherDbAttribute, $anotherValue);
}
if ($query->exists()) {
throw ValidationException::withMessages([
$errorAttribute ?? $dbAttribute => trans("validation.unique", [
'attribute' => $errorTranslation ?? trans("validation.attributes.$dbAttribute"),

View file

@ -0,0 +1,36 @@
<?php
namespace App\Http\Controllers\Api;
use App\Models\Company;
use Illuminate\Support\Str;
class CompanyController
{
/**
* Retrieve all published companies with their associated activity and sector.
*
* @return \Illuminate\Http\JsonResponse
*/
public function getCompanies(): \Illuminate\Http\JsonResponse
{
$companies = Company::query()->where('published', true)->with('companyActivity', 'companySector')->get();
$data = $companies->map(function ($company) {
return array_filter([
'name' => $company->name,
'logo' => $company->logo,
'informations' => $company->informations,
'responsable' => $company->responsable,
'employees' => $company->employees,
'url' => $company->url,
'latitude' => $company->latitude,
'longitude' => $company->longitude,
'type_activite' => Str::lower($company->companyActivity->name),
'filiere' => $company->companySector?->name,
], fn($value) => !is_null($value));
});
return response()->json($data);
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace App\Http\Controllers\Back;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Uwil\LaravelBackend\Controllers\AuthenticatedSessionController as PackageAuthenticatedSessionController;
class AuthenticatedSessionController extends PackageAuthenticatedSessionController
{
/**
* Destroy an authenticated session.
*/
public function destroy(Request $request): RedirectResponse
{
Auth::guard('backend')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return to_route('front.dynamic-pages.accueil.fr');
}
}

View file

@ -92,8 +92,10 @@ class CompanySectorController extends Controller
*/
public function show(CompanySector $company_sector): \Inertia\Response
{
$companies = $company_sector->companies()->paginate(ItemsPerPaginationEnum::five->value());
return Inertia::render('CompanySectors/Show', [
'data' => CommonResource::make($company_sector),
'companies' => CommonResource::collection($companies),
'modelName' => $this->modelName,
]);
}

View file

@ -2,8 +2,8 @@
namespace App\Http\Controllers\Back;
use App\Http\Requests\Back\DocumentCategory\StoreDocumentCategoryRequest;
use App\Http\Requests\Back\DocumentCategory\UpdateDocumentCategoryRequest;
use App\Http\Requests\Back\DocumentCategories\StoreDocumentCategoryRequest;
use App\Http\Requests\Back\DocumentCategories\UpdateDocumentCategoryRequest;
use App\Models\DocumentCategory;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
@ -99,7 +99,7 @@ class DocumentCategoryController extends Controller
/**
* Store a newly created resource.
*
* @param \App\Http\Requests\Back\News\StoreDocumentCategoryRequest $request
* @param \App\Http\Requests\Back\DocumentCategories\StoreDocumentCategoryRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(StoreDocumentCategoryRequest $request): \Illuminate\Http\RedirectResponse
@ -131,8 +131,8 @@ class DocumentCategoryController extends Controller
/**
* Update the specified resource.
*
* @param \App\Http\Requests\Back\News\UpdateDocumentCategoryRequest $request
* @param \App\Models\DocumentCategory $document_category
* @param \App\Http\Requests\Back\DocumentCategories\UpdateDocumentCategoryRequest $request
* @param \App\Models\DocumentCategory $document_category
* @return \Illuminate\Http\RedirectResponse
*/
public function update(

View file

@ -5,8 +5,8 @@ namespace App\Http\Controllers\Back;
use App\Enums\Document\DocumentStatusEnum;
use App\Enums\YearsEnum;
use App\Helpers\DocumentHelper;
use App\Http\Requests\Back\Document\StoreDocumentRequest;
use App\Http\Requests\Back\Document\UpdateDocumentRequest;
use App\Http\Requests\Back\Documents\StoreDocumentRequest;
use App\Http\Requests\Back\Documents\UpdateDocumentRequest;
use App\Models\Document;
use App\Models\DocumentCategory;
use App\Models\DocumentTag;
@ -62,6 +62,7 @@ class DocumentController extends Controller
/** Custom pagination */
$docModels = $this->paginate($query);
return Inertia::render('Document/Index', [
'documentsModel' => CommonResource::collection($docModels),
'searchFields' => $searchFields,
@ -107,7 +108,7 @@ class DocumentController extends Controller
/**
* Store a newly created resource.
*
* @param \App\Http\Requests\Back\Document\StoreDocumentRequest $request
* @param \App\Http\Requests\Back\Documents\StoreDocumentRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(StoreDocumentRequest $request): \Illuminate\Http\RedirectResponse
@ -143,8 +144,8 @@ class DocumentController extends Controller
/**
* Update the specified resource.
*
* @param \App\Http\Requests\Back\Document\UpdateDocumentRequest $request
* @param \App\Models\Document $document
* @param \App\Http\Requests\Back\Documents\UpdateDocumentRequest $request
* @param \App\Models\Document $document
* @return \Illuminate\Http\RedirectResponse
*/
public function update(UpdateDocumentRequest $request, Document $document): \Illuminate\Http\RedirectResponse

View file

@ -2,8 +2,8 @@
namespace App\Http\Controllers\Back;
use App\Http\Requests\Back\DocumentTag\StoreDocumentTagRequest;
use App\Http\Requests\Back\DocumentTag\UpdateDocumentTagRequest;
use App\Http\Requests\Back\DocumentTags\StoreDocumentTagRequest;
use App\Http\Requests\Back\DocumentTags\UpdateDocumentTagRequest;
use App\Models\DocumentTag;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
@ -99,7 +99,7 @@ class DocumentTagController extends Controller
/**
* Store a newly created resource.
*
* @param \App\Http\Requests\Back\News\StoreDocumentTagRequest $request
* @param \App\Http\Requests\Back\DocumentTags\StoreDocumentTagRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(StoreDocumentTagRequest $request): \Illuminate\Http\RedirectResponse
@ -131,8 +131,8 @@ class DocumentTagController extends Controller
/**
* Update the specified resource.
*
* @param \App\Http\Requests\Back\News\UpdateDocumentTagRequest $request
* @param \App\Models\DocumentTag $document_tag
* @param \App\Http\Requests\Back\DocumentTags\UpdateDocumentTagRequest $request
* @param \App\Models\DocumentTag $document_tag
* @return \Illuminate\Http\RedirectResponse
*/
public function update(

View file

@ -2,6 +2,7 @@
namespace App\Http\Controllers\Back;
use App\Enums\Contact\ContactFormType;
use App\Enums\DynamicPages\PageStatusEnum;
use App\Enums\ElementPositionEnum;
use App\Http\Requests\Back\DynamicPages\StoreDynamicPageRequest;
@ -92,6 +93,7 @@ class DynamicPageController extends Controller
'pageConfigModels' => $pageConfigModels,
'enumDynamicPageStatus' => PageStatusEnum::toArray(),
'enumElementPosition' => ElementPositionEnum::toArray(),
'enumContactFormType' => ContactFormType::toArray(),
'modelName' => $this->modelName,
]);
}
@ -150,7 +152,9 @@ class DynamicPageController extends Controller
'pageConfigModels' => $pageConfigModels,
'enumDynamicPageStatus' => PageStatusEnum::toArray(),
'enumElementPosition' => ElementPositionEnum::toArray(),
'enumContactFormType' => ContactFormType::toArray(),
'modelName' => $this->modelName,
'dynamicPageUrl' => DynamicPage::getAccessModel($dynamic_page->id),
]);
}

View file

@ -43,9 +43,8 @@ class InputsController extends Controller
return response()->json(['error' => 'Invalid model class'], 404);
}
// Determine the field to display.
$model = new $modelClass();
$model = new $modelClass();
$isTranslatable = method_exists($model, 'getTranslations');
$locale = app()->getLocale();
@ -120,7 +119,7 @@ class InputsController extends Controller
$url = (method_exists($modelClass, 'getAccessModel'))
? $modelClass::getAccessModel($validated['selectedEntry'])
: static::getAccessModel($validated['selectedEntry'], $validated['modelName']);
: self::getAccessModel($validated['selectedEntry'], $validated['modelName']);
$token = Crypt::encryptString(json_encode([
'modelName' => $validated['modelName'],
@ -184,7 +183,7 @@ class InputsController extends Controller
public function uploadFile(UploadFileRequest $request): \Illuminate\Http\JsonResponse
{
// Get the class of the target model and verify its existence.
$modelName = Str::singular($request->validated()['folder']);
$modelName = Str::singular($request->validated()['folder']);
$modelClass = "App\\Models\\$modelName";
if (!class_exists($modelClass)) {
return response()->json(['error' => 'Invalid model class'], 404);

View file

@ -3,8 +3,8 @@
namespace App\Http\Controllers\Back;
use App\Enums\News\NewsStatusEnum;
use App\Http\Requests\Back\NewsCategory\StoreNewsCategoryRequest;
use App\Http\Requests\Back\NewsCategory\UpdateNewsCategoryRequest;
use App\Http\Requests\Back\NewsCategories\StoreNewsCategoryRequest;
use App\Http\Requests\Back\NewsCategories\UpdateNewsCategoryRequest;
use App\Models\NewsCategory;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
@ -102,7 +102,7 @@ class NewsCategoryController extends Controller
/**
* Store a newly created resource.
*
* @param \App\Http\Requests\Back\NewsCategory\StoreNewsCategoryRequest $request
* @param \App\Http\Requests\Back\NewsCategories\StoreNewsCategoryRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(StoreNewsCategoryRequest $request): \Illuminate\Http\RedirectResponse
@ -134,8 +134,8 @@ class NewsCategoryController extends Controller
/**
* Update the specified resource.
*
* @param \App\Http\Requests\Back\News\UpdateNewsCategoryRequest $request
* @param \App\Models\NewsCategory $news_category
* @param \App\Http\Requests\Back\NewsCategories\UpdateNewsCategoryRequest $request
* @param \App\Models\NewsCategory $news_category
* @return \Illuminate\Http\RedirectResponse
*/
public function update(

View file

@ -3,8 +3,8 @@
namespace App\Http\Controllers\Back;
use App\Enums\News\NewsStatusEnum;
use App\Http\Requests\Back\NewsTag\StoreNewsTagRequest;
use App\Http\Requests\Back\NewsTag\UpdateNewsTagRequest;
use App\Http\Requests\Back\NewsTags\StoreNewsTagRequest;
use App\Http\Requests\Back\NewsTags\UpdateNewsTagRequest;
use App\Models\NewsTag;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
@ -102,7 +102,7 @@ class NewsTagController extends Controller
/**
* Store a newly created resource.
*
* @param \App\Http\Requests\Back\NewsTag\StoreNewsTagRequest $request
* @param \App\Http\Requests\Back\NewsTags\StoreNewsTagRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(StoreNewsTagRequest $request): \Illuminate\Http\RedirectResponse
@ -134,8 +134,8 @@ class NewsTagController extends Controller
/**
* Update the specified resource.
*
* @param \App\Http\Requests\Back\News\UpdateNewsTagRequest $request
* @param \App\Models\NewsTag $news_tag
* @param \App\Http\Requests\Back\NewsTags\UpdateNewsTagRequest $request
* @param \App\Models\NewsTag $news_tag
* @return \Illuminate\Http\RedirectResponse
*/
public function update(UpdateNewsTagRequest $request, NewsTag $news_tag): \Illuminate\Http\RedirectResponse

View file

@ -8,6 +8,7 @@ use App\Http\Requests\Front\Contact\ContactRequest;
use App\Jobs\Mails\SendContactMail;
use App\Models\ContactCount;
use App\Models\MailContactData;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\HtmlString;
use LDC\LDCCRM\Enums\ApiError;
@ -17,33 +18,6 @@ use LDC\LDCCRM\Models\SingleOptin;
class ContactController
{
/**
* Show contact index page.
*
* @return \Illuminate\Contracts\View\View
*/
public function index(): \Illuminate\Contracts\View\View
{
$types = ContactFormType::toArray();
return view('front.pages.contact.index', compact('types'));
}
/**
* Show contact show page.
*
* @param string $slug
* @return \Illuminate\Contracts\View\View
*/
public function show(string $slug): \Illuminate\Contracts\View\View
{
$enumType = collect(ContactFormType::all())->first(function ($enum) use ($slug) {
return $enum->slug() === $slug;
});
if (!$enumType) {
return redirect()->route('front.contact.index');
}
return view('front.pages.contact.show', compact('enumType'));
}
/**
* Contact send.
@ -53,12 +27,16 @@ class ContactController
*/
public function send(ContactRequest $request): \Illuminate\Http\RedirectResponse
{
$data = $request->validated();
$messageContact = new MailContactData($data);
$validatedData = $request->validated();
$messageContact = new MailContactData($validatedData);
$contactMessage = "";
$hasContactError = false;
$newsletterMessage = null;
$hasNewsletterError = false;
/** @var boolean $isNewsletter */
$isNewsletter = isset($validatedData['newsletter']) ? $validatedData['newsletter'] : false;
try {
dispatch(new SendContactMail($messageContact))
->onConnection('database')
@ -70,7 +48,7 @@ class ContactController
$hasContactError = true;
$contactMessage = trans('Une erreur est survenue lors de l\'envoi du message de contact.');
}
if (!empty($data['newsletter']) && $data['newsletter']) {
if ($isNewsletter) {
try {
SingleOptin::setStatus($request->email, true);
$newsletterMessage = trans("Votre inscription à la newsletter a été prise en compte");
@ -90,8 +68,8 @@ class ContactController
}
}
ContactCount::updateOrInsert(
['type' => $data['enumType']],
['submissions' => \DB::raw('submissions + 1')]
['type' => $validatedData['enumType']],
['submissions' => DB::raw('submissions + 1')]
);
$contactMessage = trans('Votre message a bien été envoyé.');

View file

@ -68,13 +68,14 @@ class HomeController extends Controller
/**
* Call the CRM to add a single optin
*
* @param \App\Http\Requests\Fo\SingleOptinRequest $request
* @param \App\Http\Requests\Front\SingleOptinRequest $request
* @return string|array|null|\Illuminate\Contracts\Translation\Translator
* @throws \LDC\LDCCRM\Exceptions\ValidationException ValidationException.
* @throws \Illuminate\Contracts\Container\BindingResolutionException BindingResolutionException.
*/
public function addOptIn(SingleOptinRequest $request)
{
public function addOptIn(
SingleOptinRequest $request,
): string|array|null|\Illuminate\Contracts\Translation\Translator {
try {
SingleOptin::setStatus($request->email, true);
return \response(trans("Votre inscription à la newsletter a été prise en compte"), 200);

View file

@ -2,16 +2,17 @@
namespace App\Http\Controllers\Front;
use Carbon\Carbon;
use App\Enums\OrdersEnum;
use App\Enums\YearsEnum;
use App\Http\Requests\Front\PostsRequest;
use App\Models\Document;
use App\Models\DocumentTag;
use App\Models\News;
use App\Models\NewsTag;
use App\Enums\YearsEnum;
use App\Models\Document;
use App\Enums\OrdersEnum;
use App\Models\DocumentTag;
use Illuminate\Support\Str;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Support\Collection;
use App\Http\Requests\Front\PostsRequest;
use Illuminate\Support\Str;
use Uwil\LaravelCommon\Controllers\Controller;
use Uwil\LaravelCommon\Helpers\CollectionHelper;
@ -26,12 +27,14 @@ class PostsController extends Controller
/**
* Extract unique years from date field in a collection.
*
* @param \Illuminate\Support\Collection $model
* @param string $field
* @param EloquentCollection $model
* @param string $field
* @return \Illuminate\Support\Collection
*/
private function getUniqueYearsFromCollection(Collection $model, string $field): \Illuminate\Support\Collection
{
private function getUniqueYearsFromCollection(
EloquentCollection $model,
string $field,
): \Illuminate\Support\Collection {
return $model->pluck($field)
->map(fn($date) => Carbon::parse($date)->year)
->unique()
@ -55,27 +58,30 @@ class PostsController extends Controller
}
/**
* Build the common filters configuration for front-end listing pages
* Build the common filters configuration for front-end listing pages.
* If the type is "documents" the "order" filter is included.
*
* If the type is "documents" the "order" filter is included
*
* @param string $type
* @param string $category
* @param \Illuminate\Database\Eloquent\Collection $publishedTags
* @param \Illuminate\Database\Eloquent\Collection $model
* @param string $type
* @param string $category
* @param EloquentCollection $publishedTags
* @param EloquentCollection $model
* @return array
*/
private function getFilters(string $type, string $category, Collection $publishedTags, Collection $model): array
{
private function getFilters(
string $type,
string $category,
EloquentCollection $publishedTags,
EloquentCollection $model,
): array {
$filters = [];
$tags = collect($publishedTags)
->map(fn($tag) => [
'title' => $tag->name,
'tag' => $tag->slug,
'category' => $category,
])
->values();
/** @phpstan-ignore-next-line */
$tags = $publishedTags->map(fn(DocumentTag|NewsTag $tag) => [
'title' => $tag->name,
'tag' => $tag->slug,
'category' => $category,
])
->values();
$enumYears = collect(YearsEnum::toArray())
->map(fn($year) => YearsEnum::make((int) $year->value))
@ -140,18 +146,19 @@ class PostsController extends Controller
*/
public function ajaxLoadNews(PostsRequest $request): \Illuminate\Http\JsonResponse
{
$data = $request->validated();
$validatedData = $request->validated();
$perPage = 9;
$selectedFilters = [];
$category = $data['category'];
app()->setLocale($data['locale']);
/** @var string $slugCategory */
$slugCategory = $validatedData['category'];
$publishedNewsTags = NewsTag::getTagsByCategoryInCurrentLocale($category);
app()->setLocale($validatedData['locale']);
$newsModel = News::getPublishedNewsByCategoryInCurrentLocale($category);
$publishedNewsTags = NewsTag::getTagsByCategoryInCurrentLocale($slugCategory);
$newsModel = News::getPublishedNewsByCategoryInCurrentLocale($slugCategory);
$requestedTags = collect($data['filters']['tags'] ?? '')
$requestedTags = collect($validatedData['filters']['tags'] ?? '')
->filter()
->unique()
->values();
@ -162,13 +169,13 @@ class PostsController extends Controller
'title' => $tag->name,
'tag' => $tag->slug,
'color' => $tag->color,
'category' => $category,
'category' => $slugCategory,
])
->values();
if ($selectedTags->isNotEmpty()) {
$newsModel = $newsModel->filter(function ($news) use ($selectedTags) {
return $news->tags->contains(
$newsModel = $newsModel->filter(function ($newsModel) use ($selectedTags) {
return $newsModel->tags->contains(
fn ($tag) => $selectedTags->contains('id', $tag->id)
);
});
@ -176,14 +183,14 @@ class PostsController extends Controller
$selectedFilters['tags'] = $selectedTags;
}
$selectedYears = collect($data['filters']['years'] ?? '')
$selectedYears = collect($validatedData['filters']['years'] ?? '')
->map(fn (string $label) => YearsEnum::makeFromLabel(Str::replace('-', '/', $label)))
->filter()
->map(fn($year) => [
'title' => $year->label(),
'tag' => Str::replace('/', '-', $year->label()),
'value' => explode('/', $year->label())[1],
'category' => $category,
'category' => $slugCategory,
])
->values();
@ -197,7 +204,7 @@ class PostsController extends Controller
$newsModel = $newsModel->sortByDesc('published_at');
$filters = $this->getFilters('news', $category, $publishedNewsTags, $newsModel);
$filters = $this->getFilters('news', $slugCategory, $publishedNewsTags, $newsModel);
// Custom pagination.
$newsModel = CollectionHelper::paginate($newsModel, $perPage);
@ -227,36 +234,36 @@ class PostsController extends Controller
*/
public function ajaxLoadDocuments(PostsRequest $request): \Illuminate\Http\JsonResponse
{
$data = $request->validated();
$validatedData = $request->validated();
$perPage = 3;
$selectedFilters = [];
$category = $data['category'];
app()->setLocale($data['locale']);
/** @var string $slugCategory */
$slugCategory = $validatedData['category'];
$publishedDocumentsTags = DocumentTag::getTagsByCategoryInCurrentLocale($category);
app()->setLocale($validatedData['locale']);
$documentsModel = Document::getPublishedDocumentsByCategoryInCurrentLocale($category);
$documentTagModels = DocumentTag::getTagsByCategoryInCurrentLocale($slugCategory);
$documentModels = Document::getPublishedDocumentsByCategoryInCurrentLocale($slugCategory);
$requestedTags = collect($data['filters']['tags'] ?? '')
$requestedTags = collect($validatedData['filters']['tags'] ?? '')
->filter()
->unique()
->values();
$selectedTags = $publishedDocumentsTags
$selectedTags = $documentTagModels
->filter(fn ($tag) => $requestedTags->contains($tag->slug ?? null))
->map(fn ($tag) => [
'id' => $tag->id,
'title' => $tag->name,
'tag' => $tag->slug,
'color' => $tag->color,
'category' => $category,
'category' => $slugCategory,
])
->values();
if ($selectedTags->isNotEmpty()) {
$documentsModel = $documentsModel->filter(function ($documents) use ($selectedTags) {
return $documents->tags->contains(
$documentModels = $documentModels->filter(function ($documentModel) use ($selectedTags) {
return $documentModel->tags->contains(
fn ($tag) => $selectedTags->contains('id', $tag->id)
);
});
@ -264,64 +271,65 @@ class PostsController extends Controller
$selectedFilters['tags'] = $selectedTags;
}
$selectedYears = collect($data['filters']['years'] ?? '')
$selectedYears = collect($validatedData['filters']['years'] ?? '')
->map(fn (string $label) => YearsEnum::makeFromLabel(Str::replace('-', '/', $label)))
->filter()
->map(fn($year) => [
'title' => $year->label(),
'tag' => Str::replace('/', '-', $year->label()),
'value' => $year->value,
'category' => $category,
'category' => $slugCategory,
])
->values();
if ($selectedYears->isNotEmpty()) {
$documentsModel = $documentsModel->filter(function ($documents) use ($selectedYears) {
return $selectedYears->contains('value', $documents->exercice);
$documentModels = $documentModels->filter(function ($documentModel) use ($selectedYears) {
return $selectedYears->contains('value', $documentModel->exercice);
});
$selectedFilters['years'] = $selectedYears;
}
$selectedOrder = collect($data['filters']['order'] ?? '')
$selectedOrder = collect($validatedData['filters']['order'] ?? '')
->map(fn (string $label) => OrdersEnum::makeFromLabel($label))
->filter()
->map(fn($order) => [
'title' => trans($order->label()),
'tag' => $order->name(),
'category' => $category,
'category' => $slugCategory,
])
->values();
if ($selectedOrder->isNotEmpty()) {
$documentsModel = $selectedOrder->map->tag->first() === 'desc'
? $documentsModel->sortByDesc('published_at')
: $documentsModel->sortBy('published_at');
$firstTag = $selectedOrder->map(fn($order) => $order['tag'])->first();
$documentModels = $firstTag === 'desc'
? $documentModels->sortByDesc('published_at')
: $documentModels->sortBy('published_at');
$selectedFilters['order'] = $selectedOrder->all();
} else {
$documentsModel = $documentsModel->sortByDesc('published_at');
$documentModels = $documentModels->sortByDesc('published_at');
}
$filters = $this->getFilters('documents', $category, $publishedDocumentsTags, $documentsModel);
$filters = $this->getFilters('documents', $slugCategory, $documentTagModels, $documentModels);
// Custom pagination.
$documentsModel = CollectionHelper::paginate($documentsModel, $perPage);
$documentModels = CollectionHelper::paginate($documentModels, $perPage);
return response()->json([
'pagination' => [
'total' => $documentsModel->total(),
'nbItems' => count($documentsModel->items()),
'currentPage' => $documentsModel->currentPage(),
'lastPage' => $documentsModel->lastPage(),
'hasMore' => $documentsModel->currentPage() < $documentsModel->lastPage(),
'total' => $documentModels->total(),
'nbItems' => count($documentModels->items()),
'currentPage' => $documentModels->currentPage(),
'lastPage' => $documentModels->lastPage(),
'hasMore' => $documentModels->currentPage() < $documentModels->lastPage(),
],
'filters' => view('components.front.posts.filters-dropdowns', ['filters' => $filters])->render(),
'selectedFilters' => view('components.front.posts.selected-filters', [
'selectedFilters' => $selectedFilters,
])->render(),
'news' => view('components.front.posts.documents', [
'documentsList' => $documentsModel->items(),
'documentsList' => $documentModels->items(),
])->render()
]);
}

View file

@ -20,9 +20,10 @@ class ForceLocale
* The issue occurred when two browser tabs were open in different locales:
* the data being saved could end up using the other tab’s locale.
*
* @param Request $request
* @param Closure $next
* @param \Illuminate\Http\Request $request
* @param Closure $next
* @return mixed
* @throws ValidationException Not supported locale.
*/
public function handle(Request $request, Closure $next): mixed
{
@ -43,7 +44,7 @@ class ForceLocale
$requestLocale = $request->locale ?? config('app.fallback_locale');
// 2. If prefix is not in supportedLocales
if (!in_array($requestLocale, LaravelLocalization::getSupportedLanguagesKeys(), true)) {
throw ValidationException::withMessages(["Langue" => "locale pas accepté"]);
throw ValidationException::withMessages(["Langue" => "Not supported locale."]);
}
// 3. Set new locale if locale and prefix are not the same
if ($requestLocale !== app()->getLocale()) {

View file

@ -5,6 +5,7 @@ namespace App\Http\Middleware;
use App\Enums\DynamicFieldTypes;
use App\Enums\ModelsList;
use App\Enums\StaticRoutesEnum;
use App\Enums\VideoProviderEnum;
use App\Helpers\TranslationHelper;
use App\Models\Company;
use App\Models\CompanyActivity;
@ -139,6 +140,7 @@ class HandleInertiaRequests extends Middleware
'enumDynamicFieldTypes' => DynamicFieldTypes::toArray(),
'enumModelsList' => ModelsList::toArray(),
'enumStaticRoutes' => StaticRoutesEnum::getRoutes(),
'videoProviderEnum' => VideoProviderEnum::toArray(),
'errors' => $errors,
'translations' => $translations,
];

View file

@ -5,13 +5,18 @@ namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class Lang
{
/**
* Handle an incoming request.
* Not used but need it for uwil package.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return \Symfony\Component\HttpFoundation\Response
*/
public function handle(Request $request, Closure $next): mixed
{
return $next($request);
}
}

View file

@ -3,9 +3,10 @@
namespace App\Http\Requests\Back\Companies;
use App\Models\Company;
use Illuminate\Foundation\Http\FormRequest;
use App\Traits\Requests\HasInertiaPicture;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
use Uwil\LaravelCommon\Rules\WysiwygLength;
class StoreCompanyRequest extends FormRequest
{
@ -48,7 +49,7 @@ class StoreCompanyRequest extends FormRequest
),
'responsable' => 'required|nullable|string|between:3,255',
'url' => 'nullable|string|between:3,255',
'informations' => 'required|string|between:3,255',
'informations' => ['required', 'string', new WysiwygLength(10, 5000)],
'employees' => 'required|nullable|numeric|between:0,100000',
'latitude' => 'required|numeric|between:-90,90',
'longitude' => 'required|numeric|between:-180,180',

View file

@ -1,6 +1,6 @@
<?php
namespace App\Http\Requests\Back\DocumentCategory;
namespace App\Http\Requests\Back\DocumentCategories;
use App\Models\DocumentCategory;
use Illuminate\Foundation\Http\FormRequest;

View file

@ -1,6 +1,6 @@
<?php
namespace App\Http\Requests\Back\DocumentCategory;
namespace App\Http\Requests\Back\DocumentCategories;
use Illuminate\Support\Facades\Gate;

View file

@ -1,6 +1,6 @@
<?php
namespace App\Http\Requests\Back\DocumentTag;
namespace App\Http\Requests\Back\DocumentTags;
use App\Models\DocumentTag;
use Illuminate\Foundation\Http\FormRequest;

View file

@ -1,6 +1,6 @@
<?php
namespace App\Http\Requests\Back\DocumentTag;
namespace App\Http\Requests\Back\DocumentTags;
use Illuminate\Support\Facades\Gate;

View file

@ -1,6 +1,6 @@
<?php
namespace App\Http\Requests\Back\Document;
namespace App\Http\Requests\Back\Documents;
use App\Enums\Document\DocumentStatusEnum;
use App\Enums\YearsEnum;

View file

@ -1,6 +1,6 @@
<?php
namespace App\Http\Requests\Back\Document;
namespace App\Http\Requests\Back\Documents;
use Illuminate\Support\Facades\Gate;

View file

@ -4,6 +4,7 @@ namespace App\Http\Requests\Back\News;
use App\Enums\News\NewsStatusEnum;
use App\Models\News;
use App\Rules\Video;
use App\Traits\Requests\HasInertiaPicture;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;

View file

@ -1,6 +1,6 @@
<?php
namespace App\Http\Requests\Back\NewsCategory;
namespace App\Http\Requests\Back\NewsCategories;
use App\Models\NewsCategory;
use Illuminate\Foundation\Http\FormRequest;

View file

@ -1,6 +1,6 @@
<?php
namespace App\Http\Requests\Back\NewsCategory;
namespace App\Http\Requests\Back\NewsCategories;
use Illuminate\Support\Facades\Gate;

View file

@ -1,6 +1,6 @@
<?php
namespace App\Http\Requests\Back\NewsTag;
namespace App\Http\Requests\Back\NewsTags;
use App\Models\NewsTag;
use Illuminate\Foundation\Http\FormRequest;

View file

@ -1,6 +1,6 @@
<?php
namespace App\Http\Requests\Back\NewsTag;
namespace App\Http\Requests\Back\NewsTags;
use Illuminate\Support\Facades\Gate;

View file

@ -50,7 +50,7 @@ class StorePageConfigRequest extends FormRequest
'string',
'between:3,255',
'regex:/^[a-z0-9]+(?:[-_][a-z0-9]+)*$/',
function ($attribute, $value, $fail) use ($prefix) {
function ($attribute, $value, $fail) {
$parentPath = implode('.', array_slice(explode('.', $attribute), 0, -2));
$levelItems = data_get($this->input(), $parentPath, []);
$keysAtLevel = collect($levelItems)->pluck('key')->filter()->toArray();

View file

@ -2,9 +2,12 @@
namespace App\Http\Requests\Back;
use App\Enums\Contact\ContactFormType;
use App\Enums\ElementPositionEnum;
use App\Enums\DynamicFieldTypes;
use App\Enums\StaticRoutesEnum;
use App\Models\PageConfig;
use App\Rules\Video;
use App\Traits\Requests\HasInertiaPicture;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Str;
@ -200,6 +203,19 @@ abstract class UpdateDynamicFieldRequest extends FormRequest
),
]);
break;
case DynamicFieldTypes::contactform->value:
$rules = array_merge($rules, [
$ruleKey => array_merge(
$this->compileValidate(DynamicFieldTypes::position->getValidation(), $input),
[new Enum(ContactFormType::class)],
),
]);
break;
case DynamicFieldTypes::video->value:
$rules = array_merge($rules, [
$ruleKey => new Video(),
]);
break;
default:
throw new \UnexpectedValueException("Unrecognized dynamic type: " . ($input['type'] ?? 'null'));
} //end switch

View file

@ -5,6 +5,7 @@ namespace App\Http\Requests\Front\Contact;
use App\Enums\Contact\BreedType;
use App\Enums\Contact\ContactFormType;
use App\Enums\Contact\CustomerAskingType;
use App\Enums\Contact\ExportAskingType;
use App\Enums\Contact\PressType;
use App\Enums\Department;
use Illuminate\Foundation\Http\FormRequest;
@ -93,14 +94,21 @@ class ContactRequest extends FormRequest
// Validation for restauration and export commun fields.
if (
ContactFormType::restauration->equals(ContactFormType::make($typeContactForm)) ||
ContactFormType::export->equals(ContactFormType::make($typeContactForm))
ContactFormType::restauration->equals(ContactFormType::make($typeContactForm))
) {
$rules = array_merge($rules, [
'type' => ['array', 'min:1'],
'type.*' => [Rule::enum(CustomerAskingType::class)],
]);
}
if (
ContactFormType::export->equals(ContactFormType::make($typeContactForm))
) {
$rules = array_merge($rules, [
'type' => ['array', 'min:1'],
'type.*' => [Rule::enum(ExportAskingType::class)],
]);
}
// Validation for press enum.
if (ContactFormType::press->equals(ContactFormType::make($typeContactForm))) {

View file

@ -2,10 +2,13 @@
namespace App\Http\Requests\Front;
use App\Traits\Requests\Api\HasLaravelFailedValidation;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class PostsRequest extends FormRequest
{
use HasLaravelFailedValidation;
/**
* Get the validation rules that apply to the request.
*
@ -16,7 +19,7 @@ class PostsRequest extends FormRequest
return [
'page' => 'required|integer',
'category' => 'required|string',
'locale' => 'required|string',
'locale' => ['required', 'string', Rule::in(config('app.locales'))],
'filters' => 'nullable|array',
'filters.order' => 'nullable|string|in:asc,desc',
'filters.tags' => 'nullable|array',

View file

@ -39,7 +39,7 @@ class Company extends Model
/**
* The attributes that are mass assignable.
*
* @var array<int, string, array[]>
* @var list<string>
*/
protected $fillable = [
'name',
@ -67,7 +67,7 @@ class Company extends Model
/**
* Relations should be included in the cache.
*
* @var array<string, string>
* @var list<string>
*/
protected static $cacheRelations = [
'companyActivity',

View file

@ -33,7 +33,7 @@ class CompanyActivity extends Model implements TranslatableModel
/**
* The attributes that are mass assignable.
*
* @var array<int, string, array[]>
* @var list<string>
*/
protected $fillable = [
'name',

View file

@ -32,7 +32,7 @@ class CompanySector extends Model implements TranslatableModel
/**
* The attributes that are mass assignable.
*
* @var array<int, string, array[]>
* @var list<string>
*/
protected $fillable = [
'name',

View file

@ -10,7 +10,7 @@ class ContactCount extends Model
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
* @var list<string>
*/
protected $fillable = [
'type',

View file

@ -7,6 +7,7 @@ use App\Enums\Document\DocumentStatusEnum;
use App\Enums\YearsEnum;
use App\Helpers\DocumentHelper;
use App\Helpers\TranslationHelper;
use App\Models\Documentable;
use App\Traits\Models\HasCache;
use App\Traits\Models\HasTranslations;
use App\Traits\Models\HasUwilImages;
@ -18,6 +19,32 @@ use Illuminate\Support\Facades\App;
use Laravel\Scout\Searchable;
use Uwil\LaravelActivityLogs\Traits\ActivityLog;
/**
* @property integer $id
* Id of the document.
* @property string $name
* Name of the document.
* @property string $path
* Path of the document.
* @property boolean $is_issuu
* Issu status of the document.
* @property string $issuu_path
* Issu path of the document.
* @property string $exercice
* Exercice of the document.
* @property \App\Enums\Document\DocumentStatusEnum $status
* Status of the document.
* @property-read \Carbon\Carbon $created_at
* Created date of the document.
* @property-read \Carbon\Carbon $updated_at
* Updated date of the document.
* @property-read \Illuminate\Support\Carbon $deleted_at
* Deleted date of the document.
* @property \Illuminate\Database\Eloquent\Collection<int, \App\Models\DocumentCategory> $categories
* Associated document categories of the document.
* @property \Illuminate\Database\Eloquent\Collection<int, \App\Models\DocumentTag> $tags
* Associated document tags of the document.
*/
class Document extends Model implements TranslatableModel
{
use ActivityLog;
@ -81,7 +108,7 @@ class Document extends Model implements TranslatableModel
/**
* Relations should be included in the cache.
*
* @var array<string, string>
* @var list<string>
*/
protected static $cacheRelations = [
'categories',
@ -324,24 +351,24 @@ class Document extends Model implements TranslatableModel
/**
* Get all published news by category.
*
* @param string $category
*
* @return \Illuminate\Database\Eloquent\Collection
* @param string $slugCategory
* @return \Illuminate\Database\Eloquent\Collection<int, \App\Models\Document>
*/
public static function getPublishedDocumentsByCategoryInCurrentLocale(string $category)
{
public static function getPublishedDocumentsByCategoryInCurrentLocale(
string $slugCategory,
): \Illuminate\Database\Eloquent\Collection {
$locale = App::getLocale();
return Document::getModelsCached()
->where('status', DocumentStatusEnum::published)
->filter(function ($document) use ($category, $locale) {
->filter(function ($document) use ($slugCategory, $locale) {
$hasPath = !empty($document->getTranslation('path', $locale, false));
$hasIssuuPath = $document->is_issuu === true
&& !empty($document->getTranslation('issuu_path', $locale, false));
return ($hasPath || $hasIssuuPath) && $document->categories->contains(
fn($cat) => $cat->slug === $category
fn($documentCategoryModel) => $documentCategoryModel->slug === $slugCategory
);
});
}
@ -349,11 +376,11 @@ class Document extends Model implements TranslatableModel
// * RELATIONSHIPS
/**
* Relation to Tag
* Relation to Tag.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
* @return \Illuminate\Database\Eloquent\Relations\MorphToMany
*/
public function tags(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
public function tags(): \Illuminate\Database\Eloquent\Relations\MorphToMany
{
return $this->morphedByMany(
DocumentTag::class,
@ -361,15 +388,15 @@ class Document extends Model implements TranslatableModel
'documentables',
'document_id',
'documentable_id'
)->withTimestamps();
)->using(Documentable::class)->withTimestamps();
}
/**
* Relation to Category
* Relation to Category.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
* @return \Illuminate\Database\Eloquent\Relations\MorphToMany
*/
public function categories(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
public function categories(): \Illuminate\Database\Eloquent\Relations\MorphToMany
{
return $this->morphedByMany(
DocumentCategory::class,
@ -377,6 +404,6 @@ class Document extends Model implements TranslatableModel
'documentables',
'document_id',
'documentable_id'
)->withTimestamps();
)->using(Documentable::class)->withTimestamps();
}
}

View file

@ -10,6 +10,22 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Uwil\LaravelActivityLogs\Traits\ActivityLog;
/**
* @property integer $id
* Id of the category.
* @property string $name
* Name of the category.
* @property string $slug
* Slug of the category.
* @property-read \Carbon\Carbon $created_at
* Created date of the category.
* @property-read \Carbon\Carbon $updated_at
* Updated date of the category.
* @property-read \Illuminate\Support\Carbon $deleted_at
* Deleted date of the category.
* @property \Illuminate\Database\Eloquent\Collection<int, \App\Models\Document> $documents
* Associated documents of the category.
*/
class DocumentCategory extends Model implements TranslatableModel
{
use ActivityLog;
@ -38,12 +54,14 @@ class DocumentCategory extends Model implements TranslatableModel
'slug'
];
// * RELATIONSHIPS
/**
* Relation to Document
* Associated documents of the category.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
* @return \Illuminate\Database\Eloquent\Relations\MorphToMany
*/
public function documents(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
public function documents(): \Illuminate\Database\Eloquent\Relations\MorphToMany
{
return $this->morphToMany(
Document::class,

View file

@ -11,6 +11,24 @@ use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\App;
use Uwil\LaravelActivityLogs\Traits\ActivityLog;
/**
* @property integer $id
* Id of the tag.
* @property string $name
* Name of the tag.
* @property string $slug
* Slug of the tag.
* @property string $color
* Color of the tag.
* @property-read \Carbon\Carbon $created_at
* Created date of the tag.
* @property-read \Carbon\Carbon $updated_at
* Updated date of the tag.
* @property-read \Illuminate\Support\Carbon $deleted_at
* Deleted date of the tag.
* @property \Illuminate\Database\Eloquent\Collection<int, \App\Models\Document> $documents
* Associated documents of the tag.
*/
class DocumentTag extends Model implements TranslatableModel
{
use ActivityLog;
@ -43,34 +61,20 @@ class DocumentTag extends Model implements TranslatableModel
/**
* Relations should be included in the cache.
*
* @var array<string, string>
* @var list<string>
*/
protected static $cacheRelations = [
'documents',
];
/**
* Relation to Document
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function documents(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
{
return $this->morphToMany(
Document::class,
'documentable',
'documentables',
'documentable_id',
'document_id'
);
}
// * METHODS
/**
* Get all Tags by category.
*
* @param string $slugCategory
*
* @return \Illuminate\Database\Eloquent\Collection
* @return \Illuminate\Database\Eloquent\Collection<int, \App\Models\DocumentTag>
*/
public static function getTagsByCategoryInCurrentLocale(
string $slugCategory,
@ -92,4 +96,22 @@ class DocumentTag extends Model implements TranslatableModel
})->isNotEmpty();
});
}
// * RELATIONSHIPS
/**
* Associated documents of the tag.
*
* @return \Illuminate\Database\Eloquent\Relations\MorphToMany
*/
public function documents(): \Illuminate\Database\Eloquent\Relations\MorphToMany
{
return $this->morphToMany(
Document::class,
'documentable',
'documentables',
'documentable_id',
'document_id'
);
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\MorphPivot;
/**
* @property integer $id Id of the document.
* @property \App\Models\Document $document_id Name of the document.
* @property \App\Models\DocumentCategory|\App\Models\DocumentTag $documentable_id Name of the category or tag.
* @property class-string<static> $documentable_type Class of the category or tag.
* @property-read \Carbon\Carbon $created_at Created date of the document.
* @property-read \Carbon\Carbon $updated_at Updated date of the document.
*/
class Documentable extends MorphPivot
{
/**
* The table associated with the model.
*
* @var string|null
*/
protected $table = 'documentables';
/**
* Perform any actions required after the model boots.
*
* @return void
*/
protected static function booted(): void
{
static::created(fn() => Document::resetCache());
static::updated(fn() => Document::resetCache());
static::deleted(fn() => Document::resetCache());
}
}

View file

@ -13,11 +13,15 @@ use App\Traits\Models\HasUwilImages;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Laravel\Scout\Searchable;
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;
use Uwil\LaravelActivityLogs\Traits\ActivityLog;
use Spatie\Sitemap\Contracts\Sitemapable;
use Spatie\Sitemap\Tags\Url;
use Uwil\SitemapGenerator\Lib\SitemapHelper;
/**
* @property string $id_key Unique key identification of the dynamic page.
@ -33,7 +37,7 @@ use Uwil\LaravelActivityLogs\Traits\ActivityLog;
* @property-read \Illuminate\Support\Carbon $updated_at Date on which the dynamic page was updated.
* @property-read \Illuminate\Support\Carbon $deleted_at Date on which the dynamic page was updated.
*/
class DynamicPage extends Model implements TranslatableModel
class DynamicPage extends Model implements TranslatableModel, Sitemapable
{
use ActivityLog;
use HasCache;
@ -86,7 +90,7 @@ class DynamicPage extends Model implements TranslatableModel
/**
* Relations should be included in the cache.
*
* @var array<string, string>
* @var list<string>
*/
protected static $cacheRelations = [
'config',
@ -104,7 +108,14 @@ class DynamicPage extends Model implements TranslatableModel
static::creating(function (self $model) {
self::manageFields($model);
// Set id_key from title.
$model->id_key = Str::slug($model->title);
$idKey = self::generateUniqueField(
'dynamic_pages',
'id_key',
Str::slug($model->title),
$model->id
);
$model->id_key = $idKey;
});
static::updating(function (self $model) {
self::manageFields($model);
@ -131,6 +142,42 @@ class DynamicPage extends Model implements TranslatableModel
}
// * METHODS
/**
* Generate unique value from model field
*
* @param string $table
* @param string $field
* @param string $baseValue
* @param mixed $ignoreId
* @return string
*/
public static function generateUniqueField(
string $table,
string $field,
string $baseValue,
int|string|null $ignoreId = null
): string {
$value = $baseValue;
$counter = 1;
// Boucle tant que le slug existe en base
while (true) {
$query = DB::table($table)->where($field, $value);
if ($ignoreId) {
$query->where('id', '!=', $ignoreId);
}
if (!$query->exists()) {
// On a trouvé un slug unique
return $value;
}
// Sinon on incrémente le suffixe
$value = $baseValue . '_' . $counter;
$counter++;
}
}
/**
* Unpublished all children pages if change status published from draft/unpublished.
@ -198,8 +245,8 @@ class DynamicPage extends Model implements TranslatableModel
self::assertRootIsUnique($model);
} else {
$slug = $model->slug ?? Str::slug($model->title);
TranslationHelper::assertTranslationFieldIsUnique($model, $model->title, 'title');
TranslationHelper::assertTranslationFieldIsUnique($model, $slug, 'slug');
TranslationHelper::assertTranslationFieldIsUnique($model, $model->title, 'title', $model->page_id, 'page_id');
TranslationHelper::assertTranslationFieldIsUnique($model, $slug, 'slug', $model->page_id, 'page_id');
$model->setAttribute('slug', $slug);
}
// Check translations.
@ -327,7 +374,7 @@ class DynamicPage extends Model implements TranslatableModel
/**
* Get fields that we want to keep in the current locale.
*
* @return array.
* @return array
*/
public static function searchTransFields(): array
{
@ -413,7 +460,7 @@ class DynamicPage extends Model implements TranslatableModel
? strip_tags(collect($this->getTranslation('content', $locale))
->flatten()
->filter(fn($value, $key) =>
is_string($value) &&
is_string($value) &&
!empty($value) &&
!is_numeric($value) &&
!in_array($key, ['order', 'id']) &&
@ -428,6 +475,47 @@ class DynamicPage extends Model implements TranslatableModel
return $data;
}
// * SITEMAP
/**
* toSitemapTag method
*
* @return Url|string|array
*/
public function toSitemapTag(): Url | string | array
{
$sitemapLines = [];
$locales = array_keys(config('laravellocalization.supportedLocales', ['fr' => "Français"]));
foreach ($locales as $locale) {
$slug = $this->getTranslation('slug', $locale, false);
// Homepage has no slug.
if ($slug || $this->isRoot) {
$sitemapLines[] = SitemapHelper::generateUrl(
$this->getLocalizedUrl($locale),
locales: $this->locales(),
getAlternate: fn (string $locale) => $this->getLocalizedUrl($locale)
);
}
}
return $sitemapLines;
}
/**
* Get localized URL.
*
* @param string $locale
* @return string|false
*/
public function getLocalizedUrl(string $locale): string|false
{
return in_array($locale, $this->locales()) ?
route("front.dynamic-pages.$this->id_key.$locale") :
false;
}
// * RELATIONSHIPS
/**

View file

@ -44,7 +44,7 @@ class MailContactData extends Model
/**
* The attributes that are mass assignable.
*
* @var array<int, string, array[]>
* @var list<string>
*/
protected $fillable = [
'enumType',

View file

@ -17,34 +17,56 @@ use Illuminate\Support\Arr;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Str;
use Laravel\Scout\Searchable;
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;
use Mcamara\LaravelLocalization\Interfaces\LocalizedUrlRoutable;
use Uwil\LaravelActivityLogs\Traits\ActivityLog;
use Spatie\Sitemap\Tags\Url;
use Uwil\SitemapGenerator\Interfaces\Sitemapable;
use Uwil\SitemapGenerator\Lib\SitemapHelper;
/**
* @property string $seo_title SEO title of the news.
* @property string $seo_description SEO description of the news.
* @property string $title Title of the news.
* @property string $slug Slug of the news.
* @property string $picture Main img of the news.
* @property string $picture_alt Alt text for the picture.
* @property string $picture_title Title for the picture.
* @property string $content Content of the news.
* @property boolean $inverted Are image and content inverted in front
* @property string $vimeo_id Optionnal vimeo_id
* @property boolean $has_show_picture Show the picture in the show page
* @property \App\Enums\News\NewsStatusEnum $status Status of the news.
* @property-read \Illuminate\Support\Carbon $created_at Date on which the news was created.
* @property-read \Illuminate\Support\Carbon $updated_at Date on which the news was updated.
* @property-read \Illuminate\Support\Carbon $deleted_at Date on which the news was updated.
* @property string $seo_title
* SEO title of the news.
* @property string $seo_description
* SEO description of the news.
* @property string $title
* Title of the news.
* @property string $slug
* Slug of the news.
* @property string $picture
* Main img of the news.
* @property string $picture_alt
* Alt text for the picture.
* @property string $picture_title
* Title for the picture.
* @property string $content
* Content of the news.
* @property boolean $inverted
* Are image and content inverted in front
* @property string $vimeo_id
* Optionnal vimeo_id
* @property boolean $has_show_picture
* Show the picture in the show page
* @property \App\Enums\News\NewsStatusEnum $status
* Status of the news.
* @property-read \Illuminate\Support\Carbon $created_at
* Date on which the news was created.
* @property-read \Illuminate\Support\Carbon $updated_at
* Date on which the news was updated.
* @property-read \Illuminate\Support\Carbon $deleted_at
* Date on which the news was updated.
* @property \Illuminate\Database\Eloquent\Collection<int, \App\Models\NewsCategory> $categories
* Associated news categories of the news.
* @property \Illuminate\Database\Eloquent\Collection<int, \App\Models\NewsTag> $tags
* Associated news tags of the news.
*/
class News extends Model implements TranslatableModel, LocalizedUrlRoutable
class News extends Model implements TranslatableModel, LocalizedUrlRoutable, Sitemapable
{
use ActivityLog;
use HasCache;
use HasFactory;
use HasTranslations;
use SoftDeletes;
use HasCache;
use HasUwilImages {
HasUwilImages::inertiaFill as traitInertiaFill;
}
@ -113,7 +135,7 @@ class News extends Model implements TranslatableModel, LocalizedUrlRoutable
'unpublished_at',
'inverted',
'vimeo_id',
'has_show_picture'
'has_show_picture',
];
/**
@ -145,7 +167,7 @@ class News extends Model implements TranslatableModel, LocalizedUrlRoutable
/**
* Relations should be included in the cache.
*
* @var array<string, string>
* @var list<string>
*/
protected static $cacheRelations = [
'categories',
@ -332,9 +354,9 @@ class News extends Model implements TranslatableModel, LocalizedUrlRoutable
* Get the url to access a specific resource.
*
* @param integer $id Id field of the target model.
* @return string
* @return string|null
*/
public static function getAccessModel(int $id): string
public static function getAccessModel(int $id): ?string
{
$targetModel = self::getModelsCached()->firstWhere('id', $id);
if (!$targetModel) {
@ -432,8 +454,7 @@ class News extends Model implements TranslatableModel, LocalizedUrlRoutable
* Get all published news by category.
*
* @param string $category
*
* @return \Illuminate\Database\Eloquent\Collection
* @return \Illuminate\Database\Eloquent\Collection<int, \App\Models\News>
*/
public static function getPublishedNewsByCategoryInCurrentLocale(string $category)
{
@ -448,6 +469,49 @@ class News extends Model implements TranslatableModel, LocalizedUrlRoutable
});
}
// * SITEMAP
/**
* toSitemapTag method
*
* @return Url|string|array
*/
public function toSitemapTag(): Url | string | array
{
$model = $this;
$locales = $model->locales();
$sitemapUrls = [];
foreach ($locales as $locale) {
if (in_array($locale, config('sitemap-generator.supported_locales'))) {
$sitemapUrls[] = SitemapHelper::generateUrl(
$model->getLocalizedUrl($locale),
$model,
$locales,
currentLocale: $locale,
isNews: true
);
}
}
return $sitemapUrls;
}
/**
* Get localized URL from Mcamara\LaravelLocalization.
*
* @param string $locale
* @return string
*/
public function getLocalizedUrl(string $locale): string
{
return LaravelLocalization::getURLFromRouteNameTranslated(
$locale,
'routes.front.news',
['news' => $this]
) ?? route('front.news.show', $this);
}
// * RELATIONSHIPS
/**
@ -463,7 +527,7 @@ class News extends Model implements TranslatableModel, LocalizedUrlRoutable
'newsables',
'news_id',
'newsable_id'
);
)->using(Newsable::class);
}
/**
@ -479,6 +543,6 @@ class News extends Model implements TranslatableModel, LocalizedUrlRoutable
'newsables',
'news_id',
'newsable_id'
)->withTimestamps();
)->using(Newsable::class)->withTimestamps();
}
}

View file

@ -8,16 +8,21 @@ use App\Traits\Models\HasCache;
use App\Traits\Models\HasTranslations;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Str;
use Uwil\LaravelActivityLogs\Traits\ActivityLog;
/**
* @property int $news_id News focused.
* @property string $name Name of the category.
* @property-read \Illuminate\Support\Carbon $created_at Date on which the news was created.
* @property-read \Illuminate\Support\Carbon $updated_at Date on which the news was updated.
* @property int $id
* Id of the news.
* @property string $name
* Name of the category.
* @property-read \Illuminate\Support\Carbon $created_at
* Date on which the category was created.
* @property-read \Illuminate\Support\Carbon $updated_at
* Date on which the category was updated.
* @property \Illuminate\Database\Eloquent\Collection<int, \App\Models\News> $news
* Associated news of the category.
*/
class NewsCategory extends Model implements TranslatableModel
{
@ -30,7 +35,7 @@ class NewsCategory extends Model implements TranslatableModel
/**
* The attributes that are mass assignable.
*
* @var array<int, string, array[]>
* @var list<string>
*/
protected $fillable = [
'name',
@ -63,6 +68,8 @@ class NewsCategory extends Model implements TranslatableModel
});
}
// * METHODS
/**
* Manage model fields.
*
@ -80,12 +87,14 @@ class NewsCategory extends Model implements TranslatableModel
TranslationHelper::validateFallbackTranslations($model);
}
// * RELATIONSHIPS
/**
* The relation with News
* Associated news of the category.
*
* @return BelongsToMany
* @return \Illuminate\Database\Eloquent\Relations\MorphToMany
*/
public function news(): BelongsToMany
public function news(): \Illuminate\Database\Eloquent\Relations\MorphToMany
{
return $this->morphToMany(
News::class,

View file

@ -12,11 +12,18 @@ use Uwil\LaravelActivityLogs\Traits\ActivityLog;
use Illuminate\Database\Eloquent\Factories\HasFactory;
/**
* @property int $news_id News focused.
* @property string $name Name of the tag.
* @property string $color Color of the tag
* @property-read \Illuminate\Support\Carbon $created_at Date on which the news was created.
* @property-read \Illuminate\Support\Carbon $updated_at Date on which the news was updated.
* @property int $news_id
* News focused.
* @property string $name
* Name of the tag.
* @property string $color
* Color of the tag
* @property-read \Illuminate\Support\Carbon $created_at
* Date on which the tag was created.
* @property-read \Illuminate\Support\Carbon $updated_at
* Date on which the tag was updated.
* @property \Illuminate\Database\Eloquent\Collection<int, \App\Models\News> $news
* Associated news of the tag.
*/
class NewsTag extends Model implements TranslatableModel
{
@ -50,33 +57,19 @@ class NewsTag extends Model implements TranslatableModel
/**
* Relations should be included in the cache.
*
* @var array<string, string>
* @var list<string>
*/
protected static $cacheRelations = [
'news',
];
/**
* Relation to Document
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function news(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
{
return $this->morphToMany(
News::class,
'newsable',
'newsables',
'newsable_id',
'news_id'
);
}
// * METHODS
/**
* Get all Tags by category.
*
* @param string $slugCategory
* @return \Illuminate\Database\Eloquent\Collection
* @return \Illuminate\Database\Eloquent\Collection<int, \App\Models\NewsTag>
*/
public static function getTagsByCategoryInCurrentLocale(
string $slugCategory,
@ -93,4 +86,22 @@ class NewsTag extends Model implements TranslatableModel
})->isNotEmpty();
});
}
// * RELATIONSHIPS
/**
* Associated news of the tag.
*
* @return \Illuminate\Database\Eloquent\Relations\MorphToMany
*/
public function news(): \Illuminate\Database\Eloquent\Relations\MorphToMany
{
return $this->morphToMany(
News::class,
'newsable',
'newsables',
'newsable_id',
'news_id'
);
}
}

35
app/Models/Newsable.php Normal file
View file

@ -0,0 +1,35 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\MorphPivot;
/**
* @property integer $id Id of the news.
* @property \App\Models\News $news_id Name of the news.
* @property \App\Models\NewsCategory|\App\Models\NewsTag $newsable_id Name of the category or tag.
* @property class-string<static> $newsable_type Class of the category or tag.
* @property-read \Carbon\Carbon $created_at Created date of the news.
* @property-read \Carbon\Carbon $updated_at Updated date of the news.
*/
class Newsable extends MorphPivot
{
/**
* The table associated with the model.
*
* @var string|null
*/
protected $table = 'newsables';
/**
* Perform any actions required after the model boots.
*
* @return void
*/
protected static function booted(): void
{
static::created(fn() => News::resetCache());
static::updated(fn() => News::resetCache());
static::deleted(fn() => News::resetCache());
}
}

View file

@ -113,51 +113,21 @@ class PageConfig extends Model implements TranslatableModel
TranslationHelper::validateFallbackTranslations($model);
}
/**
* Check if the config is updated.
*
* @param array $newArray
* @param array $originalArray
* @return boolean
*/
private static function isConfigUpdated(array $newArray, array $originalArray): bool
{
if (count($newArray) !== count($originalArray)) {
return false;
}
foreach ($newArray as $i => $subA) {
$subB = $originalArray[$i] ?? null;
if ($subB === null) {
return false;
}
ksort($subA);
ksort($subB);
if ($subA != $subB) {
return false;
}
}
return true;
}
/**
* Get the target model from a specific id and model name.
*
* @param string $modelId
* @param string $modelName
* @return \Illuminate\Database\Eloquent\Model
* @return \Illuminate\Database\Eloquent\Model|null
*/
public static function getModelFromInputModelConfig(
string $modelId,
string $modelName,
): \Illuminate\Database\Eloquent\Model {
): ?\Illuminate\Database\Eloquent\Model {
$studly = Str::studly(Str::singular($modelName));
$class = "App\\Models\\{$studly}";
if (!$class || !class_exists($class)) {
if (!class_exists($class)) {
return null;
}

View file

@ -53,6 +53,7 @@ class DynamicPageRouteServiceProvider extends ServiceProvider
$slug = $dynamicPageModel->getTranslation('slug', $locale);
// If no slug for the locale → skip.
/** @phpstan-ignore-next-line */
if (!empty($slug) && !$slug) {
continue;
}

187
app/Rules/Video.php Normal file
View file

@ -0,0 +1,187 @@
<?php
namespace App\Rules;
use App\Enums\VideoProviderEnum;
use Illuminate\Contracts\Validation\DataAwareRule;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Arr;
use Uwil\LaravelCommon\Rules\IsValidVimeoId;
use Uwil\LaravelCommon\Rules\IsValidYoutubeId;
/**
* Check the length of a Wysiwyg field, excluding tags and end spaces.
*
* @package Uwil\LaravelCommon\Rules
*/
class Video implements ValidationRule, DataAwareRule
{
protected array $data = [];
public function setData(array $data): static
{
$this->data = $data;
return $this;
}
/**
* Construct method.
*
* @param integer $min The minimum length of the content (excluding tags). Default is 15.
* @param integer|null $max The maximum length of the content (excluding tags).
* Default is null (max will be defined by the type of the column in DB).
* @return void
* @throws \InvalidArgumentException If $max < $min.
*/
public function __construct() {}
/**
* Run the validation rule.
*
* @param string $attribute
* @param mixed $value
* @param \Closure $fail
*
* @return void
*/
public function validate(string $attribute, mixed $value, \Closure $fail): void
{
$lengthRules = [
$attribute . '.alt' => ['min' => 3, 'max' => 255],
$attribute . '.caption' => ['min' => 3, 'max' => 255],
];
if (!$value) {
return;
}
$fields = [
$attribute . '.alt',
$attribute . '.caption',
$attribute . '.isMuted',
$attribute . '.hasControls',
$attribute . '.canFullScreen',
$attribute . '.provider',
$attribute . '.videoId',
$attribute . '.format',
];
foreach ($fields as $field) {
$fieldValue = Arr::get($this->data, $field);
if (in_array($field, [
$attribute . '.alt',
$attribute . '.caption'
])) {
$lengthRule = $lengthRules[$field] ?? null;
if ($lengthRule) {
$min = $lengthRule['min'] ?? 0;
$max = $lengthRule['max'] ?? PHP_INT_MAX;
if ($fieldValue === null && $field === $attribute . '.caption') {
continue;
}
if ($fieldValue === null && $field === $attribute . '.title') {
continue;
}
if (strlen($fieldValue) < $min) {
$fail(__('validation.min.string', [
'attribute' => self::getTranslatedAttribute($field),
'min' => $min
]));
return;
}
if (strlen($fieldValue) > $max) {
$fail(__('validation.max.string', [
'attribute' => self::getTranslatedAttribute($field),
'max' => $max
]));
return;
}
}
}
// Boolean fields
if (in_array($field, [$attribute . '.isMuted', $attribute . '.hasControls', $attribute . '.canFullScreen'])) {
$fieldValue = (bool)$fieldValue;
if (!is_bool($fieldValue)) {
$fail(__('validation.boolean', [
'attribute' => self::getTranslatedAttribute($field),
]));
return;
}
}
if($field === $attribute . '.format') {
if($fieldValue != "landscape" && $fieldValue != "portrait") {
$fail(__('validation.custom.format', [
'attribute' => self::getTranslatedAttribute($field),
]));
return;
}
}
if ($field === $attribute . '.provider') {
$fieldValue = (int) $fieldValue;
if (!is_int($fieldValue)) {
$fail(__('validation.integer', [
'attribute' => self::getTranslatedAttribute($field),
]));
return;
}
$enumValues = array_map(fn($case) => $case->value, VideoProviderEnum::cases());
if (!in_array($fieldValue, $enumValues)) {
$fail(__('validation.in', [
'attribute' => self::getTranslatedAttribute($field),
]));
return;
}
if ($field === $attribute . '.videoId') {
$provider = Arr::get($this->data, $attribute . '.provider');
if ($provider === \App\Enums\VideoProviderEnum::vimeo->value) {
$rule = new IsValidVimeoId();
} elseif ($provider === \App\Enums\VideoProviderEnum::youtube->value) {
$rule = new IsValidYoutubeId();
} else {
$fail(__('validation.invalid', [
'attribute' => self::getTranslatedAttribute($attribute . '.provider'),
]));
return;
}
$rule->validate($field, $fieldValue, $fail);
}
}
}
}
/**
* Get the correct translation for the given attrribute.
*
* @param string $attribute
* @return string
*/
private static function getTranslatedAttribute(string $attribute): string
{
$parts = explode('.', $attribute);
if (count($parts) > 1) {
$parts[0] = 'video_';
$attribute = implode('.', $parts);
}
/** @var \Illuminate\Translation\Translator $trans */
$trans = trans();
if ($trans->has('validation.attributes.' . $attribute)) {
return trans('validation.attributes.' . $attribute);
}
if ($trans->has('validation.custom.' . $attribute)) {
return trans('validation.custom.' . $attribute);
}
return $attribute;
}
}

View file

@ -28,10 +28,13 @@ trait HasCache
static::deleted(function () {
self::resetCache();
});
/** @phpstan-ignore-next-line */
if (method_exists(static::class, 'initializeSoftDeletes')) {
/** @phpstan-ignore-next-line */
static::restored(function () {
self::resetCache();
});
/** @phpstan-ignore-next-line */
static::forceDeleted(function () {
self::resetCache();
});
@ -57,11 +60,12 @@ trait HasCache
public static function getModelsCached(): \Illuminate\Database\Eloquent\Collection
{
return Cache::remember(static::getCacheKey(), 86400, function () {
/** @var class-string<static> $class */
$class = static::class;
/** @var \Illuminate\Database\Eloquent\Model $class */
/** @phpstan-ignore-next-line */
return $class::query()
->when(
/** @phpstan-ignore-next-line */
property_exists($class, 'cacheRelations') && is_array($class::$cacheRelations),
function ($query) use ($class) {
$query->with($class::$cacheRelations);
@ -76,8 +80,8 @@ trait HasCache
*
* @return string
*/
private static function getCacheKey(): string
public static function getCacheKey(): string
{
return 'cache_' . str_replace('\\', '_', static::class);
return sprintf("cache_%s", str_replace('\\', '_', static::class));
}
}

View file

@ -42,9 +42,11 @@ trait HasUwilImages
*/
protected static function bootHasUwilImages(): void
{
/** @phpstan-ignore-next-line */
$hadSoftDelete = method_exists(static::class, 'initializeSoftDeletes');
if ($hadSoftDelete) {
/** @phpstan-ignore-next-line */
static::forceDeleted(function (self $model) {
self::initLoop($model, self::FORCE_DELETE_ACTION);
});
@ -134,9 +136,8 @@ trait HasUwilImages
continue;
} //end if
// PAS TESTE.
// TODO pas testé
if (is_array($originalValue) && is_array($currentValue)) {
dd('juste array');
static::traverseAndManage(
$model,
$originalValue,
@ -168,7 +169,7 @@ trait HasUwilImages
$forceDeleteImage = $eventType === self::FORCE_DELETE_ACTION;
$imagesFields = self::getImageFields($model);
$wysiwygFields = self::getWysiwygFields($model);
$isTranslatable = method_exists($model, 'getTranslations') ?? false;
$isTranslatable = method_exists($model, 'getTranslations');
$locale = config('app.locale');
$availableLocales = array_keys(config('laravellocalization.supportedLocales', ['fr' => "Frenchouz"]));
@ -230,7 +231,6 @@ trait HasUwilImages
$path = $isList ? "$fieldPath.$key" : (is_int($key) ? $fieldPath : "$fieldPath.$key");
// TODO cette récursivité n'a pas encore été testé.
if (is_array($oldValue) || is_array($newValue)) {
dd('pas testé');
static::traverseAndManage(
$model,
(array) $oldValue,

View file

@ -14,7 +14,7 @@ trait HasLaravelFailedValidation
* @return void
* @throws \Illuminate\Http\Exceptions\HttpResponseException Override ajax code error.
*/
protected function failedValidation(Validator $validator): \Illuminate\Http\Exceptions\HttpResponseException
protected function failedValidation(Validator $validator): void
{
$response = response()->json([
'errors' => $validator->errors(),

View file

@ -13,7 +13,7 @@ trait HasInertiaPicture
*/
protected function mergeInertiaPicture(): void
{
if (empty($this->convertedFiles) || !is_array($this->convertedFiles)) {
if (empty($this->convertedFiles)) {
return;
}
$original = $this->all();
@ -47,7 +47,7 @@ trait HasInertiaPicture
* Set picture rules (UploadedFile or string path).
*
* @param string $rules
* @return string
* @return array<mixed>
*/
protected function pictureRules(string $rules): array
{

View file

@ -9,7 +9,7 @@ use Illuminate\View\Component;
class NewsNextEventComponent extends Component
{
/** @var array<\App\Models\News> $nextEvents List of published news. */
/** @var \Illuminate\Support\Collection<int, \App\Models\News> $nextEvents List of published news. */
public $nextEvents;
/**
@ -17,12 +17,15 @@ class NewsNextEventComponent extends Component
*/
public function __construct()
{
/** @var \App\Models\NewsCategory|null $newsCategory */
$newsCategory = NewsCategory::getModelsCached()
->filter(fn($category) => $category->getTranslation('slug', 'fr') === "actualites")
->first();
/** @var \App\Models\NewsTag|null $eventTag */
$eventTag = NewsTag::getTagsByCategoryInCurrentLocale($newsCategory->slug)
->filter(fn($tag) => $tag->getTranslation('slug', 'fr') === "evenement")
->filter(
fn(\Illuminate\Database\Eloquent\Model $tag) => $tag->getTranslation('slug', 'fr') === "actualites-rh"
)
->first();
$eventTagId = $eventTag ? $eventTag->id : null;
@ -30,7 +33,7 @@ class NewsNextEventComponent extends Component
$this->nextEvents = News::getPublishedNewsByCategoryInCurrentLocale($newsCategory->slug)
->filter(function ($news) use ($eventTagId) {
return $news->tags->contains(
fn ($tag) => $eventTagId === $tag->id
fn($tag) => $eventTagId === $tag->id
);
})
->sortByDesc('published_at')

View file

@ -11,13 +11,13 @@ class OurImplantations extends Component
/** @var string $id Identification of the section. */
public string $id;
/** @var $implantations Content section. */
/** @var array Content section. */
public $implantations;
/** @var array<\App\Models\Company> $companyModels List of published companies. */
/** @var \Illuminate\Database\Eloquent\Collection $companyModels List of published companies. */
public $companyModels;
/** @var array<\App\Models\CompanyActivity> $companyActivityModels List of published company activities. */
/** @var \Illuminate\Database\Eloquent\Collection $companyActivityModels List of published company activities. */
public $companyActivityModels;
/**

View file

@ -14,6 +14,7 @@ return Application::configure(basePath: dirname(__DIR__))
__DIR__ . '/../routes/web-front.php',
__DIR__ . '/../routes/web-back.php',
],
api: __DIR__.'/../routes/api.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {

1
commitlint.config.cjs Normal file
View file

@ -0,0 +1 @@
module.exports = require('@uwil/husky-config/commitlint.config.cjs');

View file

@ -24,10 +24,12 @@
"sentry/sentry-laravel": "^4.18",
"spatie/laravel-translatable": "^6.11",
"tightenco/ziggy": "^2.5",
"uwil/admin-secure-laravel": "^5.0",
"uwil/admin-secure-laravel": "^5.1",
"uwil/k8s-laravel": "^2.0",
"uwil/laravel-backend": "^3.0",
"uwil/laravel-common": "^4.1"
"uwil/laravel-common": "^4.1",
"uwil/sitemap-generator": "^6.0",
"verschuur/laravel-robotstxt": "*"
},
"require-dev": {
"fakerphp/faker": "^1.23",

1234
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -14,7 +14,7 @@ return [
*
*/
'default' => env('MAIL_MAILER', 'log'),
'default' => env('MAIL_MAILER', 'smtp'),
/**
*--------------------------------------------------------------------------
@ -114,4 +114,14 @@ return [
'name' => env('MAIL_FROM_NAME', 'Example'),
],
'to' => [
'breeder' => explode(',', env('MAIL_LIST_BREEDER')) ?? [],
'commitments' => explode(',', env('MAIL_LIST_COMMITMENTS')) ?? [],
'export' => explode(',', env('MAIL_LIST_EXPORT')) ?? [],
'finance' => explode(',', env('MAIL_LIST_FINANCE')) ?? [],
'info' => explode(',', env('MAIL_LIST_INFO')) ?? [],
'press' => explode(',', env('MAIL_LIST_PRESS')) ?? [],
'restauration' => explode(',', env('MAIL_LIST_RESTAURATION')) ?? [],
]
];

29
config/robots-txt.php Normal file
View file

@ -0,0 +1,29 @@
<?php
return [
'environments' => [
'production' => [
'paths' => [
'*' => [
'disallow' => [
'/admin',
'*.pdf$'
],
'allow' => []
],
],
'sitemaps' => [
'sitemap.xml'
]
],
'staging' => [
'paths' => [
'*' => [
'disallow' => [
'/'
]
]
]
]
]
];

View file

@ -20,7 +20,7 @@ return [
|
*/
'driver' => env('SCOUT_DRIVER', 'meilisearch'),
'driver' => env('SCOUT_DRIVER', null),
/*
|--------------------------------------------------------------------------

View file

@ -37,5 +37,14 @@ return [
'google' => [
'maps' => env('GOOGLE_MAPS_KEY'),
'recaptcha' => [
'public' => env('GOOGLE_RECAPTCHA_PUBLIC'),
'private' => env('GOOGLE_RECAPTCHA_PRIVATE'),
'version' => env('GOOGLE_RECAPTCHA_VERSION', "3") === "3" ? true : false
]
],
'matomo' => [
'site_id' => env('MATOMO_SITE_ID')
],
];

View file

@ -0,0 +1,74 @@
<?php
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;
return [
/**
* Single route names
*
* Ex:
*
* 'singlePages' => [
* 'homepage',
* ],
*/
'singlePages' => [
'front.search',
],
/**
* Model namespaces with filters
*
* Ex: (modelFilters & relationFilters are not necessaries)
* 'cruds' => [
* [
* 'model' => 'App\Models\Product',
* 'modelFilters' => [
* ['published', '=', true]
* ],
* 'relationFilters' => [
* 'range' => fn(Builder $q) => $q->where('status', true),
* ],
* ],
* [
* 'model' => 'App\Models\Article',
* ]
* ],
*/
'cruds' => [
[
'model' => 'App\Models\News',
'filters' => [
['status', '=', \App\Enums\News\NewsStatusEnum::published]
],
],
[
'model' => 'App\Models\DynamicPage',
'filters' => [
['status', '=', \App\Enums\DynamicPages\PageStatusEnum::published]
],
],
],
/**
* Enable or disable schedule for the job
* If set to true the sitemap generator will be run
* using the 'cron' instruction
*/
'schedule' => true,
/**
* If you want to set specific
* instruction for when to schedule job
* you can set this with a cron instruction
* You can generate one using https://crontab.guru/
*
* schedule must be set to true
*
* Default is every day at 15:45
*/
'cron' => '45 15 * * *',
'supported_locales' => array_keys(config('laravellocalization.supportedLocales', ['fr' => "Fr"]))
];

View file

@ -192,7 +192,7 @@ return [
"delete" => "delete the color",
],
"wysiwyg" => [
"undo" => "Undo",
"undo" => "Undo",
"redo" => "Redo",
"bold" => "Bold",
"italic" => "Italic",
@ -371,4 +371,40 @@ return [
"select_value" => "select a value from the list",
],
"associated_model" => "associated :model|associated :model",
"input_video" => [
"placeholder" => "Click to add a new video",
"title" => "Insert an video",
"subtitle" => "Add a new video to your content.",
"insert" => "Insert",
"cancel" => "Cancel",
"tooltip_cancel" => "Cancel adding this video",
"edit_title" => "Update a video",
"edit_subtitle" => "Edit the attributes associated with the video.",
"edit_apply" => "Apply",
"edit_cancel" => "Cancel",
"position" => "Image position",
"format" => "Image format",
"video" => "Image",
"alt" => "ALT attribute of the video",
"caption" => "Caption of the video",
"has_caption" => "Presence of a legend",
"landscape" => "Landscape",
"portrait" => "Portrait",
"square" => "Square",
"align-left" => "Align left",
"align-center" => "Align center",
"align-right" => "Align right",
"wrap-left" => "Wrap left",
"wrap-right" => "Wrap right",
"youtube" => "Youtube",
"vimeo" => "Vimeo",
"tooltip" => [
"title" => "video",
"delete" => "delete the video",
"reset" => "reset to original state",
"insert" => "add this video",
"edit_cancel" => "Cancel updating the video attributes",
"edit_apply" => "Update the video attributes",
]
]
];

View file

@ -31,47 +31,7 @@ return [
'model' => 'Model',
'staticroute' => "Static route",
'position' => 'Element position',
],
'contactFormType' => [
'collaborator' => [
'label' => 'join us as an employee',
'slug' => 'join-us-as-an-employee'
],
'breeder' => [
'label' => 'join us as a farmer',
'slug' => 'join-us-as-a-farmer'
],
'professional' => [
'label' => 'a request as a professional'
],
'restauration' => [
'label' => 'foodservice, Industry, Retailer France & Overseas',
'slug' => 'foodservice-industry-retailer-distributor',
'subtitle' => 'a request as a customer / retailer / distributor'
],
'export' => [
'label' => 'other countries / Export',
'slug' => 'export'
],
'finance' => [
'label' => 'finance',
'slug' => 'finance-2' #FIXME: revoir le slug
],
'commitments' => [
'label' => 'our commitments',
'slug' => 'our-commitments-2' #FIXME: revoir le slug
],
'product' => [
'label' => 'product information',
'slug' => 'product-information'
],
'info' => [
'label' => 'other general information',
'slug' => 'other-general-information'
],
'press' => [
'label' => 'a press request'
],
'contactform' => 'Contact form type',
],
'breedType' => [
'chicken' => 'Chickens / turkeys / guinea fowl',
@ -128,4 +88,25 @@ return [
'asc' => 'From oldest to newest.',
'desc' => 'From the most recent to the oldest.'
],
'exportAskingType' => [
"customer" => "Customer",
"provider" => "Supplier",
"other" => "Other"
],
'contactFormType' => [
'collaborator' => 'join us as an employee',
'breeder' => 'join us as a farmer',
'professional' => 'a request as a professional',
'restauration' => 'foodservice, Industry, Retailer France & Overseas',
'export' => 'other countries / Export',
'finance' => 'finance',
'commitments' => 'our commitments',
'product' => 'product information',
'info' => 'other general information',
'press' => 'a press request',
],
"videoDisplayFormat" => [
"landscape" => "landscape",
"portrait" => "portrait"
],
];

View file

@ -145,5 +145,11 @@ return [
'subtitle' => 'jobs, internships and apprenticeships',
'link_text' => 'access our recruitment portal'
],
'discover_our_jobs' => 'discover our broad range of jobs'
'discover_our_jobs' => 'discover our broad range of jobs',
'newsletter' => [
'title' => 'Subscribe to our newsletter !',
'placeholder' => 'Enter your email',
'validate' => 'Confirm',
'policy_message' => 'For any additional information regarding your personal data and your rights, please consult our '
],
];

View file

@ -2,7 +2,7 @@
return [
"classes" => [
"user" => "user",
"user" => "user|users",
"dynamic-page" => "dynamic page|dynamic pages",
"page-config" => "page configuration|page configurations",
"activity-log" => "activity log",

View file

@ -1,8 +1,8 @@
<?php
return [
'contact' => 'contact-us/{slug}',
'contact_index' => 'contact-us',
'news' => 'news/{news}',
'search' => 'search',
'front' => [
'news' => 'news/{news}',
'search' => 'search'
],
];

View file

@ -271,6 +271,15 @@ return [
"value" => "value",
"path" => "path",
"issuu_path" => "link of the Issuu preview",
"is_issuu" => "is an Issuu preview ?"
"is_issuu" => "is an Issuu preview ?",
"video" => "vidéo",
"video_" => [
"isMuted" => "muted",
"hasControls" => "controls enabled",
"canFullScreen" => "full screen allowed",
"alt" => "alternative text",
"title" => "title",
"caption" => "caption"
]
],
];

View file

@ -372,4 +372,45 @@ return [
"select_value" => "sélectionnez une valeur dans la liste",
],
"associated_model" => ":model associés|:model associées",
"input_video" => [
"placeholder" => "Cliquer pour ajouter une nouvelle vidéo",
"title" => "Insérer une vidéo",
"subtitle" => "Ajouter une nouvelle vidéo à votre contenu.",
"insert" => "Insérer",
"cancel" => "Annuler",
"edit_title" => "Mise à jour d'une vidéo",
"edit_subtitle" => "Modifier les attributs associés à la vidéo.",
"edit_apply" => "Appliquer",
"edit_cancel" => "Annuler",
"position" => "Position de la vidéo",
"format" => "Format de la vidéo",
"image" => "Vidéo",
"alt" => "Texte alternatif de la vidéo",
"caption" => "Légende de la vidéo",
"has_caption" => "Présence d'une légende",
"landscape" => "Paysage",
"portrait" => "Portrait",
"align-left" => "Aligner à gauche",
"align-center" => "Aligner au centre",
"align-right" => "Aligner à droite",
"wrap-left" => "Flottant à gauche",
"wrap-right" => "Flottant à droite",
"youtube" => "Youtube",
"vimeo" => "Viméo",
"is_muted" => "muter la vidéo ?",
"has_controls" => "permettre de controler la vidéo ?",
"can_fullScreen" => "autoriser le plein écran ?",
"provider" => "hébergeur de la vidéo",
'invalid_youtube_id' => "vidéo YOUTUBE invalide",
"invalid_vimeo_id" => "vidéo VIMEO invalide",
"tooltip" => [
"title" => "vidéo",
"delete" => "supprimer la vidéo",
"reset" => "remettre à l'état d'origine",
"insert" => "Ajouter cette vidéo",
"cancel" => "Annuler l'ajout de cette vidéo",
"edit_apply" => "Mettre à jour les attributs de la vidéo",
"edit_cancel" => "Annuler la mise à jour des attributs de la vidéo",
]
]
];

View file

@ -31,47 +31,7 @@ return [
'model' => 'Modèle',
'staticroute' => "Route statique",
'position' => 'Positionnement de l\'élement',
],
'contactFormType' => [
'collaborator' => [
'label' => 'nous rejoindre en tant que collaborateur',
'slug' => 'nous-rejoindre-en-tant-que-collaborateur'
],
'breeder' => [
'label' => 'nous rejoindre en tant qu’éleveur',
'slug' => 'nous-rejoindre-en-tant-queleveur'
],
'professional' => [
'label' => 'une demande en tant que professionnel'
],
'restauration' => [
'label' => 'restaurations, Distributeurs et Détaillants France',
'subtitle' => 'une demande en tant que client / détaillant / distributeur',
'slug' => 'une-demande-en-tant-que-client-detaillant-distributeur'
],
'export' => [
'label' => 'l’export',
'slug' => 'lexport'
],
'finance' => [
'label' => 'la finance',
'slug' => 'la-finance'
],
'commitments' => [
'label' => 'nos engagements',
'slug' => 'nos-engagements'
],
'product' => [
'label' => 'une information produit',
'slug' => 'une-information-produit'
],
'info' => [
'label' => 'une autre information générale',
'slug' => 'une-autre-information-generale'
],
'press' => [
'label' => 'une demande presse'
],
'contactform' => 'Type de formulaire de contact',
],
'breedType' => [
'chicken' => 'Poulets/dindes/pintades',
@ -128,4 +88,25 @@ return [
'asc' => 'du plus ancien au plus récent.',
'desc' => 'du plus récent au plus ancien.'
],
'exportAskingType' => [
"customer" => "Client",
"provider" => "Fournisseur",
"other" => "Autres"
],
'contactFormType' => [
'collaborator' => 'nous rejoindre en tant que collaborateur',
'breeder' => 'nous rejoindre en tant qu’éleveur',
'professional' => 'une demande en tant que professionnel',
'restauration' => 'restaurations, Distributeurs et Détaillants France',
'export' => 'l’export',
'finance' => 'la finance',
'commitments' => 'nos engagements',
'product' => 'une information produit',
'info' => 'une autre information générale',
'press' => 'une demande presse',
],
"videoDisplayFormat" => [
"landscape" => "paysage",
"portrait" => "portrait"
],
];

View file

@ -146,5 +146,11 @@ return [
'subtitle' => 'd\'emploi, de stage et d\'alternance',
'link_text' => 'accédez à notre portail de recrutement'
],
'discover_our_jobs' => 'découvrez nos métiers'
'discover_our_jobs' => 'découvrez nos métiers',
'newsletter' => [
'title' => 'Inscrivez-vous à notre newsletter !',
'placeholder' => 'Renseignez votre email',
'validate' => 'Valider',
'policy_message' => 'Pour toute information complémentaire concernant vos données personnelles et vos droits, merci de consulter notre'
],
];

View file

@ -2,7 +2,7 @@
return [
"classes" => [
"user" => "utilisateur",
"user" => "utilisateur|utilisateurs",
"dynamic-page" => "page dynamique|pages dynamiques",
"page-config" => "configuration de page|configurations de page",
"activity-log" => "journal d'activité",

View file

@ -1,8 +1,8 @@
<?php
return [
'contact' => 'nous-contacter/{slug}',
'contact_index' => 'nous-contacter',
'news' => 'actualites/{news}',
'search' => 'recherche',
'front' => [
'news' => 'actualites/{news}',
'search' => 'recherche'
],
];

View file

@ -270,6 +270,17 @@ return [
"value" => "valeur",
"path" => "chemin d'accès",
"issuu_path" => "lien du fichier Issuu",
"is_issuu" => "est-ce un fichier Issuu ?"
"is_issuu" => "est-ce un fichier Issuu ?",
"video" => "vidéo",
"video_" => [
"isMuted" => "désactiver le son",
"hasControls" => "activer les contrôles",
"canFullScreen" => "autoriser le plein écran",
"alt" => "texte alernatif",
"title" => "titre",
"caption" => "légende",
"provider" => "hébergeur",
"videoId" => "ID de la vidéo"
]
],
];

View file

@ -7,7 +7,7 @@ data:
APP_ENV: "${APP_APP_ENV}"
APP_KEY: "${APP_APP_KEY}"
APP_DEBUG: "${APP_APP_DEBUG}"
APP_URL: "https://${APP_DOMAIN}"
APP_URL: "https://${REDIRECT}${APP_DOMAIN}"
LOG_CHANNEL: "k8s"
CACHE_STORE: "${APP_CACHE_STORE}"
DB_HOST: "${APP_DB_HOST}"
@ -23,6 +23,13 @@ data:
MAIL_ENCRYPTION: "${APP_MAIL_ENCRYPTION}"
MAIL_FROM_ADDRESS: "${APP_MAIL_FROM_ADDRESS}"
MAIL_FROM_NAME: "${APP_APP_NAME}"
MAIL_LIST_BREEDER: "${APP_MAIL_LIST_BREEDER}"
MAIL_LIST_COMMITMENTS: "${APP_MAIL_LIST_COMMITMENTS}"
MAIL_LIST_EXPORT: "${APP_MAIL_LIST_EXPORT}"
MAIL_LIST_FINANCE: "${APP_MAIL_LIST_FINANCE}"
MAIL_LIST_INFO: "${APP_MAIL_LIST_INFO}"
MAIL_LIST_PRESS: "${APP_MAIL_LIST_PRESS}"
MAIL_LIST_RESTAURATION: "${APP_MAIL_LIST_RESTAURATION}"
SENTRY_LARAVEL_DSN: "${APP_SENTRY_LARAVEL_DSN}"
TELESCOPE_ENABLED: "${APP_TELESCOPE_ENABLED}"
CRM_ENDPOINT: "${APP_CRM_ENDPOINT}"
@ -56,6 +63,18 @@ spec:
requests:
storage: $APP_PVC_SIZE
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ${CI_PROJECT_PATH_SLUG}-wp-uploads-pvc-storage
spec:
storageClassName: "csi-cinder-classic"
accessModes:
- ReadWriteOnce
resources:
requests:
storage: $APP_WP_UPLOADS_PVC_SIZE
---
apiVersion: apps/v1
kind: Deployment
metadata:
@ -86,6 +105,8 @@ spec:
volumeMounts:
- name: storage
mountPath: /app/srcs/storage
- name: wp-uploads-storage
mountPath: /app/srcs/public/app/uploads
- name: ${CI_PROJECT_PATH_SLUG}-queue-worker
image: $IMAGE_TAG
lifecycle:
@ -120,6 +141,9 @@ spec:
- name: storage
persistentVolumeClaim:
claimName: ${CI_PROJECT_PATH_SLUG}-pvc-storage
- name: wp-uploads-storage
persistentVolumeClaim:
claimName: ${CI_PROJECT_PATH_SLUG}-wp-uploads-pvc-storage
---
apiVersion: v1
kind: Service
@ -141,14 +165,29 @@ spec:
- web
- websecure
routes:
- match: Host(`${APP_DOMAIN}`)
- match: Host(`${APP_DOMAIN}`) || Host(`www.${APP_DOMAIN}`)
kind: Rule
services:
- name: ${CI_PROJECT_PATH_SLUG}-service
port: 80
middlewares:
- name: redirect-no-www-to-www
- name: redirect-ldc-rgpd-to-rgpd
tls:
secretName: ${CRT_TLS_NAME}
---
# Redirect with domain replacement
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: redirect-ldc-rgpd-to-rgpd
spec:
redirectRegex:
regex: ^https://${REDIRECT}${APP_DOMAIN}/rgpd/(professionnels|salaries|consommateurs)(/(en|de))?
replacement: https://rgpd.ldc.fr/fr/${1}
permanent: true
---
apiVersion: batch/v1
kind: CronJob
metadata:

645
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -33,7 +33,6 @@
"@typescript-eslint/parser": "^8.39.0",
"@vitejs/plugin-react": "^4.7.0",
"autoprefixer": "^10.4.21",
"baseline-browser-mapping": "^2.9.19",
"concurrently": "^9.2.0",
"eslint": "^8.57.1",
"eslint-config-airbnb": "^19.0.4",
@ -104,6 +103,7 @@
"@types/pluralize": "^0.0.33",
"@types/react": "^19.1.9",
"@types/react-dom": "^19.1.7",
"@uwil/husky-config": "^1.0.1",
"axios": "^1.13.2",
"chart.js": "^4.5.1",
"class-variance-authority": "^0.7.1",

View file

@ -5,4 +5,4 @@ parameters:
paths:
- app/
level: 5
level: 7

View file

@ -5,6 +5,7 @@ metadata:
spec:
dnsNames:
- $APP_DOMAIN
- www.${APP_DOMAIN}
secretName: $APP_DOMAIN
issuerRef:
name: $APP_CERT_ISSUER

View file

@ -18,8 +18,182 @@
RewriteCond %{REQUEST_URI} (.+)/$
RewriteRule ^ %1 [L,R=301]
# Redirect /index.php/anything to /anything
RewriteCond %{THE_REQUEST} /index\.php/([^\s?]*) [NC]
RewriteRule ^index\.php/(.*)$ /$1 [L,R=301,NC]
# Send Requests To Front Controller...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]
# Redirection from old website
Redirect 301 "/category/news" "/actualites"
Redirect 301 "/100%des-elevages-nature-deleveurs-dici-a-2025" "/actualites/100-des-elevages-nature-deleveurs-dici-a-2025"
Redirect 301 "/acquisition-de-la-societe-asia-general-food" "/actualites/acquisition-de-la-societe-asia-general-food"
Redirect 301 "/big-bang-de-lemploi-2023-rencontrons-nous-dans-des-conditions-exceptionnelles" "/actualites/big-bang-de-lemploi-2023-rencontrons-nous-dans-des-conditions-exceptionnelles"
Redirect 301 "/bonne-annee-2024-a-tous" "/actualites/bonne-annee-2024-a-tous"
Redirect 301 "/decouvrez-notre-nouvelle-plaquette-rse" "/actualites/decouvrez-notre-nouvelle-plaquette-rse"
Redirect 301 "/decouvrez-notre-rapport-annuel-2020-2021" "/actualites/decouvrez-notre-rapport-annuel-2020-2021"
Redirect 301 "/decouvrez-notre-rapport-annuel-2021-2022" "/actualites/decouvrez-notre-rapport-annuel-2021-2022"
Redirect 301 "/echanges-entre-les-etudiants-de-lessca-et-le-raf-du-pole-volaille-ldc" "/actualites/echanges-entre-les-etudiants-de-lessca-et-le-raf-du-pole-volaille-ldc"
Redirect 301 "/echanges-entre-les-etudiants-unilasalle-et-des-collaborateurs-ldc" "/actualites/echanges-entre-les-etudiants-unilasalle-et-des-collaborateurs-ldc"
Redirect 301 "/entree-en-negociation-exclusive-en-vue-de-lacquisition-de-la-societe-indykpol-sa-2" "/actualites/entree-en-negociation-exclusive-en-vue-de-lacquisition-de-la-societe-indykpol-sa"
Redirect 301 "/entree-en-negociation-exclusive-en-vue-de-lacquisition-de-la-societe-indykpol-sa-3" "/actualites/entree-en-negociation-exclusive-en-vue-de-lacquisition-de-la-societe-indykpol-sa"
Redirect 301 "/entree-en-negociation-exclusive-en-vue-de-lacquisition-de-lusine-konspol-de-nowy-sacz-en-pologne-2" "/actualites/entree-en-negociation-exclusive-en-vue-de-lacquisition-de-lusine-konspol-de-nowy-sacz-en-pologne"
Redirect 301 "/entree-en-negociation-exclusive-en-vue-de-lacquisition-potentielle-de-100-du-capital-du-groupe-pierre-martinet-2" "/actualites/entree-en-negociation-exclusive-en-vue-de-lacquisition-potentielle-de-100-du-capital-du-groupe-pierre-martinet"
Redirect 301 "/entree-en-negociation-exclusive-en-vue-de-lacquisition-potentielle-de-100-du-capital-du-groupe-pierre-martinet-3" "/actualites/entree-en-negociation-exclusive-en-vue-de-lacquisition-potentielle-de-100-du-capital-du-groupe-pierre-martinet"
Redirect 301 "/entree-en-partenariat-avec-la-famille-buchanan-au-royaume-uni-2" "/actualites/entree-en-partenariat-avec-la-famille-buchanan-au-royaume-uni"
Redirect 301 "/feu-vert-de-lautorite-de-la-concurrence-au-rachat-du-groupe-ronsard-2" "/actualites/feu-vert-de-lautorite-de-la-concurrence-au-rachat-du-groupe-ronsard"
Redirect 301 "/feu-vert-de-lautorite-de-la-concurrence-au-rachat-du-groupe-ronsard-3" "/actualites/feu-vert-de-lautorite-de-la-concurrence-au-rachat-du-groupe-ronsard"
Redirect 301 "/interview-philippe-gelin-sur-la-chaine-youtube-meilleurtaux-placement" "/actualites/interview-philippe-gelin-sur-la-chaine-youtube-meilleurtaux-placement"
Redirect 301 "/journee-mondiale-de-leau" "/actualites/journee-mondiale-de-leau"
Redirect 301 "/journee-mondiale-du-recyclage-%e2%99%bb" "/actualites/journee-mondiale-du-recyclage"
Redirect 301 "/journee-portes-ouvertes-ldc-sable-le-1er-avril-2023" "/actualites/journee-portes-ouvertes-ldc-sable-le-1er-avril-2023"
Redirect 301 "/joyeux-nouvel-an-chinois" "/actualites/joyeux-nouvel-an-chinois"
Redirect 301 "/lancement-campagne-alternance-2024" "/actualites/lancement-campagne-alternance-2024"
Redirect 301 "/ldc-a-la-reconquete-du-marche-national" "/actualites/ldc-a-la-reconquete-du-marche-national"
Redirect 301 "/ldc-partage-sa-politique-alimentation-des-volailles" "/actualites/ldc-partage-sa-politique-dalimentation-durable-des-volailles"
Redirect 301 "/ldc-participe-a-la-semaine-nationale-de-lemploi-agroalimentaire-100-digital" "/actualites/ldc-participe-a-la-semaine-nationale-de-lemploi-agroalimentaire-100-digital"
Redirect 301 "/ldc-participe-au-campus-des-metiers-et-des-qualifications" "/actualites/ldc-participe-au-campus-des-metiers-et-des-qualifications"
Redirect 301 "/ldc-sable-lance-sa-premiere-formation-qualifiante-post-bac" "/actualites/ldc-sable-lance-sa-premiere-formation-qualifiante-post-bac"
Redirect 301 "/ldc-sable-le-gaulois-signe-un-partenariat-avec-coolplanet-pour-optimiser-ses-consommations-denergie-et-accelerer-sa-feuille-de-route-en-matiere-de-decarbonation" "/actualites/ldc-sable-le-gaulois-signe-un-partenariat-avec-coolplanet-pour-optimiser-ses-consommations-denergie-et-accelerer-sa-feuille-de-route-en-matiere-de-decarbonation"
Redirect 301 "/le-cavol-a-la-reconquete-de-la-rhd" "/actualites/le-cavol-a-la-reconquete-de-la-rhd"
Redirect 301 "/le-gaulois-maitre-coq-loue-il-est-le-champion-du-poulet-toutes-categories" "/actualites/le-gaulois-maitre-coq-loue-il-est-le-champion-du-poulet-toutes-categories"
Redirect 301 "/le-gaulois-professionnel-lance-son-site-internet" "/actualites/le-gaulois-professionnel-lance-son-site-internet"
Redirect 301 "/le-gaulois-signe-un-partenariat-avec-programme-malin" "/actualites/le-gaulois-signe-un-partenariat-avec-programme-malin"
Redirect 301 "/le-groupe-ldc-a-participe-au-forum-uni-la-salle-de-beauvais-le-7-octobre-2021" "/actualites/le-groupe-ldc-a-participe-au-forum-uni-la-salle-de-beauvais-le-7-octobre-2021"
Redirect 301 "/le-groupe-ldc-cree-un-partenariat-avec-citeo-pour-le-developpement-dune-filiere-de-recyclage-du-polystyrene-en-france" "/actualites/le-groupe-ldc-cree-un-partenariat-avec-citeo-pour-le-developpement-dune-filiere-de-recyclage-du-polystyrene-en-france"
Redirect 301 "/le-groupe-ldc-officialise-sa-strategie-climat-biodiversite-2030-%f0%9f%8c%8d" "/actualites/le-groupe-ldc-officialise-sa-strategie-climat-biodiversite-2030"
Redirect 301 "/le-groupe-ldc-participe-a-la-semaine-de-lagro-grand-ouest-du-12-au-20-avril" "/actualites/le-groupe-ldc-participe-a-la-semaine-de-lagro-grand-ouest-du-12-au-20-avril"
Redirect 301 "/le-groupe-ldc-participe-a-la-semaine-nationale-de-lemploi-agroalimentaire" "/actualites/le-groupe-ldc-participe-a-la-semaine-nationale-de-lemploi-agroalimentaire"
Redirect 301 "/anuga-2023" "/actualites/le-groupe-ldc-present-a-lanuga-2023"
Redirect 301 "/le-groupe-ldc-present-au-forum-oniris-le-18-novembre-2021" "/actualites/le-groupe-ldc-present-au-forum-oniris-le-18-novembre-2021"
Redirect 301 "/le-groupe-ldc-present-au-jobfest-du-mans-le-30-mars-2023" "/actualites/le-groupe-ldc-present-au-jobfest-du-mans-le-30-mars-2023"
Redirect 301 "/le-groupe-ldc-present-aux-forums-ecoles-de-lesa-angers-et-agrosup-dijon" "/actualites/le-groupe-ldc-present-aux-forums-ecoles-de-lesa-angers-et-agrosup-dijon"
Redirect 301 "/le-groupe-ldc-present-aux-forums-ecoles-de-linstitut-agro-dijon-esa-angers-oniris-nantes-esaip-angers" "/actualites/le-groupe-ldc-present-aux-forums-ecoles-de-linstitut-agro-dijon-esa-angers-oniris-nantes-esaip-angers"
Redirect 301 "/le-groupe-ldc-present-aux-forums-polytech-angers-et-agro-campus-rennes-le-18-octobre-2022" "/actualites/le-groupe-ldc-present-aux-forums-polytech-angers-et-agro-campus-rennes-le-18-octobre-2022"
Redirect 301 "/le-groupe-ldc-reagit-face-aux-nouvelles-attaques-de-l214" "/actualites/le-groupe-ldc-reagit-face-aux-nouvelles-attaques-de-l214"
Redirect 301 "/le-groupe-ldc-renforce-ses-engagements-et-son-objectif-soja-zero-deforestation" "/actualites/le-groupe-ldc-reagit-face-aux-nouvelles-attaques-de-l214"
Redirect 301 "/le-groupe-ldc-renforce-ses-engagements-et-son-objectif-soja-zero-deforestation-2" "/actualites/le-groupe-ldc-renforce-ses-engagements-et-son-objectif-soja-zero-deforestation"
Redirect 301 "/les-industries-de-la-transformation-des-volailles-offrent-une-diversite-de-metiers-et-de-nombreuses-opportunites-demploi" "/actualites/les-industries-de-la-transformation-des-volailles-offrent-une-diversite-de-metiers-et-de-nombreuses-opportunites-demploi"
Redirect 301 "/les-talents-de-la-sarthe-2019" "/actualites/les-talents-de-la-sarthe-2019"
Redirect 301 "/les-talents-de-la-sarthe-2019-2" "/actualites/les-talents-de-la-sarthe-2019"
Redirect 301 "/maitre-coq-devient-partenaire-officiel-dangers-sco-handball" "/actualites/maitre-coq-devient-partenaire-officiel-dangers-sco-handball"
Redirect 301 "/marie-surgeles-organise-ses-job-dating-en-entreprise" "/actualites/marie-surgeles-organise-ses-job-dating-en-entreprise"
Redirect 301 "/nos-equipes-volena-et-sbv-etaient-presentes-au-food-talent-du-cfia-expo-rennes-ce-14-mars" "/actualites/nos-equipes-volena-et-sbv-etaient-presentes-au-food-talent-du-cfia-expo-rennes-ce-14-mars"
Redirect 301 "/note-dinformation-deces-de-monsieur-chancereul-gerard" "/actualites/note-dinformation-deces-de-monsieur-chancereul-gerard"
Redirect 301 "/partenariat-ecole-les-etudiants-uni-la-salle-en-visite-chez-ldc-groupe" "/actualites/partenariat-ecole-les-etudiants-unilasalle-en-visite-chez-ldc-groupe"
Redirect 301 "/partenariat-ecole-visite-de-lesa-sur-lunite-dinde-ldc-sable" "/actualites/partenariat-ecole-visite-de-lesa-sur-lunite-dinde-ldc-sable"
Redirect 301 "/publication-de-notre-rapport-annuel-2022-2023" "/actualites/publication-de-notre-rapport-annuel-2022-2023"
Redirect 301 "/recrutement-en-alternance-plus-de-100-opportunites-dans-tous-les-metiers" "/actualites/recrutement-en-alternance-plus-de-100-opportunites-dans-tous-les-metiers"
Redirect 301 "/reprise-de-kiplama-en-belgique" "/actualites/reprise-de-kiplama-en-belgique"
Redirect 301 "/reprise-de-luche-tradition-volailles-a-luche-pringe-sarthe" "/actualites/reprise-de-luche-tradition-volailles-a-luche-pringe-sarthe"
Redirect 301 "/reprise-de-marnevall-en-hongrie" "/actualites/reprise-de-marnevall-en-hongrie"
Redirect 301 "/resultats-annuels-2021-2022-2" "/actualites/resultats-annuels-2021-2022"
Redirect 301 "/resultats-semestriels-2020-2021-3" "/actualites/resultats-semestriels-2020-2021"
Redirect 301 "/resultats-semestriels-2023-2024-2" "/actualites/resultats-semestriels-20232024"
Redirect 301 "/retour-en-video-sur-le-space-rennes-2024" "/actualites/retour-en-video-sur-le-space-rennes-2024"
Redirect 301 "/retour-en-video-sur-notre-evenement-interne-challenge-remy-lambert" "/actualites/retour-en-video-sur-notre-evenement-interne-challenge-remy-lambert"
Redirect 301 "/retrospective-2024-retrouvez-les-entreprises-qui-ont-rejoint-le-groupe-en-2024" "/actualites/retrospective-2024-retrouvez-les-entreprises-qui-ont-rejoint-le-groupe-en-2024"
Redirect 301 "/retrouvez-nous-aux-forums-ecoles-en-fevrier-2025" "/actualites/retrouvez-nous-aux-forums-ecoles-en-fevrier-2025"
Redirect 301 "/retrouvez-nous-aux-forums-ecoles-en-mars-2025" "/actualites/retrouvez-nous-aux-forums-ecoles-en-mars-2025"
Redirect 301 "/signature-de-conventions-avec-les-banques-alimentaires" "/actualites/signature-de-conventions-avec-les-banques-alimentaires"
Redirect 301 "/sirha-lyon-2025-le-groupe-ldc-et-ses-marques-restauration-seront-presents-du-23-au-27-janvier-2025" "/actualites/sirha-lyon-2025-le-groupe-ldc-et-ses-marques-restauration-seront-presents-du-23-au-27-janvier-2025"
Redirect 301 "/sponsoring-sportif-maitre-coq-et-yannick-bestaven" "/actualites/sponsoring-sportif-maitre-coq-et-yannick-bestaven"
Redirect 301 "/synthese-rse-du-groupe-ldc-2020" "/actualites/synthese-rse-du-groupe-ldc-2020"
Redirect 301 "/teasing-retrouvez-le-pole-amont-au-space-du-17-au-19-septembre-2024" "/actualites/teasing-retrouvez-le-pole-amont-au-space-du-17-au-19-septembre-2024"
Redirect 301 "/top-depart-pour-le-vendee-globe-avec-maitre-coq" "/actualites/top-depart-pour-le-vendee-globe-avec-maitre-coq"
Redirect 301 "/tous-derriere-yannick-et-julien-pour-la-transat-jacques-vabre" "/actualites/tous-derriere-yannick-et-julien-pour-la-transat-jacques-vabre"
Redirect 301 "/une-annee-de-plus-ou-nos-produits-ont-ete-recompenses-par-le-concours-general-agricole-2024" "/actualites/une-annee-de-plus-ou-nos-produits-ont-ete-recompenses-par-le-concours-general-agricole-2024"
Redirect 301 "/venez-nous-rencontrer-au-salon-work-in-agro-2023" "/actualites/venez-nous-rencontrer-au-salon-work-in-agro-2023"
Redirect 301 "/victoire-de-yannick-bestaven-et-son-maitre-coq-iv-de-la-9eme-edition-du-vendee-globe" "/actualites/victoire-de-yannick-bestaven-et-son-maitre-coq-iv-de-la-9eme-edition-du-vendee-globe"
Redirect 301 "/victoire-de-yannick-bestaven-et-son-maitre-coq-iv-de-la-9eme-edition-du-vendee-globe-2" "/actualites/victoire-de-yannick-bestaven-et-son-maitre-coq-iv-de-la-9eme-edition-du-vendee-globe"
Redirect 301 "/ldc-sable-lance-sa-premiere-formation-qualifiante-post-bac-pour-devenir-conducteur-de-ligne" "/actualites/video-ldc-sable-lance-sa-premiere-formation-qualifiante-post-bac-pour-devenir-conducteur-de-ligne"
Redirect 301 "/volena-volaillers-engages-pour-lavenir-le-nouveau-nom-du-pole-normandie" "/actualites/volena-volaillers-engages-pour-lavenir-le-nouveau-nom-du-pole-normandie"
Redirect 301 "/webinaire-entre-les-etudiants-doniris-et-des-collaborateurs-ldc" "/actualites/webinaire-entre-les-etudiants-doniris-et-des-collaborateurs-ldc"
Redirect 301 "/en/100-of-nature-deleveurs-farms-by-2025" "/en/news/100-of-nature-deleveurs-farms-by-2025"
Redirect 301 "/en/acquisition-of-asia-general-food" "/en/news/acquisition-of-asia-general-food"
Redirect 301 "/en/cavol-to-re-conquer-hrd" "/en/news/cavol-to-re-conquer-hrd"
Redirect 301 "/en/entry-into-partnership-with-the-buchanan-family-2" "/en/news/entry-into-partnership-with-the-buchanan-family"
Redirect 301 "/en/ldc-participates-in-the-trades-and-skills-campus" "/en/news/ldc-participates-in-the-trades-and-skills-campus"
Redirect 301 "/en/ldc-to-regain-the-national-market" "/en/news/ldc-to-regain-the-national-market"
Redirect 301 "/en/le-gaulois-maitre-coq-loue-the-chicken-champion-across-the-categories" "/en/news/le-gaulois-maitre-coq-loue-the-chicken-champion-across-the-categories"
Redirect 301 "/en/le-gaulois-signs-a-partnership-with-programme-malin" "/en/news/le-gaulois-signs-a-partnership-with-programme-malin"
Redirect 301 "/en/maitre-coq-becomes-an-official-partner-of-angers-sco-handball" "/en/news/maitre-coq-becomes-an-official-partner-of-angers-sco-handball"
Redirect 301 "/en/signature-of-agreements-with-food-banks" "/en/news/signature-of-agreements-with-food-banks"
Redirect 301 "/en/take-over-of-kiplama-in-belgium" "/en/news/take-over-of-kiplama-in-belgium"
Redirect 301 "/en/takeover-of-luche-tradition-volailles-in-luche-pringe-sarthe-2" "/en/news/takeover-of-luche-tradition-volailles-in-luche-pringe-sarthe"
Redirect 301 "/en/takeover-of-marnevall-in-hungary" "/en/news/takeover-of-marnevall-in-hungary"
Redirect 301 "/en/les-talents-de-la-sarthe-2019-2" "/en/news/talents-in-the-sarthe-2019"
Redirect 301 "/en/talents-in-the-sarthe-2019" "/en/news/talents-in-the-sarthe-2019"
Redirect 301 "/en/the-ldc-group-creates-a-partnership-with-citeo-for-the-development-of-a-polystyrene-recycling-industry-in-france" "/en/news/the-ldc-group-creates-a-partnership-with-citeo-for-the-development-of-a-polystyrene-recycling-industry-in-france"
Redirect 301 "/en/top-start-for-the-vendee-globe-with-maitre-coq" "/en/news/top-start-for-the-vendee-globe-with-maitre-coq"
Redirect 301 "/search" "/recherche"
Redirect 301 "/nous-contacter/nos-engagements" "/nous-contacter/engagements"
</IfModule>
<IfModule mod_expires.c>
ExpiresActive On
# Images
ExpiresByType image/jpg "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/gif "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
ExpiresByType image/svg+xml "access plus 1 year"
# CSS / JS
ExpiresByType text/css "access plus 1 year"
ExpiresByType text/javascript "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType application/x-javascript "access plus 1 year"
# WOFF / WOFF2 / Fonts
ExpiresByType application/font-woff "access plus 1 year"
ExpiresByType font/woff "access plus 1 year"
ExpiresByType application/font-woff2 "access plus 1 year"
ExpiresByType font/woff2 "access plus 1 year"
# Favicon
ExpiresByType image/x-icon "access plus 1 year"
</IfModule>
<FilesMatch "\.(jpe?g|png|gif|webp|css|js|woff|woff2|ico)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
<IfModule mod_brotli.c>
AddOutputFilterByType BROTLI_COMPRESS text/html text/css text/plain text/xml text/javascript application/javascript application/x-javascript
AddType text/plain .br
</IfModule>
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE text/plain
AddOutputFilterByType DEFLATE text/xml
AddOutputFilterByType DEFLATE text/css
AddOutputFilterByType DEFLATE text/javascript
AddOutputFilterByType DEFLATE application/javascript
AddOutputFilterByType DEFLATE application/x-javascript
AddOutputFilterByType DEFLATE application/xml
AddOutputFilterByType DEFLATE application/xhtml+xml
AddOutputFilterByType DEFLATE application/rss+xml
AddOutputFilterByType DEFLATE image/svg+xml
</IfModule>
<IfModule mod_mime.c>
AddType application/javascript .js
AddType font/woff2 .woff2
AddType application/font-woff2 .woff2
AddType application/font-woff .woff
AddType font/woff .woff
AddType image/svg+xml .svg
</IfModule>
<FilesMatch "\.(css|js|jpg|jpeg|gif|png|ico|svg|woff|woff2)$">
FileETag None
Header unset ETag
Header unset Set-Cookie
</FilesMatch>

View file

@ -20,6 +20,21 @@
.image-border_bottom {
top: auto;
}
.textblock__container_swap > .image-content-size img {
width: 100% !important;
object-fit: cover !important;
}
.textblock__container_row > .image-content-size img {
width: 100% !important;
height: 300px !important;
object-fit: cover !important;
}
.textblock__container_side > .image-content-size img {
width: 100% !important;
object-fit: cover !important;
}
@media screen and (max-width: 1600px) {
.wrapper {
@ -88,6 +103,11 @@
.textblock__container_swap .textblock__image {
margin-left: 70px;
}
.textblock__container_row > .image-content-size img {
width: 100% !important;
height: 300px !important;
object-fit: cover !important;
}
.breadcrumbs {
margin-bottom: 30px;
}
@ -246,10 +266,35 @@
width: 310px;
margin: 0px 11px 35px;
}
.image-border-wrap {
width: 100%;
height: 100%;
margin-bottom: 25px;
}
.textblock__container_side > .image-content-size img {
width: 100% !important;
height: 425px !important;
object-fit: cover !important;
}
.textblock__container_swap_left > .image-content-size img {
width: 100% !important;
height: 425px !important;
object-fit: cover !important;
}
.textblock__container_row > .image-content-size {
width: 100% !important;
max-width: 100% !important;
}
.textblock__container_row > .image-content-size img {
height: 210px !important;
object-fit: cover !important;
}
.values {
padding-bottom: 55px;
}
@ -403,7 +448,6 @@
padding-left: 10px;
}
[data-menu="submenu-2951"] .mobile-menu__level {
padding-top: 0;
min-height: calc(100vh - 80px);
}
.offer-form {
@ -918,7 +962,7 @@
font-size: 34px;
margin-bottom: 30px;
}
.engagements__wrapper {
text-align: center;
}
@ -979,8 +1023,30 @@
border: 1px solid #a0a0a0;
}
.image-border-wrap {
width: 100%;
height: 100%;
margin-bottom: 10px;
}
.textblock__container_swap > .image-content-size img {
width: 100% !important;
height: auto !important;
object-fit: cover !important;
}
.textblock__container_side > .image-content-size img {
width: 100% !important;
height: auto !important;
object-fit: cover !important;
}
.textblock__container_row > .image-content-size {
width: 100% !important;
max-width: 100% !important;
height: 210px !important;
}
.image-content-size img {
height: 100% !important;
object-fit: cover !important;
}
.list-item__title {
margin-bottom: 15px;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 368 KiB

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show more