Merge branch 'develop' into trunk
This commit is contained in:
commit
d44f99df84
52 changed files with 2499 additions and 1054 deletions
|
|
@ -88,3 +88,11 @@ TELESCOPE_ENABLED=
|
|||
|
||||
# MATOMO
|
||||
MATOMO_SITE_ID=
|
||||
|
||||
# MEILISEARCH
|
||||
MEILISEARCH_HOST=
|
||||
MEILISEARCH_KEY=
|
||||
|
||||
# LARAVEL SCOUT
|
||||
SCOUT_PREFIX=''
|
||||
SCOUT_QUEUE=
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ enum StaticPageType: int
|
|||
case breedingJob = 17;
|
||||
case breedingFood = 18;
|
||||
case breedingHealth = 19;
|
||||
case search = 20;
|
||||
|
||||
/**
|
||||
* Optionnal labels definition.
|
||||
|
|
@ -47,12 +48,13 @@ enum StaticPageType: int
|
|||
self::home->name => 'Accueil',
|
||||
self::selling->name => 'Conditions générales de vente',
|
||||
self::accessibility->name => 'Accessibilité',
|
||||
self::news->name => 'Actualités',
|
||||
self::social->name => 'Réseaux sociaux',
|
||||
self::breedingPoultry->name => 'L\'élevage des volailles',
|
||||
self::breedingJob->name => 'Le métier d\'éleveur de volailles',
|
||||
self::breedingFood->name => 'L\'alimentation des volailles',
|
||||
self::breedingHealth->name => 'La santé des volailles',
|
||||
self::news->name => 'Actualités',
|
||||
self::social->name => 'Réseaux sociaux',
|
||||
self::search->name => 'Recherche',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ namespace App\Http\Controllers\Fo;
|
|||
|
||||
use App\Enums\StaticPageType;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Lib\Helpers\CollectionHelper;
|
||||
use App\Models\Meat;
|
||||
use App\Models\Product;
|
||||
use App\Models\Range;
|
||||
|
|
@ -54,7 +55,11 @@ class ProductController extends Controller
|
|||
$meatModels = Meat::getPublishedOrderedWithSubmeats();
|
||||
$submeatModels = $submeatModelsFiltered ?? Submeat::getPublishedOrdered();
|
||||
return view('fo.pages.products.index', [
|
||||
'viewsPaginate' => $this->getViewsFromPaginateModels($productModels),
|
||||
'viewsPaginate' => CollectionHelper::getViewsFromPaginateModels(
|
||||
$productModels,
|
||||
'fo.partials.product-card',
|
||||
'model'
|
||||
),
|
||||
'productDiscoverModels' => $productDiscoverModels,
|
||||
'staticPageModel' => $staticPageModel,
|
||||
'rangeModels' => $rangeModels,
|
||||
|
|
@ -182,8 +187,7 @@ class ProductController extends Controller
|
|||
*/
|
||||
public function getProductModelsTitles(string|null $search = null): \Illuminate\Pagination\LengthAwarePaginator
|
||||
{
|
||||
$query = Product::query();
|
||||
return $this->getModelsTitles($query, 'label', $search);
|
||||
return $this->getModelsTitles(Product::query(), 'label', $search);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -226,7 +230,11 @@ class ProductController extends Controller
|
|||
|
||||
if ($request->ajax()) {
|
||||
return response()->json([
|
||||
'viewsPaginate' => $this->getViewsFromPaginateModels($productModels),
|
||||
'viewsPaginate' => CollectionHelper::getViewsFromPaginateModels(
|
||||
$productModels,
|
||||
'fo.partials.product-card',
|
||||
'model'
|
||||
),
|
||||
'children' => [],
|
||||
]);
|
||||
}
|
||||
|
|
@ -276,7 +284,11 @@ class ProductController extends Controller
|
|||
|
||||
if ($request->ajax()) {
|
||||
return response()->json([
|
||||
'viewsPaginate' => $this->getViewsFromPaginateModels($productModels),
|
||||
'viewsPaginate' => CollectionHelper::getViewsFromPaginateModels(
|
||||
$productModels,
|
||||
'fo.partials.product-card',
|
||||
'model'
|
||||
),
|
||||
'children' => $subrangeModels,
|
||||
'productModels' => $productModels,
|
||||
]);
|
||||
|
|
@ -330,7 +342,11 @@ class ProductController extends Controller
|
|||
if ($request->ajax()) {
|
||||
return response()->json([
|
||||
'productModels' => $productModels,
|
||||
'viewsPaginate' => $this->getViewsFromPaginateModels($productModels),
|
||||
'viewsPaginate' => CollectionHelper::getViewsFromPaginateModels(
|
||||
$productModels,
|
||||
'fo.partials.product-card',
|
||||
'model'
|
||||
),
|
||||
'children' => $subrangeModels,
|
||||
]);
|
||||
}
|
||||
|
|
@ -373,7 +389,11 @@ class ProductController extends Controller
|
|||
->paginate($this->productModelsPerPage);
|
||||
if ($request->ajax()) {
|
||||
return response()->json([
|
||||
'viewsPaginate' => $this->getViewsFromPaginateModels($productModels),
|
||||
'viewsPaginate' => CollectionHelper::getViewsFromPaginateModels(
|
||||
$productModels,
|
||||
'fo.partials.product-card',
|
||||
'model'
|
||||
),
|
||||
'children' => $submeatModels,
|
||||
]);
|
||||
}
|
||||
|
|
@ -421,7 +441,11 @@ class ProductController extends Controller
|
|||
->paginate($this->productModelsPerPage);
|
||||
if ($request->ajax()) {
|
||||
return response()->json([
|
||||
'viewsPaginate' => $this->getViewsFromPaginateModels($productModels),
|
||||
'viewsPaginate' => CollectionHelper::getViewsFromPaginateModels(
|
||||
$productModels,
|
||||
'fo.partials.product-card',
|
||||
'model'
|
||||
),
|
||||
'children' => $submeatModels,
|
||||
]);
|
||||
}
|
||||
|
|
@ -529,20 +553,6 @@ class ProductController extends Controller
|
|||
return $recipeModels->slice(0, $nbrMaxRecipes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of views for each product models given.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Pagination\LengthAwarePaginator $productModels
|
||||
* @return array
|
||||
*/
|
||||
public function getViewsFromPaginateModels(LengthAwarePaginator $productModels): array
|
||||
{
|
||||
// @phpstan-ignore-next-line
|
||||
return $productModels->through(function ($productModel) {
|
||||
return view('fo.partials.product-card', ['model' => $productModel])->render();
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get products by a specific range into a json response.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ namespace App\Http\Controllers\Fo;
|
|||
use App\Enums\StaticPageType;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\Fo\Recipes\RecipeCommericalSheetResource;
|
||||
use App\Lib\Helpers\CollectionHelper;
|
||||
use App\Models\ConsumptionMoment;
|
||||
use App\Models\Meat;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
|
@ -51,7 +52,11 @@ class RecipeController extends Controller
|
|||
Submeat::getPublishedOrdered();
|
||||
|
||||
return view('fo.pages.recipes.index', [
|
||||
'viewsPaginate' => $this->getViewsFromPaginateModels($recipeModels),
|
||||
'viewsPaginate' => CollectionHelper::getViewsFromPaginateModels(
|
||||
$recipeModels,
|
||||
'fo.partials.recipe-card',
|
||||
'model'
|
||||
),
|
||||
'recipeModel' => $recipeModel,
|
||||
'staticPageModel' => $staticPageModel,
|
||||
'consumptionMomentModels' => $consumptionMomentModels,
|
||||
|
|
@ -140,7 +145,11 @@ class RecipeController extends Controller
|
|||
|
||||
if ($request->ajax()) {
|
||||
return response()->json([
|
||||
'viewsPaginate' => $this->getViewsFromPaginateModels($recipeModels),
|
||||
'viewsPaginate' => CollectionHelper::getViewsFromPaginateModels(
|
||||
$recipeModels,
|
||||
'fo.partials.recipe-card',
|
||||
'model'
|
||||
),
|
||||
'children' => [],
|
||||
]);
|
||||
}
|
||||
|
|
@ -170,7 +179,11 @@ class RecipeController extends Controller
|
|||
->paginate($this->recipeModelsPerPage);
|
||||
if ($request->ajax()) {
|
||||
return response()->json([
|
||||
'viewsPaginate' => $this->getViewsFromPaginateModels($recipeModels),
|
||||
'viewsPaginate' => CollectionHelper::getViewsFromPaginateModels(
|
||||
$recipeModels,
|
||||
'fo.partials.recipe-card',
|
||||
'model'
|
||||
),
|
||||
'children' => [],
|
||||
]);
|
||||
}
|
||||
|
|
@ -214,7 +227,11 @@ class RecipeController extends Controller
|
|||
->paginate($this->recipeModelsPerPage);
|
||||
if ($request->ajax()) {
|
||||
return response()->json([
|
||||
'viewsPaginate' => $this->getViewsFromPaginateModels($recipeModels),
|
||||
'viewsPaginate' => CollectionHelper::getViewsFromPaginateModels(
|
||||
$recipeModels,
|
||||
'fo.partials.recipe-card',
|
||||
'model'
|
||||
),
|
||||
'children' => $submeatModels,
|
||||
]);
|
||||
}
|
||||
|
|
@ -264,27 +281,17 @@ class RecipeController extends Controller
|
|||
->paginate($this->recipeModelsPerPage);
|
||||
if ($request->ajax()) {
|
||||
return response()->json([
|
||||
'viewsPaginate' => $this->getViewsFromPaginateModels($recipeModels),
|
||||
'viewsPaginate' => CollectionHelper::getViewsFromPaginateModels(
|
||||
$recipeModels,
|
||||
'fo.partials.recipe-card',
|
||||
'model'
|
||||
),
|
||||
'children' => [],
|
||||
]);
|
||||
}
|
||||
return $this->index($recipeModels, $submeatModels);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of views for each recipe models given.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Pagination\LengthAwarePaginator $recipeModels
|
||||
* @return array
|
||||
*/
|
||||
public function getViewsFromPaginateModels(LengthAwarePaginator $recipeModels): array
|
||||
{
|
||||
// @phpstan-ignore-next-line
|
||||
return $recipeModels->through(function ($recipeModel) {
|
||||
return view('fo.partials.recipe-card', ['model' => $recipeModel])->render();
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recipes by a selected recipe model,
|
||||
* filtered first by products associated,
|
||||
|
|
|
|||
101
app/Http/Controllers/Fo/SearchController.php
Normal file
101
app/Http/Controllers/Fo/SearchController.php
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Fo;
|
||||
|
||||
use App\Enums\StaticPageType;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Lib\Helpers\CollectionHelper;
|
||||
use App\Models\StaticPage;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class SearchController extends Controller
|
||||
{
|
||||
/**
|
||||
* Search page.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Contracts\View\View
|
||||
*/
|
||||
public function search(Request $request): \Illuminate\Contracts\View\View
|
||||
{
|
||||
$searchString = $request->input('texte');
|
||||
return view('fo.pages.search', [
|
||||
'staticPageModel' => StaticPage::getCachedStaticPage(StaticPageType::search),
|
||||
'productModels' => $this->getResultFromMeilisearch($searchString, 'App\Models\Product'),
|
||||
'recipeModels' => $this->getResultFromMeilisearch($searchString, 'App\Models\Recipe'),
|
||||
'newsModels' => $this->getResultFromMeilisearch($searchString, 'App\Models\News'),
|
||||
'newsBreedingModels' => $this->getResultFromMeilisearch($searchString, 'App\Models\NewsBreeding'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search string on multiple models.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function searchString(Request $request): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$search = $request->input('texte');
|
||||
$page = $request->input('page');
|
||||
$results = collect($request->models)->map(function (string $model) use ($search, $page) {
|
||||
return $this->getResultFromMeilisearch($search, $model, $page);
|
||||
});
|
||||
return response()->json($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search string on multiple models.
|
||||
*
|
||||
* @param string|null $search
|
||||
* @param string $class
|
||||
* @param integer $itemsPerPage
|
||||
* @param integer $page
|
||||
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
|
||||
*/
|
||||
public function getResultFromMeilisearch(
|
||||
string|null $search,
|
||||
string $className,
|
||||
int $page = 1
|
||||
): \Illuminate\Contracts\Pagination\LengthAwarePaginator {
|
||||
$models = $className::search($search)
|
||||
->where('published', true)
|
||||
->paginateRaw(perPage: 4, page: $page);
|
||||
return $this->getViewsFromMeilisearchPagination($models, $className);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search string on multiple models.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Pagination\LengthAwarePaginator $models
|
||||
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
|
||||
*/
|
||||
public function getViewsFromMeilisearchPagination(
|
||||
LengthAwarePaginator $models,
|
||||
string $className
|
||||
): \Illuminate\Contracts\Pagination\LengthAwarePaginator {
|
||||
$models['hits'] = collect($models['hits'])->map(function ($model) use ($className) {
|
||||
$class = new $className();
|
||||
return $class->forceFill($model);
|
||||
});
|
||||
$models['hits'] = CollectionHelper::getViewsFromCollection(
|
||||
$models['hits'],
|
||||
"fo.partials.{$this->transformClassNameToSnakeCase($className)}-card",
|
||||
'model'
|
||||
);
|
||||
return $models;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform class name to snake case.
|
||||
*
|
||||
* @param string $className
|
||||
* @return string
|
||||
*/
|
||||
public function transformClassNameToSnakeCase($className): string
|
||||
{
|
||||
return Str::lower(preg_replace('/(?<!^)[A-Z]/', '-$0', class_basename(explode('App\\Models\\', $className)[1])));
|
||||
}
|
||||
}
|
||||
|
|
@ -15,8 +15,8 @@ use App\Models\Profession;
|
|||
use App\Models\StaticPage;
|
||||
use App\Models\Testimonial;
|
||||
use App\Models\Video;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Detection\MobileDetect;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class StaticPageController extends Controller
|
||||
{
|
||||
|
|
@ -125,7 +125,7 @@ class StaticPageController extends Controller
|
|||
* @param \App\Models\StaticPage $staticPageModel
|
||||
* @return \Illuminate\Contracts\View\View
|
||||
*/
|
||||
public function saga(StaticPage $staticPageModel)
|
||||
public function saga(StaticPage $staticPageModel): \Illuminate\Contracts\View\View
|
||||
{
|
||||
$staticPageModel = StaticPage::getCachedStaticPage(StaticPageType::saga);
|
||||
$videoModels = Video::get6PublishedVideosOrderedWithProducts();
|
||||
|
|
|
|||
|
|
@ -144,7 +144,8 @@ class UpdateStaticPageRequest extends UpdateMiniCrudRequest
|
|||
!$staticPageModel->type->equals(StaticPageType::recipe) &&
|
||||
!$staticPageModel->type->equals(StaticPageType::product) &&
|
||||
!$staticPageModel->type->equals(StaticPageType::home) &&
|
||||
!$staticPageModel->type->equals(StaticPageType::contact)
|
||||
!$staticPageModel->type->equals(StaticPageType::contact) &&
|
||||
!$staticPageModel->type->equals(StaticPageType::search)
|
||||
) {
|
||||
$rules = \array_merge(
|
||||
$rules,
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
namespace App\Interfaces;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\MorphOne;
|
||||
|
||||
interface Notifiable
|
||||
{
|
||||
/**
|
||||
|
|
@ -11,5 +9,5 @@ interface Notifiable
|
|||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphOne
|
||||
*/
|
||||
public function notifiable(): MorphOne;
|
||||
public function notifiable(): \Illuminate\Database\Eloquent\Relations\MorphOne;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,4 +69,35 @@ class CollectionHelper
|
|||
'options' => $options
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of views for each recipe models given.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Pagination\LengthAwarePaginator $models
|
||||
* @param string $path
|
||||
* @param string $key
|
||||
* @return array
|
||||
*/
|
||||
public static function getViewsFromPaginateModels(LengthAwarePaginator $models, string $path, string $key): array
|
||||
{
|
||||
// @phpstan-ignore-next-line
|
||||
return $models->through(function ($model) use ($path, $key) {
|
||||
return view($path, [$key => $model])->render();
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of views for each item in collection given.
|
||||
*
|
||||
* @param \Illuminate\Support\Collection $collection
|
||||
* @param string $path
|
||||
* @param string $key
|
||||
* @return array
|
||||
*/
|
||||
public static function getViewsFromCollection(Collection $collection, string $path, string $key): array
|
||||
{
|
||||
return $collection->map(function ($item) use ($path, $key) {
|
||||
return view($path, [$key => $item])->render();
|
||||
})->toArray();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,11 +12,10 @@ use Illuminate\Database\Eloquent\Model;
|
|||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Scout\Searchable;
|
||||
use Spatie\Sitemap\Contracts\Sitemapable;
|
||||
use Spatie\Sitemap\Tags\Url;
|
||||
use Uwil\SitemapGenerator\Lib\SitemapHelper;
|
||||
|
||||
|
||||
/**
|
||||
* @property string $seo_title The seo title of the news.
|
||||
* @property string $seo_description The seo description of the news.
|
||||
|
|
@ -42,6 +41,7 @@ class News extends Model implements Sitemapable
|
|||
{
|
||||
use ActivityLog;
|
||||
use HasFactory;
|
||||
use Searchable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
|
|
@ -80,6 +80,16 @@ class News extends Model implements Sitemapable
|
|||
'highlight' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get model's setting to apply on scout search (meilisearch).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static $searchSettings = [
|
||||
'filterableAttributes' => ['title', 'content', 'published'],
|
||||
'sortableAttributes' => ['title', 'content']
|
||||
];
|
||||
|
||||
/**
|
||||
* Perform any actions required after the model boots.
|
||||
*
|
||||
|
|
@ -269,7 +279,7 @@ class News extends Model implements Sitemapable
|
|||
->orderByDesc('created_at')
|
||||
->paginate($detect->isMobile() ? 3 : 6)
|
||||
->through(function ($newsModel) {
|
||||
return view('fo.partials.news-card', ['newsModel' => $newsModel])->render();
|
||||
return view('fo.partials.news-card', ['model' => $newsModel])->render();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -298,16 +308,16 @@ class News extends Model implements Sitemapable
|
|||
->orderByDesc('created_at')
|
||||
->paginate($detect->isMobile() ? 4 : 12)
|
||||
->through(function ($socialModel) {
|
||||
return view('fo.partials.news-card', ['newsModel' => $socialModel])->render();
|
||||
return view('fo.partials.news-card', ['model' => $socialModel])->render();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* toSitemapTag method
|
||||
* To sitemap tag .
|
||||
*
|
||||
* @return Url|string|array
|
||||
* @return \Spatie\Sitemap\Tags\Url|string|array
|
||||
*/
|
||||
public function toSitemapTag(): Url | string | array
|
||||
public function toSitemapTag(): \Spatie\Sitemap\Tags\Url|string|array
|
||||
{
|
||||
$model = $this;
|
||||
$route = 'fo.news.show';
|
||||
|
|
@ -315,6 +325,16 @@ class News extends Model implements Sitemapable
|
|||
return SitemapHelper::generateUrl($route, $params, $model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the model should be searchable.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function shouldBeSearchable(): bool
|
||||
{
|
||||
return $this->published === true;
|
||||
}
|
||||
|
||||
// * RELATIONS
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Scout\Searchable;
|
||||
|
||||
/**
|
||||
* @property string $title The title of the bredding news.
|
||||
|
|
@ -31,6 +32,7 @@ class NewsBreeding extends Model
|
|||
{
|
||||
use ActivityLog;
|
||||
use HasFactory;
|
||||
use Searchable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
|
|
@ -66,6 +68,16 @@ class NewsBreeding extends Model
|
|||
'associated_page' => StaticPageType::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* Get model's setting to apply on scout search (meilisearch).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static $searchSettings = [
|
||||
'filterableAttributes' => ['title', 'content', 'published'],
|
||||
'sortableAttributes' => ['title', 'content']
|
||||
];
|
||||
|
||||
/**
|
||||
* Perform any actions required after the model boots.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -5,19 +5,18 @@ namespace App\Models;
|
|||
use App\Lib\Helpers\FileStorageHelper;
|
||||
use App\Lib\Helpers\ToolboxHelper;
|
||||
use App\Traits\Models\ActivityLog;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Uwil\Seo\Laravel\Interfaces\SeoInterface;
|
||||
use Uwil\Seo\Laravel\Traits\SeoTrait;
|
||||
use Uwil\Seo\Laravel\Objects\SeoItem;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Scout\Searchable;
|
||||
use Spatie\Sitemap\Contracts\Sitemapable;
|
||||
use Spatie\Sitemap\Tags\Url;
|
||||
use Uwil\Seo\Laravel\Interfaces\SeoInterface;
|
||||
use Uwil\Seo\Laravel\Objects\SeoItem;
|
||||
use Uwil\Seo\Laravel\Traits\SeoTrait;
|
||||
use Uwil\SitemapGenerator\Lib\SitemapHelper;
|
||||
|
||||
|
||||
/**
|
||||
* @property string $seo_title Product seo title.
|
||||
* @property string $seo_description Product seo description.
|
||||
|
|
@ -52,6 +51,7 @@ class Product extends Model implements SeoInterface, Sitemapable
|
|||
use ActivityLog;
|
||||
use HasFactory;
|
||||
use SeoTrait;
|
||||
use Searchable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
|
|
@ -98,6 +98,16 @@ class Product extends Model implements SeoInterface, Sitemapable
|
|||
'published' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get model's setting to apply on scout search (meilisearch).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static $searchSettings = [
|
||||
'filterableAttributes' => ['label', 'description', 'preparation', 'ingredients', 'published'],
|
||||
'sortableAttributes' => ['label', 'description', 'preparation', 'ingredients']
|
||||
];
|
||||
|
||||
/**
|
||||
* Perform any actions required after the model boots.
|
||||
*
|
||||
|
|
@ -232,14 +242,21 @@ class Product extends Model implements SeoInterface, Sitemapable
|
|||
/**
|
||||
* toSitemapTag method
|
||||
*
|
||||
* @return Url|string|array
|
||||
* @return \Spatie\Sitemap\Tags\Url|string|array
|
||||
*/
|
||||
public function toSitemapTag(): Url | string | array
|
||||
public function toSitemapTag(): \Spatie\Sitemap\Tags\Url|string|array
|
||||
{
|
||||
$model = $this;
|
||||
$route = 'fo.products.show';
|
||||
$params = ['slug' => $model->slug];
|
||||
return SitemapHelper::generateUrl($route, $params, $model);
|
||||
return SitemapHelper::generateUrl('fo.products.show', ['slug' => $this->slug], $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the model should be searchable.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function shouldBeSearchable(): bool
|
||||
{
|
||||
return $this->published === true;
|
||||
}
|
||||
|
||||
// * RELATIONSHIPS
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Scout\Searchable;
|
||||
use Uwil\Seo\Laravel\Interfaces\SeoInterface;
|
||||
use Uwil\Seo\Laravel\Traits\SeoTrait;
|
||||
use Uwil\Seo\Laravel\Objects\SeoItem;
|
||||
use Spatie\Sitemap\Contracts\Sitemapable;
|
||||
use Spatie\Sitemap\Tags\Url;
|
||||
use Uwil\SitemapGenerator\Lib\SitemapHelper;
|
||||
|
||||
/**
|
||||
|
|
@ -44,6 +44,7 @@ class Recipe extends Model implements SeoInterface, Sitemapable
|
|||
use ActivityLog;
|
||||
use HasFactory;
|
||||
use SeoTrait;
|
||||
use Searchable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
|
|
@ -85,6 +86,16 @@ class Recipe extends Model implements SeoInterface, Sitemapable
|
|||
'published' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get model's setting to apply on scout search (meilisearch).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static $searchSettings = [
|
||||
'filterableAttributes' => ['title', 'preparation', 'advice', 'published'],
|
||||
'sortableAttributes' => ['title', 'preparation', 'advice']
|
||||
];
|
||||
|
||||
/**
|
||||
* Perform any actions required after the model boots.
|
||||
*
|
||||
|
|
@ -239,14 +250,21 @@ class Recipe extends Model implements SeoInterface, Sitemapable
|
|||
/**
|
||||
* toSitemapTag method
|
||||
*
|
||||
* @return Url|string|array
|
||||
* @return \Spatie\Sitemap\Tags\Url|string|array
|
||||
*/
|
||||
public function toSitemapTag(): Url | string | array
|
||||
public function toSitemapTag(): \Spatie\Sitemap\Tags\Url|string|array
|
||||
{
|
||||
$model = $this;
|
||||
$route = 'fo.recipes.show';
|
||||
$params = ['slug' => $model->slug];
|
||||
return SitemapHelper::generateUrl($route, $params, $model);
|
||||
return SitemapHelper::generateUrl('fo.recipes.show', ['slug' => $this->slug], $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the model should be searchable.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function shouldBeSearchable(): bool
|
||||
{
|
||||
return $this->published === true;
|
||||
}
|
||||
|
||||
// * RELATIONSHIPS
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ class ViewServiceProvider extends ServiceProvider
|
|||
['name' => 'breedingFoodStaticPageModel', 'value' => StaticPage::getCachedStaticPage(StaticPageType::breedingFood)],
|
||||
['name' => 'breedingHealthStaticPageModel', 'value' => StaticPage::getCachedStaticPage(StaticPageType::breedingHealth)],
|
||||
['name' => 'newsStaticPageModel', 'value' => StaticPage::getCachedStaticPage(StaticPageType::news)],
|
||||
['name' => 'searchStaticPageModel', 'value' => StaticPage::getCachedStaticPage(StaticPageType::search)],
|
||||
])->map(function ($array) {
|
||||
View::share($array['name'], (function () use ($array) {
|
||||
return $array['value'] ?: null;
|
||||
|
|
|
|||
|
|
@ -19,12 +19,15 @@
|
|||
"carlos-meneses/laravel-mpdf": "^2.1",
|
||||
"diglactic/laravel-breadcrumbs": "^9.0",
|
||||
"guzzlehttp/guzzle": "^7.2",
|
||||
"http-interop/http-factory-guzzle": "^1.2",
|
||||
"laravel/framework": "^11.0",
|
||||
"laravel/octane": "^2.5",
|
||||
"laravel/scout": "^10.11",
|
||||
"laravel/tinker": "^2.8",
|
||||
"laravel/ui": "^4.2",
|
||||
"ldc/ldccrmv4client": "^4.0",
|
||||
"league/csv": "^9.16",
|
||||
"meilisearch/meilisearch-php": "^1.11",
|
||||
"mews/purifier": "^3.4",
|
||||
"mobiledetect/mobiledetectlib": "^4.8",
|
||||
"moneyphp/money": "^4.5",
|
||||
|
|
|
|||
1765
composer.lock
generated
1765
composer.lock
generated
File diff suppressed because it is too large
Load diff
117
config/scout.php
Normal file
117
config/scout.php
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
<?php
|
||||
|
||||
use App\Models\News;
|
||||
use App\Models\NewsBreeding;
|
||||
use App\Models\Product;
|
||||
use App\Models\Recipe;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Search Engine
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default search connection that gets used while
|
||||
| using Laravel Scout. This connection is used when syncing all models
|
||||
| to the search service. You should adjust this based on your needs.
|
||||
|
|
||||
| Supported: "algolia", "meilisearch", "typesense",
|
||||
| "database", "collection", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'driver' => env('SCOUT_DRIVER', 'meilisearch'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Index Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify a prefix that will be applied to all search index
|
||||
| names used by Scout. This prefix may be useful if you have multiple
|
||||
| "tenants" or applications sharing the same search infrastructure.
|
||||
|
|
||||
*/
|
||||
|
||||
'prefix' => env('SCOUT_PREFIX', ''),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queue Data Syncing
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows you to control if the operations that sync your data
|
||||
| with your search engines are queued. When this is set to "true" then
|
||||
| all automatic data syncing will get queued for better performance.
|
||||
|
|
||||
*/
|
||||
|
||||
'queue' => env('SCOUT_QUEUE', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Database Transactions
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This configuration option determines if your data will only be synced
|
||||
| with your search indexes after every open database transaction has
|
||||
| been committed, thus preventing any discarded data from syncing.
|
||||
|
|
||||
*/
|
||||
|
||||
'after_commit' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Chunk Sizes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These options allow you to control the maximum chunk size when you are
|
||||
| mass importing data into the search engine. This allows you to fine
|
||||
| tune each of these chunk sizes based on the power of the servers.
|
||||
|
|
||||
*/
|
||||
|
||||
'chunk' => [
|
||||
'searchable' => 500,
|
||||
'unsearchable' => 500,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Soft Deletes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows to control whether to keep soft deleted records in
|
||||
| the search indexes. Maintaining soft deleted records can be useful
|
||||
| if your application still needs to search for the records later.
|
||||
|
|
||||
*/
|
||||
|
||||
'soft_delete' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Meilisearch Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure your Meilisearch settings. Meilisearch is an open
|
||||
| source search engine with minimal configuration. Below, you can state
|
||||
| the host and key information for your own Meilisearch installation.
|
||||
|
|
||||
| See: https://www.meilisearch.com/docs/learn/configuration/instance_options#all-instance-options
|
||||
|
|
||||
*/
|
||||
|
||||
'meilisearch' => [
|
||||
'host' => env('MEILISEARCH_HOST', 'http://localhost:7700'),
|
||||
'key' => env('MEILISEARCH_KEY'),
|
||||
'index-settings' => [
|
||||
Product::class => Product::$searchSettings,
|
||||
Recipe::class => Recipe::$searchSettings,
|
||||
News::class => News::$searchSettings,
|
||||
NewsBreeding::class => NewsBreeding::$searchSettings,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\StaticPageType;
|
||||
use App\Models\StaticPage;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
StaticPage::query()->forceCreateQuietly(
|
||||
[
|
||||
'type' => StaticPageType::search,
|
||||
'seo_title' => sprintf('Seo title pour la page %s', StaticPageType::search->label()),
|
||||
'seo_description' => sprintf('Seo description pour la page %s', StaticPageType::search->label()),
|
||||
'title' => StaticPageType::search->label(),
|
||||
'slug' => Str::of(StaticPageType::search->label())->slug(),
|
||||
'subtitle' => null,
|
||||
'picture_alt' => null,
|
||||
'picture_title' => null,
|
||||
'picture' => null,
|
||||
'content' => null,
|
||||
'data' => [],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
DB::statement('DELETE FROM `static_pages` WHERE `type` LIKE "' . StaticPageType::search->value() . '"');
|
||||
}
|
||||
};
|
||||
|
|
@ -106,6 +106,7 @@ class StaticPageSeeder extends Seeder
|
|||
'required' => true
|
||||
]
|
||||
]);
|
||||
$this->seedStaticPage(StaticPageType::search);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
156
manifest.yaml
156
manifest.yaml
|
|
@ -43,7 +43,10 @@ data:
|
|||
GOOGLE_RECAPTCHA_PRIVATE: "${APP_GOOGLE_RECAPTCHA_PRIVATE}"
|
||||
GOOGLE_RECAPTCHA_PUBLIC: "${APP_GOOGLE_RECAPTCHA_PUBLIC}"
|
||||
GOOGLE_RECAPTCHA_VERSION: "${APP_GOOGLE_RECAPTCHA_VERSION}"
|
||||
|
||||
MEILISEARCH_HOST: "meilisearch.common-services:7700"
|
||||
MEILISEARCH_KEY: "${APP_MEILISEARCH_KEY}"
|
||||
SCOUT_PREFIX: "${APP_SCOUT_PREFIX}"
|
||||
SCOUT_QUEUE: "${APP_SCOUT_QUEUE}"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
|
|
@ -52,11 +55,10 @@ metadata:
|
|||
spec:
|
||||
storageClassName: "csi-cinder-classic"
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: $APP_PVC_SIZE
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
|
|
@ -73,51 +75,45 @@ spec:
|
|||
app: ${CI_PROJECT_PATH_SLUG}-app
|
||||
spec:
|
||||
containers:
|
||||
- name: ${CI_PROJECT_PATH_SLUG}-container
|
||||
image: $IMAGE_TAG
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: ${CI_PROJECT_PATH_SLUG}-config-map
|
||||
volumeMounts:
|
||||
- name: storage
|
||||
mountPath: /app/srcs/storage
|
||||
- name: ${CI_PROJECT_PATH_SLUG}-queue-worker
|
||||
image: $IMAGE_TAG
|
||||
lifecycle:
|
||||
preStop:
|
||||
exec:
|
||||
command:
|
||||
[
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"kill -s TERM 1; while test $(ps ax | grep -v grep | grep bash | wc -l) -gt 0; do sleep 2s; done",
|
||||
]
|
||||
resources:
|
||||
limits:
|
||||
memory: 512Mi
|
||||
cpu: "1"
|
||||
command:
|
||||
- "bash"
|
||||
args:
|
||||
- "k8s-queues.sh"
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: ${CI_PROJECT_PATH_SLUG}-config-map
|
||||
volumeMounts:
|
||||
- name: storage
|
||||
mountPath: /app/srcs/storage
|
||||
- name: ${CI_PROJECT_PATH_SLUG}-container
|
||||
image: $IMAGE_TAG
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: ${CI_PROJECT_PATH_SLUG}-config-map
|
||||
volumeMounts:
|
||||
- name: storage
|
||||
mountPath: /app/srcs/storage
|
||||
- name: ${CI_PROJECT_PATH_SLUG}-queue-worker
|
||||
image: $IMAGE_TAG
|
||||
lifecycle:
|
||||
preStop:
|
||||
exec:
|
||||
command: [ "/bin/sh", "-c", "kill -s TERM 1; while test $(ps ax | grep -v grep | grep bash | wc -l) -gt 0; do sleep 2s; done" ]
|
||||
resources:
|
||||
limits:
|
||||
memory: 512Mi
|
||||
cpu: "1"
|
||||
command:
|
||||
- "bash"
|
||||
args:
|
||||
- "k8s-queues.sh"
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: ${CI_PROJECT_PATH_SLUG}-config-map
|
||||
volumeMounts:
|
||||
- name: storage
|
||||
mountPath: /app/srcs/storage
|
||||
imagePullSecrets:
|
||||
- name: docker-registry-credentials
|
||||
- name: docker-registry-credentials
|
||||
securityContext:
|
||||
runAsUser: 33
|
||||
runAsGroup: 33
|
||||
fsGroup: 33
|
||||
fsGroupChangePolicy: "OnRootMismatch"
|
||||
volumes:
|
||||
- name: storage
|
||||
persistentVolumeClaim:
|
||||
claimName: ${CI_PROJECT_PATH_SLUG}-pvc-storage
|
||||
|
||||
- name: storage
|
||||
persistentVolumeClaim:
|
||||
claimName: ${CI_PROJECT_PATH_SLUG}-pvc-storage
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
|
|
@ -127,9 +123,8 @@ spec:
|
|||
selector:
|
||||
app: ${CI_PROJECT_PATH_SLUG}-app
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 443
|
||||
|
||||
- port: 80
|
||||
targetPort: 443
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
|
|
@ -137,19 +132,18 @@ metadata:
|
|||
name: ${CI_PROJECT_PATH_SLUG}-ingress-route
|
||||
spec:
|
||||
entryPoints:
|
||||
- web
|
||||
- websecure
|
||||
- web
|
||||
- websecure
|
||||
routes:
|
||||
- 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
|
||||
- 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
|
||||
tls:
|
||||
secretName: ${CRT_TLS_NAME}
|
||||
|
||||
---
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
|
|
@ -165,35 +159,35 @@ spec:
|
|||
affinity:
|
||||
podAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
- labelSelector:
|
||||
matchExpressions:
|
||||
- key: app
|
||||
operator: In
|
||||
values:
|
||||
- ${CI_PROJECT_PATH_SLUG}-app
|
||||
topologyKey: "kubernetes.io/hostname"
|
||||
namespaces: [$KUBE_NAMESPACE]
|
||||
- labelSelector:
|
||||
matchExpressions:
|
||||
- key: app
|
||||
operator: In
|
||||
values:
|
||||
- ${CI_PROJECT_PATH_SLUG}-app
|
||||
topologyKey: "kubernetes.io/hostname"
|
||||
namespaces: [ $KUBE_NAMESPACE ]
|
||||
containers:
|
||||
- name: ${CI_PROJECT_PATH_SLUG}-scheduled
|
||||
image: $IMAGE_TAG
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- "php"
|
||||
args:
|
||||
- "artisan"
|
||||
- "schedule:run"
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: ${CI_PROJECT_PATH_SLUG}-config-map
|
||||
volumeMounts:
|
||||
- name: storage
|
||||
mountPath: /app/srcs/storage
|
||||
imagePullSecrets:
|
||||
- name: docker-registry-credentials
|
||||
volumes:
|
||||
- name: ${CI_PROJECT_PATH_SLUG}-scheduled
|
||||
image: $IMAGE_TAG
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- "php"
|
||||
args:
|
||||
- "artisan"
|
||||
- "schedule:run"
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: ${CI_PROJECT_PATH_SLUG}-config-map
|
||||
volumeMounts:
|
||||
- name: storage
|
||||
persistentVolumeClaim:
|
||||
claimName: ${CI_PROJECT_PATH_SLUG}-pvc-storage
|
||||
mountPath: /app/srcs/storage
|
||||
imagePullSecrets:
|
||||
- name: docker-registry-credentials
|
||||
volumes:
|
||||
- name: storage
|
||||
persistentVolumeClaim:
|
||||
claimName: ${CI_PROJECT_PATH_SLUG}-pvc-storage
|
||||
securityContext:
|
||||
runAsUser: 33
|
||||
runAsGroup: 33
|
||||
|
|
|
|||
24
package-lock.json
generated
24
package-lock.json
generated
|
|
@ -3034,9 +3034,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
|
|
@ -5542,9 +5542,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.7",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -7549,9 +7549,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/sweetalert2": {
|
||||
"version": "11.14.4",
|
||||
"resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.14.4.tgz",
|
||||
"integrity": "sha512-8QMzjxCuinwm18EK5AtYvuhP+lRMRxTWVXy8om9wGlULsXSI4TD29kyih3VYrSXMMBlD4EShFvNC7slhTC7j0w==",
|
||||
"version": "11.15.3",
|
||||
"resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.15.3.tgz",
|
||||
"integrity": "sha512-+0imNg+XYL8tKgx8hM0xoiXX3KfgxHDmiDc8nTJFO89fQEEhJlkecSdyYOZ3IhVMcUmoNte4fTIwWiugwkPU6w==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
|
|
@ -7559,9 +7559,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/swiper": {
|
||||
"version": "11.1.1",
|
||||
"resolved": "https://registry.npmjs.org/swiper/-/swiper-11.1.1.tgz",
|
||||
"integrity": "sha512-jGmEA/fNz1lACIcY4/40ggm1Gcyv+EUivmgV/Jd2WFPsEJhbWXnRAwzZR8OPjkBLtDxmzcoYG/iiAMWfRs0YKQ==",
|
||||
"version": "11.2.0",
|
||||
"resolved": "https://registry.npmjs.org/swiper/-/swiper-11.2.0.tgz",
|
||||
"integrity": "sha512-rjjAKgDEs+grR2eQshVDCcE4KNPC7CI294nfcbV9gE8WCsLdvOYXDeZKUYevqAZZp8j5hE7kpT3dAGVKFBWlxQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -230,55 +230,6 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
#input-group-search {
|
||||
[type="submit"] {
|
||||
border-radius: 1rem;
|
||||
border-right-width: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
border-top-right-radius: 0 !important;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
border-radius: 0 1rem 1rem 0 !important;
|
||||
border-right-width: var(--bs-border-width) !important;
|
||||
border-left-width: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
#btn-search-empty {
|
||||
border-radius: 1rem;
|
||||
border-left-width: 0 !important;
|
||||
border-bottom-left-radius: 0 !important;
|
||||
border-top-left-radius: 0 !important;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
border-radius: 1rem 0 0 1rem !important;
|
||||
border-right-width: 0 !important;
|
||||
border-left-width: var(--bs-border-width) !important;
|
||||
}
|
||||
}
|
||||
|
||||
#search-input {
|
||||
height: calc(2.65rem + calc(var(--bs-border-width) * 2));
|
||||
min-height: calc(2.65rem + calc(var(--bs-border-width) * 2));
|
||||
line-height: 1.25;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
@include rfs(20rem, min-width);
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
@include rfs(30rem, min-width);
|
||||
}
|
||||
|
||||
+label {
|
||||
height: 2rem;
|
||||
padding: 0.5rem 0.25rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#menu-lang-list {
|
||||
display: none;
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
@import "./pages/page-des-40-ans";
|
||||
@import "./pages/export-40-ans";
|
||||
@import "./pages/news";
|
||||
@import "./pages/search";
|
||||
|
||||
// * PARTIALS
|
||||
@import "./partials/breeder-card";
|
||||
|
|
@ -47,6 +48,5 @@
|
|||
@import "./partials/testimonial-card";
|
||||
@import "./partials/video-card";
|
||||
|
||||
|
||||
// * COMPONENTS
|
||||
@import "./components/breeding/find-poultry-breeder";
|
||||
|
|
|
|||
43
resources/sass/fo/pages/search.scss
Normal file
43
resources/sass/fo/pages/search.scss
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
#search-section{
|
||||
#input-group-search {
|
||||
[type="submit"] {
|
||||
border-radius: 1rem;
|
||||
border-right-width: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
border-top-right-radius: 0 !important;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
border-radius: 0 1rem 1rem 0 !important;
|
||||
border-right-width: var(--bs-border-width) !important;
|
||||
border-left-width: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
#search-input {
|
||||
height: calc(2.65rem + calc(var(--bs-border-width) * 2));
|
||||
min-height: calc(2.65rem + calc(var(--bs-border-width) * 2));
|
||||
border-radius: 1rem 0 0 1rem;
|
||||
border-right-width: 0;
|
||||
line-height: 1.25;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
@include rfs(20rem, min-width);
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
@include rfs(30rem, min-width);
|
||||
}
|
||||
|
||||
+label {
|
||||
height: 2rem;
|
||||
padding: 0.8rem 1rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#search-no-result-icon {
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
}
|
||||
}
|
||||
2
resources/sass/fo/vendor/_swiper.scss
vendored
2
resources/sass/fo/vendor/_swiper.scss
vendored
|
|
@ -1,4 +1,4 @@
|
|||
@import '~/swiper/swiper.min.css';
|
||||
@import '~/swiper/swiper-bundle.min';
|
||||
|
||||
// * CUSTOM STYLE
|
||||
.swiper {
|
||||
|
|
|
|||
|
|
@ -410,6 +410,35 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
},
|
||||
},
|
||||
},
|
||||
"swiper-news": {
|
||||
modules: [Navigation, Pagination],
|
||||
grabCursor: true,
|
||||
slidesPerView: 1,
|
||||
centeredSlides: true,
|
||||
spaceBetween: 20,
|
||||
navigation: {
|
||||
nextEl: ".swiper-news-button-next",
|
||||
prevEl: ".swiper-news-button-prev",
|
||||
},
|
||||
breakpoints: {
|
||||
768: {
|
||||
spaceBetween: 40,
|
||||
slidesPerView: 2,
|
||||
},
|
||||
992: {
|
||||
slidesPerView: 3,
|
||||
centeredSlides: false,
|
||||
slidesPerGroup: 3,
|
||||
},
|
||||
},
|
||||
pagination: {
|
||||
el: ".swiper-recipe-pagination",
|
||||
clickable: true,
|
||||
renderBullet: function (index: number, className: string) {
|
||||
return "<button class=\"" + className + "\" title=\"Aller à la slide " + (index + 1) + "\"></button>";
|
||||
},
|
||||
},
|
||||
},
|
||||
"swiper-testimonials": {
|
||||
modules: [Navigation, Pagination],
|
||||
grabCursor: true,
|
||||
|
|
|
|||
189
resources/ts/fo/components/MeilisearchModelsComponent.vue
Normal file
189
resources/ts/fo/components/MeilisearchModelsComponent.vue
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
<template>
|
||||
<div class="search-component row">
|
||||
<!-- INPUT -->
|
||||
<div class="col-12 col-md-6 mb-5 mx-auto text-center">
|
||||
<div class="input-group mx-auto">
|
||||
<div class="form-floating">
|
||||
<input
|
||||
id="search-input"
|
||||
ref="searchInput"
|
||||
class="form-control fs-5 border-dark border-1 border-end-0 f-bourton-base shadow-none rounded-start-2"
|
||||
type="text"
|
||||
name="search"
|
||||
:value="searchValue"
|
||||
@keydown="executeSearchFromKey($event)"
|
||||
:placeholder="trans.methods.__('Rechercher...')"
|
||||
>
|
||||
<label for="search-input">{{ trans.methods.__('Rechercher...') }}</label>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-outline-dark shadow-none border-end-0"
|
||||
id="btn-search-empty"
|
||||
type="button"
|
||||
@click="resetSearch()"
|
||||
:title="trans.methods.__('Effacer la recherche')"
|
||||
:aria-label="trans.methods.__('Effacer le contenu la barre de recherche')"
|
||||
>
|
||||
<i class="fa-solid fa-xmark" />
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-outline-dark shadow-none rounded-end-2"
|
||||
type="button"
|
||||
@click="setSearch()"
|
||||
:title="trans.methods.__('Lancer la recherche')"
|
||||
:aria-label="trans.methods.__('Lancer la recherche')"
|
||||
>
|
||||
<i class="fa-solid fa-magnifying-glass" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- RESULTS -->
|
||||
<template v-if="elements.length">
|
||||
<template
|
||||
v-for="(element, elementKey) in elements"
|
||||
:key="elementKey"
|
||||
>
|
||||
<div class="col-12 mb-5">
|
||||
<MeilisearchPaginateViewsComponent
|
||||
:identifier="elementKey"
|
||||
:search="searchInput?.value"
|
||||
:route-url="routeUrl"
|
||||
:trans-name="element.name"
|
||||
:class-name="element.class"
|
||||
:default-models="element.models"
|
||||
@reset-search="resetSearch"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineOptions, onMounted, ref, useAttrs, nextTick } from "vue";
|
||||
import trans from "../../modules/trans";
|
||||
import MeilisearchPaginateViewsComponent from "./MeilisearchPaginateViewsComponent.vue";
|
||||
import parseLaravelErrorMessages from "../../modules/parseLaravelErrorMessages";
|
||||
import type { SwiperContainer } from "swiper/element";
|
||||
|
||||
defineOptions({
|
||||
name: "MeilisearchModelsComponent"
|
||||
});
|
||||
|
||||
// * ATTRIBUTES
|
||||
const attrs = useAttrs();
|
||||
|
||||
// * REFS
|
||||
const searchInput = ref<HTMLInputElement|null>(null);
|
||||
|
||||
// * DATA
|
||||
const elements = ref<Array<{name: string, class: string, models: LaravelPaginator}>>([]);
|
||||
const searchValue = ref<string|null>(null);
|
||||
const routeUrl = ref<string>("");
|
||||
const loading = ref<boolean>(false);
|
||||
|
||||
// * COMPUTED
|
||||
const isUsedWithProps = computed<boolean>(() => attrs.json === undefined);
|
||||
|
||||
// * LIFECYCLE
|
||||
onMounted(() => {
|
||||
const json = String(attrs.json ?? "{}"),
|
||||
data = JSON.parse(json);
|
||||
if (!isUsedWithProps.value) {
|
||||
elements.value = data.elements;
|
||||
searchValue.value = data.searchValue;
|
||||
routeUrl.value = data.routeUrl;
|
||||
}
|
||||
});
|
||||
|
||||
// * METHODS
|
||||
|
||||
/**
|
||||
* Reset all filters activated.
|
||||
* @return void
|
||||
*/
|
||||
function resetSearch(): void {
|
||||
searchInput.value!.value = "";
|
||||
searchValue.value = "";
|
||||
setSearch();
|
||||
history.replaceState("Object", "", "/recherche");
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the search when the enter key is down.
|
||||
* @return void
|
||||
*/
|
||||
function executeSearchFromKey(event: KeyboardEvent): void {
|
||||
if (event.key === "Enter") {
|
||||
setSearch();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get models by activated filters.
|
||||
* @return void
|
||||
*/
|
||||
function setSearch(): void {
|
||||
if (!routeUrl.value) {
|
||||
throw new Error(`Route ${routeUrl.value} doesn't exist`);
|
||||
}
|
||||
searchValue.value = String(searchInput.value?.value);
|
||||
loading.value = true;
|
||||
window.axios
|
||||
.post(routeUrl.value, {
|
||||
"texte": searchValue.value,
|
||||
"models": elements.value.map((element) => { return element.class; }),
|
||||
"page": 1,
|
||||
})
|
||||
.then((response) => {
|
||||
elements.value.forEach((element, index) => {
|
||||
// Erase existing results by the new.
|
||||
element.models = response.data[index];
|
||||
// Update sliders in children components.
|
||||
const swiperContainer = document.querySelector(`.swiper-search-${index}`) as SwiperContainer|null;
|
||||
swiperContainer?.swiper.slideTo(0);
|
||||
nextTick(() => {
|
||||
swiperContainer?.swiper.update();
|
||||
});
|
||||
});
|
||||
history.replaceState("Object", "", "/recherche" + ((searchValue.value?.length) ? "?texte=" + searchValue.value : ""));
|
||||
}).then(() => {
|
||||
loading.value = false;
|
||||
document.getElementById("search-input")?.scrollIntoView();
|
||||
})
|
||||
.catch(ajaxErrorHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return error message after the ajax call.
|
||||
* @return void
|
||||
*/
|
||||
function ajaxErrorHandler(e: any): void {
|
||||
let message = trans.methods.__("Une erreur est survenue");
|
||||
if (e.response.status === 422) {
|
||||
message = parseLaravelErrorMessages.methods.parseValidationErrors(
|
||||
e.response?.data?.errors ?? {}
|
||||
) || message;
|
||||
if (window.vueDebug) {
|
||||
console.warn(e.response.data.errors);
|
||||
}
|
||||
} else if (e.response.status === 419) {
|
||||
console.log(trans.methods.__("Votre session a expiré, la page va être rechargée"));
|
||||
setTimeout(() => window.location.reload(), 2000);
|
||||
} else if (e.response.status === 403) {
|
||||
console.log(trans.methods.__("Vous n'êtes pas autorisé a effectuer cette action"));
|
||||
setTimeout(() => window.location.reload(), 2000);
|
||||
} else {
|
||||
if (window.vueDebug) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
console.log(message);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
#search-input {
|
||||
scroll-margin-top: 8rem;
|
||||
}
|
||||
</style>
|
||||
395
resources/ts/fo/components/MeilisearchPaginateViewsComponent.vue
Normal file
395
resources/ts/fo/components/MeilisearchPaginateViewsComponent.vue
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
<template>
|
||||
<div class="search-component row">
|
||||
<div class="col-12 pt-4">
|
||||
<p class="h2 text-uppercase text-start m-0">
|
||||
<span class="me-2">{{ name }}</span>
|
||||
<span class="fs-4 text-body-secondary">
|
||||
{{ (models.total) ? "(" + models.data.hits.length + "/" + models.total + ")" : "" }}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<!-- RESULTS -->
|
||||
<div
|
||||
v-show="models.data.hits.length"
|
||||
class="col-12 pt-4"
|
||||
>
|
||||
<div class="row">
|
||||
<!-- MOBILE -->
|
||||
<div
|
||||
v-show="isMobile"
|
||||
class="col-12 position-relative d-lg-none"
|
||||
>
|
||||
<div
|
||||
:id="`swiper-search-${identifier}`"
|
||||
:class="`swiper swiper-search-${identifier} overflow-hidden px-3 pt-1 pb-4`"
|
||||
>
|
||||
<div class="swiper-wrapper align-items-stretch w-fit">
|
||||
<div
|
||||
v-for="(model, modelKey) in models.data.hits"
|
||||
:key="modelKey"
|
||||
v-html="model"
|
||||
class="swiper-slide h-auto"
|
||||
/>
|
||||
<div
|
||||
v-if="paginationParameters.page < paginationParameters.lastPage"
|
||||
class="swiper-slide h-auto"
|
||||
>
|
||||
<template v-if="loading || paginationParameters.loading">
|
||||
<div class="card placeholder-glow overflow-hidden border-0 h-100 p-0">
|
||||
<div class="placeholder placeholder-img w-100 h-100" />
|
||||
</div>
|
||||
</template>
|
||||
<button
|
||||
v-else
|
||||
class="btn btn-primary d-flex flex-column justify-content-center align-items-center fs-4 rounded-2 w-100 h-100"
|
||||
@click="getModels()"
|
||||
>
|
||||
<span class="position-relative">
|
||||
{{ trans.methods.__('VOIR PLUS') }}
|
||||
<FontAwesomeIcon
|
||||
icon="fa-solid fa-arrow-right"
|
||||
class="fa-animation position-absolute top-100"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
:class="`swiper-button swiper-search-${identifier}-button-prev btn btn-primary position-absolute rounded-circle border-0 p-1 z-2`"
|
||||
:title="trans.methods.__('Suivant')"
|
||||
:aria-label="trans.methods.__('Suivant')"
|
||||
type="button"
|
||||
>
|
||||
<FontAwesomeIcon icon="fa-solid fa-arrow-left" />
|
||||
</button>
|
||||
<button
|
||||
:class="`swiper-button swiper-search-${identifier}-button-next btn btn-primary position-absolute rounded-circle border-0 p-1 z-2`"
|
||||
:title="trans.methods.__('Précédent')"
|
||||
:aria-label="trans.methods.__('Précédent')"
|
||||
type="button"
|
||||
>
|
||||
<FontAwesomeIcon icon="fa-solid fa-arrow-right" />
|
||||
</button>
|
||||
<div
|
||||
v-if="models.data.hits.length > 1"
|
||||
:class="`swiper-pagination swiper-search-${identifier}-pagination position-relative py-2 z-1`"
|
||||
/>
|
||||
</div>
|
||||
<!-- DESKTOP -->
|
||||
<template v-if="!isMobile && !loading">
|
||||
<template
|
||||
v-for="(model, modelKey) in models.data.hits"
|
||||
:key="modelKey"
|
||||
>
|
||||
<div
|
||||
class="col-12 col-sm-6 col-md-4 col-lg-3 mb-3 d-none d-lg-block"
|
||||
v-html="model"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<!-- MESSAGES & PAGINATION -->
|
||||
<template v-if="!loading">
|
||||
<div
|
||||
v-if="paginationParameters.page < paginationParameters.lastPage"
|
||||
class="col-12 d-none d-lg-block"
|
||||
>
|
||||
<button
|
||||
id="btn-arrow"
|
||||
class="btn btn-primary d-flex justify-content-center align-items-center mx-auto"
|
||||
type="button"
|
||||
:title="trans.methods.__('Charger les éléments suivants')"
|
||||
@click="getModels()"
|
||||
:disabled="paginationParameters.loading"
|
||||
>
|
||||
<template v-if="paginationParameters.loading">
|
||||
<span
|
||||
class="spinner-border spinner-border-sm me-2"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span role="status">{{ trans.methods.__('Chargement...') }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ trans.methods.__("Voir plus") }}
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="!isMobile && models.data.hits.length && !paginationParameters.loading && paginationParameters.page === paginationParameters.lastPage"
|
||||
class="col-12"
|
||||
>
|
||||
<p class="text-body-secondary text-center fst-italic m-0">
|
||||
{{ trans.methods.__('Tous les éléments sont affichés') }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-if="!models.data.hits.length"
|
||||
class="col-12 text-center mt-2"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon="fa-solid fa-triangle-exclamation"
|
||||
class="no-result-icon"
|
||||
/>
|
||||
<p class="text-body-secondary text-center fst-italic my-2">
|
||||
{{ trans.methods.__('Aucun élément n\'est disponible') }}
|
||||
</p>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
type="button"
|
||||
@click="resetSearch()"
|
||||
>
|
||||
{{ trans.methods.__('Effacer la recherche') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { defineOptions, onMounted, nextTick, ref, reactive, watch } from "vue";
|
||||
import type { PropType } from "vue";
|
||||
import trans from "../../modules/trans";
|
||||
import parseLaravelErrorMessages from "../../modules/parseLaravelErrorMessages";
|
||||
import Swiper from "swiper";
|
||||
import { Keyboard, Navigation, Pagination } from "swiper/modules";
|
||||
|
||||
defineOptions({
|
||||
name: "MeilisearchPaginateComponent"
|
||||
});
|
||||
|
||||
// * EMITS
|
||||
const emits = defineEmits<{
|
||||
resetSearch: [],
|
||||
}>();
|
||||
|
||||
// * PROPS
|
||||
const props = defineProps({
|
||||
identifier: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
search: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
routeUrl: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
transName: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
className: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
defaultModels: {
|
||||
type: Array as unknown as PropType<LaravelPaginator>,
|
||||
default: () => { return []; }
|
||||
},
|
||||
});
|
||||
|
||||
// * DATA
|
||||
const identifier = ref<Number>(props.identifier);
|
||||
const search = ref<string>(props.search);
|
||||
const name = ref<string>(props.transName);
|
||||
const className = ref<string>(props.className);
|
||||
const models = ref<LaravelPaginator>(props.defaultModels);
|
||||
const routeUrl = ref<string|null>(props.routeUrl);
|
||||
const slider = ref<Swiper|null>(null);
|
||||
const loading = ref<boolean>(false);
|
||||
const isMobile = ref<boolean>(true);
|
||||
|
||||
/** Pagination parameters. */
|
||||
const paginationParameters = reactive<{
|
||||
page: number,
|
||||
lastPage: number,
|
||||
loading: boolean,
|
||||
}>({
|
||||
page: props.defaultModels.current_page,
|
||||
lastPage: props.defaultModels.last_page,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
// * LIFECYCLE
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
window.addEventListener("resize", function () {
|
||||
checkIfDevice();
|
||||
});
|
||||
checkIfDevice();
|
||||
setSwiper();
|
||||
});
|
||||
});
|
||||
|
||||
// * WATCHERS
|
||||
|
||||
/** Update models by watching parent component models. */
|
||||
watch(() => props.defaultModels, () => {
|
||||
search.value = props.search;
|
||||
models.value = props.defaultModels;
|
||||
paginationParameters.page = props.defaultModels.current_page;
|
||||
paginationParameters.lastPage = props.defaultModels.last_page;
|
||||
});
|
||||
|
||||
// * METHODS
|
||||
|
||||
/**
|
||||
* Reset the search by spreading an event.
|
||||
* @return void
|
||||
*/
|
||||
function checkIfDevice(): void {
|
||||
isMobile.value = (window.innerWidth < 992) ? true : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the slider for related games.
|
||||
* @return void
|
||||
*/
|
||||
function setSwiper(): void {
|
||||
slider.value = new Swiper(`.swiper-search-${identifier.value}`, {
|
||||
modules: [Navigation, Keyboard, Pagination],
|
||||
grabCursor: true,
|
||||
slidesPerView: 1.2,
|
||||
initialSlide: 0,
|
||||
centeredSlides: true,
|
||||
spaceBetween: 10,
|
||||
navigation: {
|
||||
nextEl: `.swiper-search-${identifier.value}-button-next`,
|
||||
prevEl: `.swiper-search-${identifier.value}-button-prev`,
|
||||
},
|
||||
keyboard: {
|
||||
enabled: true,
|
||||
},
|
||||
pagination: {
|
||||
el: `.swiper-search-${identifier.value}-pagination`,
|
||||
dynamicBullets: true,
|
||||
clickable: true,
|
||||
renderBullet: function (index: number, className: string) {
|
||||
return `<button class="btn btn-primary rounded-circle border-0 mx-1 ${className}"
|
||||
title="${trans.methods.__("Diapositive n°:slideId", {"slideId": String(index + 1)})}"
|
||||
aria-label="${trans.methods.__("Cliquez pour accéder à la diapositive n°:slideId", {"slideId": String(index + 1)})}"
|
||||
type="button"></button>`;
|
||||
},
|
||||
},
|
||||
breakpoints: {
|
||||
576: {
|
||||
centeredSlides: true,
|
||||
slidesPerView: 1.3,
|
||||
spaceBetween: 25,
|
||||
},
|
||||
768: {
|
||||
centeredSlides: false,
|
||||
slidesPerView: 2,
|
||||
spaceBetween: 35,
|
||||
},
|
||||
992: {
|
||||
centeredSlides: false,
|
||||
slidesPerView: 3,
|
||||
spaceBetween: 35,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the search by spreading an event.
|
||||
*/
|
||||
function resetSearch(): void {
|
||||
emits("resetSearch");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get models by activated filters.
|
||||
* @return void
|
||||
*/
|
||||
function getModels(): void {
|
||||
if (!routeUrl.value) {
|
||||
throw new Error(`Route ${routeUrl.value} doesn't exist`);
|
||||
}
|
||||
paginationParameters.page = paginationParameters.page + 1;
|
||||
paginationParameters.loading = true;
|
||||
window.axios
|
||||
.post(routeUrl.value, {
|
||||
"texte": search.value,
|
||||
"models": [className.value],
|
||||
"page": paginationParameters.page
|
||||
})
|
||||
.then((response) => {
|
||||
paginationParameters.page = response.data[0].current_page;
|
||||
paginationParameters.lastPage = response.data[0].last_page;
|
||||
models.value.data.hits = models.value.data.hits.concat(response.data[0].data.hits);
|
||||
paginationParameters.loading = false;
|
||||
}).then(() => {
|
||||
slider.value?.update();
|
||||
})
|
||||
.catch(ajaxErrorHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return error message after the ajax call.
|
||||
* @return void
|
||||
*/
|
||||
function ajaxErrorHandler(e: any): void {
|
||||
let message = trans.methods.__("Une erreur est survenue");
|
||||
if (e.response.status === 422) {
|
||||
message = parseLaravelErrorMessages.methods.parseValidationErrors(
|
||||
e.response?.data?.errors ?? {}
|
||||
) || message;
|
||||
if (window.vueDebug) {
|
||||
console.warn(e.response.data.errors);
|
||||
}
|
||||
} else if (e.response.status === 419) {
|
||||
console.log(trans.methods.__("Votre session a expiré, la page va être rechargée"));
|
||||
setTimeout(() => window.location.reload(), 2000);
|
||||
} else if (e.response.status === 403) {
|
||||
console.log(trans.methods.__("Vous n'êtes pas autorisé a effectuer cette action"));
|
||||
setTimeout(() => window.location.reload(), 2000);
|
||||
} else {
|
||||
if (window.vueDebug) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
console.log(message);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.swiper {
|
||||
&-pagination {
|
||||
bottom: 4px !important;
|
||||
}
|
||||
&-button {
|
||||
width: 2.5rem !important;
|
||||
height: 2.5rem !important;
|
||||
&:first-of-type,
|
||||
&:last-of-type {
|
||||
top: auto;
|
||||
bottom: 0rem;
|
||||
}
|
||||
&:first-of-type {
|
||||
left: calc(var(--bs-gutter-x) * .5);
|
||||
}
|
||||
&:last-of-type {
|
||||
right: calc(var(--bs-gutter-x) * .5);
|
||||
}
|
||||
}
|
||||
}
|
||||
.fa-animation {
|
||||
animation: translation 1.5s cubic-bezier(.17,.84,.44,1) infinite;
|
||||
}
|
||||
@keyframes translation {
|
||||
0% { left: 0%;opacity: 0; }
|
||||
30% { opacity: 1; }
|
||||
70% { opacity: 1; }
|
||||
100% { left: calc(100% - 20.5px);opacity: 0; }
|
||||
}
|
||||
.no-result-icon {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
}
|
||||
</style>
|
||||
25
resources/ts/index.d.ts
vendored
25
resources/ts/index.d.ts
vendored
|
|
@ -12,17 +12,19 @@ interface EnumInterface {
|
|||
value: number | string;
|
||||
}
|
||||
|
||||
// * Laravel Paginator
|
||||
|
||||
interface PaginatorLink {
|
||||
url: null | string;
|
||||
label: string;
|
||||
active: boolean;
|
||||
// * Paginator
|
||||
interface LaravelScout {
|
||||
hits: string[];
|
||||
hitsPerPage: Number;
|
||||
page: Number;
|
||||
processingTimeMs: Number;
|
||||
query: String;
|
||||
totalHits: Number;
|
||||
totalPages: Number;
|
||||
}
|
||||
|
||||
interface Paginator {
|
||||
current_page: number;
|
||||
data: LaravelModelList;
|
||||
data: LaravelScout|array;
|
||||
first_page_url: string;
|
||||
from: number;
|
||||
last_page: number;
|
||||
|
|
@ -35,6 +37,11 @@ interface Paginator {
|
|||
to: number;
|
||||
total: number;
|
||||
}
|
||||
interface PaginatorLink {
|
||||
url: null | string;
|
||||
label: string;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
// * Others
|
||||
type VselectOptionType = {
|
||||
|
|
@ -70,7 +77,7 @@ type NestedStringList = NestedStringListInterface | StringList | string;
|
|||
type NestedNumberList = NestedNumberListInterface | NumberList | number;
|
||||
|
||||
type ModelList = Array<Model>;
|
||||
interface ModelListInterface extends ModelList { }
|
||||
interface ModelListInterface extends ModelList {}
|
||||
|
||||
declare global {
|
||||
DepStatus;
|
||||
|
|
|
|||
|
|
@ -1002,6 +1002,25 @@
|
|||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
{{-- RECIPE PAGE --}}
|
||||
@if (isset($searchStaticPageModel))
|
||||
<a data-bs-toggle="tooltip" href="{{ route('bo.static_pages.edit', $searchStaticPageModel) }}"
|
||||
title="{{ __('crud.helpers.update_model', ['model' => $searchStaticPageModel->type->label()]) }}" role="button"
|
||||
@class([
|
||||
'btn btn-primary w-100 text-start mt-1',
|
||||
'fw-bold active' =>
|
||||
url()->current() ===
|
||||
route('bo.static_pages.edit', $searchStaticPageModel->type->value()),
|
||||
])>
|
||||
<i @class([
|
||||
'fa-solid fa-file fa-fw',
|
||||
'fa-lg' =>
|
||||
url()->current() ===
|
||||
route('bo.static_pages.edit', $searchStaticPageModel->type->value()),
|
||||
])></i>
|
||||
{{ sprintf('Page %s', $searchStaticPageModel->type->label()) }}
|
||||
</a>
|
||||
@endif
|
||||
{{-- CLUB --}}
|
||||
<div class="dropdown">
|
||||
<button data-bs-toggle="dropdown" type="button" aria-expanded="false" @class([
|
||||
|
|
|
|||
|
|
@ -160,7 +160,8 @@
|
|||
!$staticPageModel->type->equals(StaticPageType::recruitment) &&
|
||||
!$staticPageModel->type->equals(StaticPageType::recipe) &&
|
||||
!$staticPageModel->type->equals(StaticPageType::product) &&
|
||||
!$staticPageModel->type->equals(StaticPageType::home))
|
||||
!$staticPageModel->type->equals(StaticPageType::home) &&
|
||||
!$staticPageModel->type->equals(StaticPageType::search))
|
||||
{{-- CONTENT --}}
|
||||
<div class="col-12 mb-3">
|
||||
<fieldset class="border p-3">
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
</li>
|
||||
|
||||
{{-- Search BTN --}}
|
||||
{{-- @include('fo.layouts.main-menu.search-btn', ['offcanvaId' => 'offcanvas-search']) --}}
|
||||
@include('fo.layouts.main-menu.search-btn')
|
||||
|
||||
{{-- LINKS --}}
|
||||
@include('fo.layouts.main-menu.links', [
|
||||
|
|
@ -32,7 +32,7 @@
|
|||
'header-export' => request()->routeIs('fo.*.export'),
|
||||
])>
|
||||
{{-- Search BTN --}}
|
||||
{{-- @include('fo.layouts.main-menu.search-btn', ['offcanvaId' => 'offcanvas-search']) --}}
|
||||
@include('fo.layouts.main-menu.search-btn')
|
||||
|
||||
{{-- LANGS --}}
|
||||
@include('fo.layouts.main-menu.locales')
|
||||
|
|
@ -51,4 +51,3 @@
|
|||
</nav>
|
||||
|
||||
@include('fo.layouts.main-menu.offcanvas-menu-mobile', ['id' => 'offcanvas-main-menu'])
|
||||
@include('fo.layouts.main-menu.offcanvas-search', ['id' => 'offcanvas-search'])
|
||||
|
|
|
|||
|
|
@ -1,124 +0,0 @@
|
|||
<div class="offcanvas offcanvas-top h-100 vw-100" id="{{ $id }}" aria-labelledby="{{ $id }}-label" tabindex="-1">
|
||||
<div class="offcanvas-body pt-0">
|
||||
<form class="container-fluid d-flex flex-column pt-0" role="search" action="#FIXEME:SEARCH" method="GET">
|
||||
<div class="row sticky-top bg-light pb-2 pt-4">
|
||||
<div class="col-12 d-flex justify-content-between align-items-center g-0 mb-2 flex-row">
|
||||
<button class="btn btn-link text-decoration-none f-bourton-base" data-bs-dismiss="offcanvas" type="button"
|
||||
aria-label="{{ __('Fermer le menu de recherche') }}">
|
||||
<i class="fa-solid fa-xmark me-2"></i>
|
||||
<span class="d-none d-sm-inline">{{ __('Fermer') }}</span>
|
||||
</button>
|
||||
|
||||
{{-- INPUT SEARCH --}}
|
||||
<div class="mx-auto">
|
||||
<div class="input-group flex-sm-row rounded-pill flex-row-reverse bg-white" id="input-group-search">
|
||||
<button class="btn btn-outline-dark shadow-none" id="btn-search-empty" type="button">
|
||||
<i class="fa-solid fa-xmark me-2"></i>
|
||||
<span class="visually-hidden">{{ __('Effacer le contenu la barre de recherche') }}</span>
|
||||
</button>
|
||||
<div class="form-floating">
|
||||
<input class="form-control fs-5 border-dark border-1 border-start-0 border-end-0 f-bourton-base shadow-none"
|
||||
id="search-input" type="search" placeholder="{{ __('Nom de ma recherche') }}">
|
||||
<label for="search-input">{{ __('Nom de ma recherche') }}</label>
|
||||
</div>
|
||||
<button class="btn btn-outline-dark shadow-none" type="submit" title="{{ __('Lancer la recherche') }}"
|
||||
aria-label="{{ __('Lancer la recherche') }}">
|
||||
<i class="fa-solid fa-magnifying-glass"></i>
|
||||
</button>
|
||||
@push('scripts')
|
||||
<script @if (!empty($nonce)) nonce="{{ $nonce }}" @endif>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const eraseBtn = document.getElementById('btn-search-empty'),
|
||||
searchInput = document.getElementById('search-input');
|
||||
eraseBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
searchInput.value = '';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="visually-hidden" id="{{ $id }}-label">{{ __('Menu de recherche') }}</span>
|
||||
</div>
|
||||
<div class="col mt-2 text-center">
|
||||
@php
|
||||
$searchFilters = ['recipes' => 'Recettes', 'products' => 'Produits', 'others' => 'Autres'];
|
||||
@endphp
|
||||
@foreach ($searchFilters as $name => $searchFilter)
|
||||
<div class="btn-group">
|
||||
<input class="btn-check" id="btn-check-{{ $name }}" name="{{ $name }}" type="checkbox"
|
||||
@checked(request()->get($name)) autocomplete="off">
|
||||
<label class="btn btn-primary mx-2 mb-3" for="btn-check-{{ $name }}">{{ $searchFilter }}</label>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
<div class="row flex-grow-1 py-3">
|
||||
<div class="col overflow-y text-center" aria-label="{{ __('Liste des résultats de la recherche') }}">
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
<span class="d-block fs-1">PRINT RESULTS CARDS</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col text-center">
|
||||
PAGINATION
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col text-center">
|
||||
LAST X NEWS ?? RULE ?
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<li class="nav-item no-wrap">
|
||||
<button class="btn btn-link" data-bs-toggle="offcanvas" data-bs-target="#{{ $offcanvaId }}"
|
||||
title="{{ __('Rechercher dans le site :appname', ['appname' => config('app.name')]) }}" aria-controls="{{ $offcanvaId }}">
|
||||
<a class="btn btn-link" href="{{ route('fo.search.index') }}"
|
||||
title="{{ __('Rechercher dans le site :appname', ['appname' => config('app.name')]) }}">
|
||||
<i class="fa-solid fa-magnifying-glass"></i>
|
||||
</button>
|
||||
</a>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{-- CONTENT --}}
|
||||
<section class="container mt-3">
|
||||
<div class="row py-5">
|
||||
<article class="col me-4">
|
||||
|
|
@ -56,10 +57,10 @@
|
|||
{!! clean($newsBreedingModel->content) !!}
|
||||
</div>
|
||||
</article>
|
||||
<aside class="col-lg-3 col-12">
|
||||
<aside class="col-12 col-lg-3">
|
||||
<div class="line-border-small bg-white px-2 py-3">
|
||||
<div class="row p-2">
|
||||
<div class="col text-uppercase fs-5 f-bourton-base">{{ _('Découvrez aussi') }}</div>
|
||||
<div class="col text-uppercase fs-5 f-bourton-base">{{ __('Découvrez aussi') }}</div>
|
||||
</div>
|
||||
@foreach ($asideNewsModels as $asideNews)
|
||||
<a class="text-decoration-none h-100"
|
||||
|
|
@ -71,7 +72,7 @@
|
|||
alt="{{ $asideNews->picture_alt }}">
|
||||
</div>
|
||||
<div class="col-8">
|
||||
{{ __($asideNews->title) }}
|
||||
{{ $asideNews->title }}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -3,31 +3,27 @@
|
|||
@section('title', str('Les élevages')->upper())
|
||||
@section('description', str('Les élevages')->ucFirst())
|
||||
@section('bg-offset', true)
|
||||
@section('content')
|
||||
|
||||
@section('content')
|
||||
<x-fo.breeding.section.header-section title="{{ __('NOS ELEVAGES') }}"
|
||||
subtitle="{{ __('L’amélioration du bien-être animal est au coeur de nos élevages et des éleveurs partenaires Le Gaulois. C’est pourquoi, nous avons mis en place des cahiers des charges spécifiques sur les aménagements des poulaillers, l’alimentation et la santé des volailles.') }}" />
|
||||
|
||||
<section class="position-relative line-border-large py-4" id="les-elevages">
|
||||
<div class="position-absolute h-100 w-100 top-0 z-0">
|
||||
<div class="bg-green-wood h-100 w-100" id=""></div>
|
||||
<div class="bg-green-wood h-100 w-100"></div>
|
||||
</div>
|
||||
|
||||
<x-fo.breeding.section.les-elevages.les-demarches-chez-le-gaulois />
|
||||
|
||||
@include('fo.partials.breeding.farming-poultry-other', [
|
||||
'commitmentId' => 4,
|
||||
'title' => 'L’élevage des volailles',
|
||||
'text' => nl2br(
|
||||
__(
|
||||
'Face aux préoccupations des consommateurs sur le bien-être animal, Le Gaulois vous dit tout sur ses :strongStartéleveurs partenaires:strongEnd et ses :strongStartconditions d’élevages de volailles.:strongEnd :br Chaque jour, nous collaborons avec les éleveurs partenaires engagés dans la :strongStartdémarche éthique et responsable:strongEnd pour améliorer le confort de nos volailles. Chez les éleveurs partenaires Le Gaulois, la :strongStartprotection animale:strongEnd ne cesse d’être l’enjeu majeur.',
|
||||
__('Face aux préoccupations des consommateurs sur le bien-être animal, Le Gaulois vous dit tout sur ses :strongStartéleveurs partenaires:strongEnd et ses :strongStartconditions d’élevages de volailles.:strongEnd :br Chaque jour, nous collaborons avec les éleveurs partenaires engagés dans la :strongStartdémarche éthique et responsable:strongEnd pour améliorer le confort de nos volailles. Chez les éleveurs partenaires Le Gaulois, la :strongStartprotection animale:strongEnd ne cesse d’être l’enjeu majeur.',
|
||||
[
|
||||
'strongStart' => '<strong>',
|
||||
'strongEnd' => '</strong>',
|
||||
'br' => '<br>',
|
||||
])),
|
||||
]
|
||||
)
|
||||
),
|
||||
])
|
||||
|
||||
</section>
|
||||
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -3,10 +3,9 @@
|
|||
@section('title', str('La marque 100% volaille française')->upper())
|
||||
@section('description', str('La marque 100% volaille française')->ucFirst())
|
||||
@section('bg-offset', true)
|
||||
|
||||
@section('content')
|
||||
|
||||
<x-fo.breeding.section.header-section title="{{ __('Démarche Oui c’est bon') }}" />
|
||||
|
||||
<section class="position-relative line-border-large" id="oui-c-est-bon">
|
||||
<div class="position-absolute h-100 w-100 top-0 z-0">
|
||||
<div class="bg-green-wood h-100 w-100" id="demarche-bg"></div>
|
||||
|
|
@ -20,7 +19,5 @@
|
|||
<div class="breeder-map" data-json="@json([])"></div>
|
||||
</div>
|
||||
</section> --}}
|
||||
|
||||
</section>
|
||||
{{-- <x-fo.breeding.find-poultry-breeder /> --}}
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -7,13 +7,11 @@
|
|||
|
||||
@section('content')
|
||||
@include('fo.partials.breeding.navigation')
|
||||
|
||||
@include('fo.layouts.header-banner', [
|
||||
'picto' => $staticPageModel->picture,
|
||||
'picto_alt' => $staticPageModel->picture_alt,
|
||||
'picto_title' => $staticPageModel->picture_title,
|
||||
])
|
||||
|
||||
<section id="l-elevage-des-volailles">
|
||||
@foreach ($newsBreedingModels as $newsBreedingModel)
|
||||
{{-- @foreach ($items as $newsBreedingModel) --}}
|
||||
|
|
@ -27,18 +25,18 @@
|
|||
'btnLink' => route('fo.breeding.article.show', [$staticPageModel, $newsBreedingModel]),
|
||||
])
|
||||
@endforeach
|
||||
|
||||
@include('fo.partials.breeding.farming-poultry-other', [
|
||||
'commitmentId' => 4,
|
||||
'title' => 'L\'élevage des volailles',
|
||||
'text' => nl2br(
|
||||
__(
|
||||
'Face aux préoccupations des consommateurs sur le bien-être animal, Le Gaulois vous dit tout sur ses :strongStartéleveurs partenaires:strongEnd et ses :strongStartconditions d\'élevages de volailles.:strongEnd :br Chaque jour, nous collaborons avec les éleveurs partenaires engagés dans la :strongStartdémarche éthique et responsable:strongEnd pour améliorer le confort de nos volailles. Chez les éleveurs partenaires Le Gaulois, la :strongStartprotection animale:strongEnd ne cesse d\'être l\'enjeu majeur.',
|
||||
__('Face aux préoccupations des consommateurs sur le bien-être animal, Le Gaulois vous dit tout sur ses :strongStartéleveurs partenaires:strongEnd et ses :strongStartconditions d\'élevages de volailles.:strongEnd :br Chaque jour, nous collaborons avec les éleveurs partenaires engagés dans la :strongStartdémarche éthique et responsable:strongEnd pour améliorer le confort de nos volailles. Chez les éleveurs partenaires Le Gaulois, la :strongStartprotection animale:strongEnd ne cesse d\'être l\'enjeu majeur.',
|
||||
[
|
||||
'strongStart' => '<strong>',
|
||||
'strongEnd' => '</strong>',
|
||||
'br' => '<br>',
|
||||
])),
|
||||
]
|
||||
)
|
||||
),
|
||||
])
|
||||
</section>
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -245,7 +245,7 @@
|
|||
<div class="row news-wrapper justify-content-center">
|
||||
<div class="col-12 col-md-6 col-xl-4">
|
||||
<div class="h-100 mx-2">
|
||||
@include('fo.partials.news-card', ['newsModel' => $newsModels->first()])
|
||||
@include('fo.partials.news-card', ['model' => $newsModels->first()])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -253,7 +253,7 @@
|
|||
{{-- 3 horizontal cards (only for devices with width > md size) --}}
|
||||
<div class="d-none d-md-flex flex-column justify-content-around col-md-6 col-xl-4">
|
||||
@foreach ($newsModels->slice(1, 3) as $newsModel)
|
||||
@include('fo.partials.news-card-tiny', ['newsModel' => $newsModel])
|
||||
@include('fo.partials.news-card-tiny', ['model' => $newsModel])
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
|
|
|||
|
|
@ -79,13 +79,13 @@
|
|||
</div>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-sm-6 col-lg-4 mb-4">
|
||||
@include('fo.partials.news-card-tiny', ['newsModel' => $newsModel])
|
||||
@include('fo.partials.news-card-tiny', ['model' => $newsModel])
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 col-lg-4 mb-4">
|
||||
@include('fo.partials.news-card-tiny', ['newsModel' => $newsModel])
|
||||
@include('fo.partials.news-card-tiny', ['model' => $newsModel])
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 col-lg-4 mb-4">
|
||||
@include('fo.partials.news-card-tiny', ['newsModel' => $newsModel])
|
||||
@include('fo.partials.news-card-tiny', ['model' => $newsModel])
|
||||
</div>
|
||||
</div>
|
||||
{{-- PRODUCT CARDS TINY VERSION --}}
|
||||
|
|
|
|||
45
resources/views/fo/pages/search.blade.php
Normal file
45
resources/views/fo/pages/search.blade.php
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
@extends('fo.layout', ['brParam' => $staticPageModel])
|
||||
|
||||
@section('title', $staticPageModel->seo_title)
|
||||
@section('description', $staticPageModel->seo_description)
|
||||
@section('breadcrumb', request()->route()->getName())
|
||||
@section('bg-offset', true)
|
||||
|
||||
@section('content')
|
||||
{{-- HEADER --}}
|
||||
@include('fo.layouts.header-banner')
|
||||
{{-- MAIN CONTENT --}}
|
||||
<section id="search-section" class="position-relative bg-light line-border-large overflow-hidden pt-5">
|
||||
<div class="container mb-5">
|
||||
@php
|
||||
$data = [
|
||||
'elements' => [
|
||||
[
|
||||
'name' => str(__('models.classes.product'))->ucFirst()->plural(),
|
||||
'class' => App\Models\Product::class,
|
||||
'models' => $productModels,
|
||||
],
|
||||
[
|
||||
'name' => str(__('models.classes.recipe'))->ucFirst()->plural(),
|
||||
'class' => App\Models\Recipe::class,
|
||||
'models' => $recipeModels,
|
||||
],
|
||||
[
|
||||
'name' => str(__('models.classes.news'))->ucFirst()->plural(),
|
||||
'class' => App\Models\News::class,
|
||||
'models' => $newsModels,
|
||||
],
|
||||
[
|
||||
'name' => str(trans_choice('models.classes.news_breeding', \INF))->ucFirst()->plural(),
|
||||
'class' => App\Models\NewsBreeding::class,
|
||||
'models' => $newsBreedingModels,
|
||||
],
|
||||
],
|
||||
'searchValue' => request()->input('texte'),
|
||||
'routeUrl' => route('fo.search.string'),
|
||||
];
|
||||
@endphp
|
||||
<div class="meilisearch-models" data-json='@json($data)'></div>
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
|
|
@ -26,9 +26,7 @@
|
|||
@endif
|
||||
<p class="mt-3">{{ str(strip_tags($newsBreedingModel->content))->limit('300') }}</p>
|
||||
@if (isset($newsModel))
|
||||
@include('fo.partials.news-card-tiny', [
|
||||
'newsModel' => $newsModel,
|
||||
])
|
||||
@include('fo.partials.news-card-tiny', ['model' => $newsModel])
|
||||
@endif
|
||||
@if (isset($btnLabel) && !empty($btnLabel))
|
||||
<a class="btn btn-{{ $btn_color }}" href="{{ $btnLink }}" title="{{ __('Accéder à la page') }}"
|
||||
|
|
|
|||
|
|
@ -4,9 +4,13 @@
|
|||
'bg-white-wood-big' => empty($noBg) && empty($bgReverse),
|
||||
])>
|
||||
<div class="position-relative z-2 row w-100 container mx-auto">
|
||||
<div class="col-12 position-relative z-1 text-center">
|
||||
<h2>{{ str(__($title))->upper() }}</h2>
|
||||
{!! clean($text) !!}
|
||||
<div class="col-12 position-relative z-1">
|
||||
@if(isset($title))
|
||||
<h2 @class([(isset($classTitle)) ? $classTitle : 'text-center'])>{{ str(__($title))->upper() }}</h2>
|
||||
@endif
|
||||
@if(isset($text))
|
||||
{!! clean($text) !!}
|
||||
@endif
|
||||
</div>
|
||||
<div class="col-12 mx-auto mt-4">
|
||||
<div id="swiper-{{ $id }}s" @class([
|
||||
|
|
@ -17,7 +21,7 @@
|
|||
<div class="swiper-wrapper w-fit">
|
||||
@foreach ($models as $model)
|
||||
<div class="swiper-slide p-md-4">
|
||||
@include("fo.partials.$id-card")
|
||||
@include($viewPath ?? "fo.partials.$id-card")
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
|
@ -42,7 +46,7 @@
|
|||
<a class="btn btn-secondary" href="{{ $btnRouteName }}"
|
||||
title="{{ __('Accéder à la page :routeName', ['routeName' => $btnRouteTitle]) }}"
|
||||
@if (isset($newTab) && $newTab) target="_blank" @endif>
|
||||
{{ str($btnText ?? __('voir toutes les recettes'))->upper() }}
|
||||
{{ str($btnText ?? __('voir toutes les recettes'))->upper() }}
|
||||
@if (isset($isYoutube) && $isYoutube)
|
||||
<img src="{{ Vite::asset('resources/assets/images/footer/socials/YT.svg') }}" alt="Logo Youtube">
|
||||
@endif
|
||||
|
|
|
|||
|
|
@ -25,9 +25,7 @@
|
|||
<p>{{ $subtext }}</p>
|
||||
@endif
|
||||
@if (isset($newsModel))
|
||||
@include('fo.partials.news-card-tiny', [
|
||||
'newsModel' => $newsModel,
|
||||
])
|
||||
@include('fo.partials.news-card-tiny', ['model' => $newsModel])
|
||||
@endif
|
||||
@if (isset($btnLabel) && !empty($btnLabel))
|
||||
<a class="btn btn-{{ $btn_color }}" href="{{ $btnLink }}" title="{{ __('Accéder à la page') }}"
|
||||
|
|
|
|||
1
resources/views/fo/partials/news-breeding-card.blade.php
Normal file
1
resources/views/fo/partials/news-breeding-card.blade.php
Normal file
|
|
@ -0,0 +1 @@
|
|||
@include('fo.partials.news-card', $model)
|
||||
|
|
@ -1,29 +1,29 @@
|
|||
<div class="news-small-card bottom-left-shadow border-0 bg-transparent">
|
||||
<a class="text-decoration-none" href="{{ route('fo.news.show', $newsModel->slug) }}"
|
||||
title="{{ __('Accéder à l\'article : :name', ['name' => $newsModel->title]) }}">
|
||||
<a class="text-decoration-none" href="{{ route('fo.news.show', $model->slug) }}"
|
||||
title="{{ __('Accéder à l\'article : :name', ['name' => $model->title]) }}">
|
||||
<div class="line-border-small bg-white p-3">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-4">
|
||||
<div class="ratio ratio-1x1">
|
||||
<img class="object-fit-cover"
|
||||
src="{{ asset($newsModel->thumbnail) }}"
|
||||
title="{{ $newsModel->thumbnail_title }}" alt="{{ $newsModel->thumbnail_alt }}">
|
||||
src="{{ asset($model->thumbnail) }}"
|
||||
title="{{ $model->thumbnail_title }}" alt="{{ $model->thumbnail_alt }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
{{-- TAG --}}
|
||||
<div class="news-tag text-uppercase f-bourton-base fs-5 me-3 px-2 py-1"
|
||||
style="background-color: {{ $newsModel->newsCategory->color }}; color: #fff">
|
||||
{{ $newsModel->newsCategory->title }}
|
||||
style="background-color: {{ $model->newsCategory->color }}; color: #fff">
|
||||
{{ $model->newsCategory->title }}
|
||||
</div>
|
||||
{{-- DATE --}}
|
||||
<p class="f-bakerie-rough mb-0">
|
||||
{{ ($newsModel->updated_at)->format('d/m/Y') }}
|
||||
{{ ($model->updated_at)->format('d/m/Y') }}
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-uppercase f-bourton-base m-0">
|
||||
{{ $newsModel->title }}
|
||||
{{ $model->title }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,22 +8,26 @@
|
|||
--}}
|
||||
|
||||
<div class="news-card bottom-left-shadow card h-100 border-0 bg-transparent">
|
||||
<a class="text-decoration-none h-100" href="{{ route('fo.news.show', $newsModel->slug) }}" title="{{ __('Accéder à l\'article : :name', ['name' => $newsModel->title]) }}">
|
||||
<a class="text-decoration-none h-100" href="{{ route('fo.news.show', $model->slug) }}"
|
||||
title="{{ __('Accéder à l\'article : :name', ['name' => $model->title]) }}">
|
||||
<div class="line-border-small h-100 bg-white">
|
||||
<div class="card-img-top position-relative">
|
||||
<img class="w-100 line-border-small" src="{{ asset($newsModel->thumbnail) }}" title="{{ $newsModel->thumbnail_title }}"
|
||||
alt="{{ $newsModel->thumbnail_alt }}">
|
||||
<div class="position-absolute tag">
|
||||
<p class="text-uppercase rotate-left f-bourton-base fs-5 px-2 py-1"
|
||||
style="background-color: {{ $newsModel->newsCategory->color }}; color: #fff">
|
||||
{{ $newsModel->newsCategory->title }}
|
||||
</p>
|
||||
</div>
|
||||
<img class="w-100 line-border-small" src="{{ asset($model->thumbnail ?? $model->picture) }}"
|
||||
title="{{ $model->thumbnail_title ?? $model->picture_title }}"
|
||||
alt="{{ $model->thumbnail_alt ?? $model->picture_alt }}">
|
||||
@if (isset($model->newsCategory))
|
||||
<div class="position-absolute tag">
|
||||
<p class="text-uppercase rotate-left f-bourton-base fs-5 px-2 py-1"
|
||||
style="background-color: {{ $model->newsCategory->color }}; color: #fff">
|
||||
{{ $model->newsCategory->title }}
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<p class="text-small f-bakerie-rough">{{ $newsModel->updated_at ->format('d/m/Y') }}</p>
|
||||
<p class="text-small f-bakerie-rough">{{ $model->updated_at ->format('d/m/Y') }}</p>
|
||||
<p class="text-uppercase fs-4 f-bourton-base">
|
||||
{{ $newsModel->title }}</p>
|
||||
{{ $model->title }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<div class="position-relative product-card bottom-left-shadow h-100 mt-5 py-3">
|
||||
<div class="position-relative product-card bottom-left-shadow h-100 py-5">
|
||||
<div class="line-border-medium bg-tertiary position-absolute w-100 h-75 left-0 start-0"></div>
|
||||
<div class="h-75 position-relative" title="{{ __('Accéder au produit : :name', ['name' => $model->label]) }}">
|
||||
<a class="d-flex justify-content-center text-decoration-none pb-5" href="{{ route('fo.products.show', $model->slug) }}">
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
@php
|
||||
$items = [];
|
||||
foreach ($model->products as $product) {
|
||||
$relation = isset($product->submeat) && !is_null($product->submeat) ? $product->submeat : $product->meat;
|
||||
$relation = isset($product->submeat) && !is_null($product->submeat) ? $product->submeat : ($product->meat);
|
||||
$items = array_merge($items, isset($relation) && !is_null($relation) ? [$relation->only('label')['label']] : []);
|
||||
}
|
||||
@endphp
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
<div class="recipe-card bottom-left-shadow h-100">
|
||||
|
||||
<div class="line-border-medium h-100 bg-white">
|
||||
<div class="position-relative d-flex justify-content-center">
|
||||
<a class="text-decoration-none" href="{{ route('fo.recipes.show', $model->slug) }}"
|
||||
|
|
@ -7,10 +6,9 @@
|
|||
<img class="w-100 line-border-medium" src="{{ asset($model->picture) }}" title="{{ $model->picture_title }}"
|
||||
alt="{{ $model->picture_alt }}" />
|
||||
</a>
|
||||
|
||||
{{-- FIXME: change packaging by the first product packaging
|
||||
<img class="w-25 position-absolute recipe-card-product rotate-left z-1 bottom-0" src="{{ $model->products[0]->packaging }}"
|
||||
title="{{ $model->products[0]->packaging_title }}" alt="{{ $model->products[0]->packaging_alt }}" /> --}}
|
||||
<img class="w-25 position-absolute recipe-card-product rotate-left z-1 bottom-0" src="{{ $model->products[0]->packaging }}"
|
||||
title="{{ $model->products[0]->packaging_title }}" alt="{{ $model->products[0]->packaging_alt }}" /> --}}
|
||||
<x-fo.btn-club-favorite :model="$model" />
|
||||
</div>
|
||||
<a class="text-decoration-none" href="{{ route('fo.recipes.show', $model->slug) }}"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ use App\Http\Controllers\Fo\HomeController;
|
|||
use App\Http\Controllers\Fo\NewsController;
|
||||
use App\Http\Controllers\Fo\ProductController;
|
||||
use App\Http\Controllers\Fo\RecipeController;
|
||||
use App\Http\Controllers\Fo\SearchController;
|
||||
use App\Http\Controllers\Fo\StaticPageController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
|
|
@ -32,6 +33,10 @@ Route::middleware(['throttle:web'])->name('fo.')->group(function () {
|
|||
Route::get('/conditions-generales-de-vente', [StaticPageController::class, 'selling'])->name('conditions.show');
|
||||
Route::get('/accessibilite', [StaticPageController::class, 'accessibility'])->name('accessibility.show');
|
||||
|
||||
// * SEARCH
|
||||
Route::get('/recherche', [SearchController::class, 'search'])->name('search.index');
|
||||
Route::post('/recherche', [SearchController::class, 'searchString'])->name('search.string');
|
||||
|
||||
// * CONTACT
|
||||
Route::get('/contact', [ContactController::class, 'contact'])->name('contact.show');
|
||||
Route::post('/contactSubmit', [ContactController::class, 'contactSubmit'])->name('contact.submit');
|
||||
|
|
@ -148,7 +153,6 @@ Route::middleware(['throttle:web'])->name('fo.')->group(function () {
|
|||
->where('unsubscribe', 'unsubscribe|unsubscribe.php|unsubscribe.html|newsletter')
|
||||
->name('unsubscribeNewsletter');
|
||||
|
||||
|
||||
// * BREEDERS
|
||||
Route::prefix('breeders')
|
||||
->name('breeders.')
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue