refactor: products.index and recipes.index pages on Bo

This commit is contained in:
Jérémy Munsch 2026-07-06 12:52:52 +02:00
parent a7992f96c8
commit 43c3d55b72
Signed by: forge-joniko
GPG key ID: D7EFE2F5CB6CC0E4
36 changed files with 1900 additions and 384 deletions

View file

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Exceptions;
class AuthServiceException extends \RuntimeException
{
public static function of(?string $message = null, ?\Throwable $cause = null): self
{
return new self($message ?? 'An authentication error occurred.', previous: $cause);
}
}

View file

@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Back;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Contracts\Session\Session;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Http\Request;
use Illuminate\Routing\Route;
use Illuminate\Support\Str;
use Uwil\LaravelBackend\Controllers\BackendController as UwilBackendController;
use Uwil\LaravelBackend\Enums\ItemsPerPaginationEnum;
use Uwil\LaravelCommon\Helpers\ToolboxHelper;
class BackendController extends UwilBackendController
{
use AuthorizesRequests;
use ValidatesRequests;
public function __construct(
protected readonly Request $request,
protected readonly Session $session,
) {
}
/**
* Customize pagination with cache or config (default).
*
* @template T of \Illuminate\Database\Eloquent\Model
*
* @param \Illuminate\Database\Eloquent\Builder<T> $query
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator<array-key,T>
*/
protected function paginate(
Builder $query,
?ItemsPerPaginationEnum $pagination = null
): LengthAwarePaginator {
$route = $this->request->route();
throw_if(
$route === null,
\UnexpectedValueException::class,
'Route is required in order to paginate'
);
$cacheKey = $this->getCacheKey($route);
$perPage = $this->getPerPage($cacheKey);
$this->persistPerPage($cacheKey, $perPage);
return $query->paginate(perPage: $perPage)->withQueryString();
}
private function persistPerPage(string $cacheKey, int $perPage): void
{
if ($this->session->get($cacheKey) !== $perPage) {
$this->session->put($cacheKey, $perPage);
}
}
private function getPerPage(string $cacheKey): int
{
$perPage = $this->session->get($cacheKey);
$perPage = $perPage ?? ItemsPerPaginationEnum::twelve->value();
if (! is_scalar($perPage)) {
throw new \UnexpectedValueException('perPage shall be a scalar valuye');
}
return ToolboxHelper::getValidatedEnum(
intval($perPage),
'pagination',
'\Uwil\LaravelBackend\Enums\ItemsPerPaginationEnum',
);
}
private function getCacheKey(Route $route): string
{
$currentRoutePath = Str::slug($route->getName() ?? 'unknown');
return "pagination.{$currentRoutePath}";
}
}

View file

@ -0,0 +1,241 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Back;
use App\Http\Requests\Back\Products\IndexProductRequest;
use App\Http\Requests\Back\Products\StoreProductRequest;
use App\Http\Requests\Back\Products\UpdateProductRequest;
use App\Models\Product;
use Illuminate\Contracts\Session\Session;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Inertia\Response;
use Uwil\LaravelBackend\Resources\CommonResource;
/**
* Handles CRUD operations for Product in the backend.
*/
class ProductController extends BackendController
{
public function __construct(
Request $request,
Session $session,
) {
parent::__construct(
$request,
$session
);
$this->authorizeResource(Product::class);
$this->modelName = Product::modelName();
$this->modelPage = Product::modelPage();
}
/**
* Display a listing of the resource.
*/
public function index(IndexProductRequest $request): Response
{
/** @var \Illuminate\Database\Eloquent\Builder<\App\Models\Product> $query */
$query = Product::query();
if ($request->routeIs("back.{$this->modelName}.trash")) {
$query = $query->onlyTrashed();
}
$searchValue = $request->getSearchValue();
if ($searchValue) {
$this->searchQuery(
$query,
$searchValue,
null,
'name'
);
}
$searchFields = \implode(', ', [
trans('validation.attributes.name'),
]);
/** Sort columns with a query */
$this->sortQuery($query);
/** Custom pagination */
$paginatedModels = $this->paginate($query);
return $this->inertiaRender("back/Pages/{$this->modelPage}/Index", [
'paginatedModels' => CommonResource::collection($paginatedModels),
'search' => $searchValue,
'searchFields' => $searchFields,
]);
}
/**
* Show the form for creating a new resource.
*/
public function create(Product $product): Response
{
return $this->inertiaRender("back/Pages/{$this->modelPage}/Create", [
'data' => CommonResource::make($product),
]);
}
/**
* Show a specific resource.
*/
public function show(Product $product): Response
{
return $this->inertiaRender("back/Pages/{$this->modelPage}/Show", [
'data' => CommonResource::make($product),
]);
}
/**
* Store a newly created resource.
*/
public function store(StoreProductRequest $request): RedirectResponse
{
return DB::transaction(function () use ($request) {
$product = new Product();
$product->inertiaFill($request->validated());
if ($product->saveOrFail()) {
return redirect()->route("back.{$this->modelName}.edit", $product)
->with('success', trans('uwil-backend::crud.messages.has_been_created', [
'model' => Str::ucfirst(trans_choice("models.classes.{$this->modelName}", 1)),
]));
}
return redirect()->back()
->with('error', trans('uwil-backend::crud.messages.cannot_be_created', [
'model' => Str::ucfirst(trans_choice("models.classes.{$this->modelName}", 1)),
]));
});
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Product $product): Response
{
return $this->inertiaRender("back/Pages/{$this->modelPage}/Edit", [
'data' => CommonResource::make($product),
]);
}
/**
* Update the specified resource.
*/
public function update(
UpdateProductRequest $request,
Product $product
): RedirectResponse {
return DB::transaction(function () use ($request, $product) {
$product->inertiaFill($request->validated());
if ($product->saveOrFail()) {
return redirect()->route("back.{$this->modelName}.edit", $product)
->with('success', trans('uwil-backend::crud.messages.has_been_updated', [
'model' => Str::ucfirst(trans_choice("models.classes.{$this->modelName}", 1)),
]));
}
return redirect()->route("back.{$this->modelName}.edit", $product)
->with('error', trans('uwil-backend::crud.messages.cannot_be_updated', [
'model' => Str::ucfirst(trans_choice("models.classes.{$this->modelName}", 1)),
]));
});
}
/**
* Duplicate the specified resource.
*/
public function duplicate(Product $product): Response
{
return $this->create($product->replicate());
}
/**
* Restore the specified resource.
*/
public function restore(int $product): RedirectResponse
{
$product = Product::withTrashed()->findOrFail($product);
$previousQueries = $this->getPreviousQueries();
if ($product->restore()) {
return redirect()->route("back.{$this->modelName}.trash", $previousQueries)
->with('success', trans('uwil-backend::crud.messages.has_been_restored', [
'model' => Str::ucfirst(trans_choice("models.classes.{$this->modelName}", 1)),
]));
}
return redirect()->back()
->with('error', trans('uwil-backend::crud.messages.cannot_be_restored', [
'model' => Str::ucfirst(trans_choice("models.classes.{$this->modelName}", 1)),
]));
}
/**
* Destroy the specified resource.
*/
public function destroy(Product $product): RedirectResponse
{
$previousQueries = $this->getPreviousQueries();
if ($product->delete()) {
return redirect()->route("back.{$this->modelName}.index", $previousQueries)
->with('success', trans('uwil-backend::crud.messages.has_been_deleted', [
'model' => Str::ucfirst(trans_choice("models.classes.{$this->modelName}", 1)),
]));
}
return redirect()->back()
->with('error', trans('uwil-backend::crud.messages.cannot_be_deleted', [
'model' => Str::ucfirst(trans_choice("models.classes.{$this->modelName}", 1)),
]));
}
/**
* Destroy the specified resource.
*/
public function forceDestroy(Product | int $product): RedirectResponse
{
if (is_int($product)) {
$product = Product::withTrashed()->findOrFail($product);
}
$previousQueries = $this->getPreviousQueries();
if ($product->forceDelete()) {
return redirect()->route("back.{$this->modelName}.trash", $previousQueries)
->with('success', trans('uwil-backend::crud.messages.has_been_deleted', [
'model' => Str::ucfirst(trans_choice("models.classes.{$this->modelName}", 1)),
]));
}
return redirect()->back()
->with('error', trans('uwil-backend::crud.messages.cannot_be_deleted', [
'model' => Str::ucfirst(trans_choice("models.classes.{$this->modelName}", 1)),
]));
}
/** @return array<array-key,string|array<mixed>> */
private function getPreviousQueries(): array
{
$previousQueries = [];
$previousQueryString = \parse_url($this->getPreviousUrl(), \PHP_URL_QUERY) ?? '' ?: '';
\parse_str($previousQueryString, $previousQueries);
return $previousQueries;
}
private function getPreviousUrl(): string
{
return url()->previous(route('back.dashboard.index'));
}
}

View file

@ -0,0 +1,241 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Back;
use App\Http\Requests\Back\Recipes\IndexRecipeRequest;
use App\Http\Requests\Back\Recipes\StoreRecipeRequest;
use App\Http\Requests\Back\Recipes\UpdateRecipeRequest;
use App\Models\Recipe;
use Illuminate\Contracts\Session\Session;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Inertia\Response;
use Uwil\LaravelBackend\Resources\CommonResource;
/**
* Handles CRUD operations for Recipe in the backend.
*/
class RecipeController extends BackendController
{
public function __construct(
Request $request,
Session $session,
) {
parent::__construct(
$request,
$session
);
$this->authorizeResource(Recipe::class);
$this->modelName = Recipe::modelName();
$this->modelPage = Recipe::modelPage();
}
/**
* Display a listing of the resource.
*/
public function index(IndexRecipeRequest $request): Response
{
/** @var \Illuminate\Database\Eloquent\Builder<\App\Models\Recipe> $query */
$query = Recipe::query();
if ($request->routeIs("back.{$this->modelName}.trash")) {
$query = $query->onlyTrashed();
}
$searchValue = $request->getSearchValue();
if ($searchValue) {
$this->searchQuery(
$query,
$searchValue,
null,
'name'
);
}
$searchFields = \implode(', ', [
trans('validation.attributes.name'),
]);
/** Sort columns with a query */
$this->sortQuery($query);
/** Custom pagination */
$paginatedModels = $this->paginate($query);
return $this->inertiaRender("back/Pages/{$this->modelPage}/Index", [
'paginatedModels' => CommonResource::collection($paginatedModels),
'search' => $searchValue,
'searchFields' => $searchFields,
]);
}
/**
* Show the form for creating a new resource.
*/
public function create(Recipe $recipe): Response
{
return $this->inertiaRender("back/Pages/{$this->modelPage}/Create", [
'data' => CommonResource::make($recipe),
]);
}
/**
* Show a specific resource.
*/
public function show(Recipe $recipe): Response
{
return $this->inertiaRender("back/Pages/{$this->modelPage}/Show", [
'data' => CommonResource::make($recipe),
]);
}
/**
* Store a newly created resource.
*/
public function store(StoreRecipeRequest $request): RedirectResponse
{
return DB::transaction(function () use ($request) {
$recipe = new Recipe();
$recipe->inertiaFill($request->validated());
if ($recipe->saveOrFail()) {
return redirect()->route("back.{$this->modelName}.edit", $recipe)
->with('success', trans('uwil-backend::crud.messages.has_been_created', [
'model' => Str::ucfirst(trans_choice("models.classes.{$this->modelName}", 1)),
]));
}
return redirect()->back()
->with('error', trans('uwil-backend::crud.messages.cannot_be_created', [
'model' => Str::ucfirst(trans_choice("models.classes.{$this->modelName}", 1)),
]));
});
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Recipe $recipe): Response
{
return $this->inertiaRender("back/Pages/{$this->modelPage}/Edit", [
'data' => CommonResource::make($recipe),
]);
}
/**
* Update the specified resource.
*/
public function update(
UpdateRecipeRequest $request,
Recipe $recipe
): RedirectResponse {
return DB::transaction(function () use ($request, $recipe) {
$recipe->inertiaFill($request->validated());
if ($recipe->saveOrFail()) {
return redirect()->route("back.{$this->modelName}.edit", $recipe)
->with('success', trans('uwil-backend::crud.messages.has_been_updated', [
'model' => Str::ucfirst(trans_choice("models.classes.{$this->modelName}", 1)),
]));
}
return redirect()->route("back.{$this->modelName}.edit", $recipe)
->with('error', trans('uwil-backend::crud.messages.cannot_be_updated', [
'model' => Str::ucfirst(trans_choice("models.classes.{$this->modelName}", 1)),
]));
});
}
/**
* Duplicate the specified resource.
*/
public function duplicate(Recipe $recipe): Response
{
return $this->create($recipe->replicate());
}
/**
* Restore the specified resource.
*/
public function restore(int $recipe): RedirectResponse
{
$recipe = Recipe::withTrashed()->findOrFail($recipe);
$previousQueries = $this->getPreviousQueries();
if ($recipe->restore()) {
return redirect()->route("back.{$this->modelName}.trash", $previousQueries)
->with('success', trans('uwil-backend::crud.messages.has_been_restored', [
'model' => Str::ucfirst(trans_choice("models.classes.{$this->modelName}", 1)),
]));
}
return redirect()->back()
->with('error', trans('uwil-backend::crud.messages.cannot_be_restored', [
'model' => Str::ucfirst(trans_choice("models.classes.{$this->modelName}", 1)),
]));
}
/**
* Destroy the specified resource.
*/
public function destroy(Recipe $recipe): RedirectResponse
{
$previousQueries = $this->getPreviousQueries();
if ($recipe->delete()) {
return redirect()->route("back.{$this->modelName}.index", $previousQueries)
->with('success', trans('uwil-backend::crud.messages.has_been_deleted', [
'model' => Str::ucfirst(trans_choice("models.classes.{$this->modelName}", 1)),
]));
}
return redirect()->back()
->with('error', trans('uwil-backend::crud.messages.cannot_be_deleted', [
'model' => Str::ucfirst(trans_choice("models.classes.{$this->modelName}", 1)),
]));
}
/**
* Destroy the specified resource.
*/
public function forceDestroy(Recipe | int $recipe): RedirectResponse
{
if (is_int($recipe)) {
$recipe = Recipe::withTrashed()->findOrFail($recipe);
}
$previousQueries = $this->getPreviousQueries();
if ($recipe->forceDelete()) {
return redirect()->route("back.{$this->modelName}.trash", $previousQueries)
->with('success', trans('uwil-backend::crud.messages.has_been_deleted', [
'model' => Str::ucfirst(trans_choice("models.classes.{$this->modelName}", 1)),
]));
}
return redirect()->back()
->with('error', trans('uwil-backend::crud.messages.cannot_be_deleted', [
'model' => Str::ucfirst(trans_choice("models.classes.{$this->modelName}", 1)),
]));
}
/** @return array<array-key,string|array<mixed>> */
private function getPreviousQueries(): array
{
$previousQueries = [];
$previousQueryString = \parse_url($this->getPreviousUrl(), \PHP_URL_QUERY) ?? '' ?: '';
\parse_str($previousQueryString, $previousQueries);
return $previousQueries;
}
private function getPreviousUrl(): string
{
return url()->previous(route('back.dashboard.index'));
}
}

View file

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
{
use AuthorizesRequests;
use DispatchesJobs;
use ValidatesRequests;
}

View file

@ -5,6 +5,7 @@ namespace App\Http\Middlewares;
use App\Enums\Contact\ActivitySectorEnum;
use App\Enums\Contact\ObjectEnum;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use Inertia\Middleware;
use Uwil\LaravelCommon\Helpers\InertiaHelper;
use Uwil\LaravelCommon\Support\InertiaSharedRegistry;
@ -17,7 +18,7 @@ class HandleInertiaRequests extends Middleware
* Define the props that are shared by default.
*
* @param \Illuminate\Http\Request $request
* @return array<string, mixed>
* @return array<array-key,mixed>
*/
public function share(Request $request): array
{
@ -30,18 +31,13 @@ class HandleInertiaRequests extends Middleware
foreach ($dynamicPageModels as $dynamicPageModel) {
$pascalCase =
str_replace(' ', '', ucwords(str_replace('-', ' ', $dynamicPageModel->id_key)));
str_replace(' ', '', ucwords(str_replace('-', ' ', $dynamicPageModel->id_key)));
$dynamicPages["page{$pascalCase}"] = $dynamicPageModel;
}
$props = [
...InertiaHelper::errorProps(parent::share($request)),
'recaptchaKey' => config('services.google.recaptcha.public'),
'dynamicPages' => $dynamicPages,
'activitySectorEnum' => ActivitySectorEnum::toArray(),
'objectEnum' => ObjectEnum::toArray()
// More props here.
];
@ -50,4 +46,13 @@ class HandleInertiaRequests extends Middleware
InertiaSharedRegistry::resolve($request)
);
}
public function shareOnce(Request $request): array
{
return array_merge(parent::shareOnce($request), [
'recaptchaKey' => fn() => Config::string('services.google.recaptcha.public'),
'activitySectorEnum' => fn() => ActivitySectorEnum::toArray(),
'objectEnum' => fn() => ObjectEnum::toArray()
]);
}
}

View file

@ -0,0 +1,167 @@
<?php
declare(strict_types=1);
namespace App\Http\Middlewares;
use Closure;
use Illuminate\Contracts\Container\Container;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Vite;
use Symfony\Component\HttpFoundation\Response;
class SecureHeaders
{
/**
* Enumerate headers which you do not want in your application's responses.
* Great starting point would be to go check out @Scott_Helme's:
* https://securityheaders.com/
*
* @var string[]
*/
private $unwantedHeaderList = [
'X-Powered-By',
'Server',
];
public function __construct(
private readonly Container $container
) {}
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request):(\Illuminate\Http\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$appUrl = Config::string('app.url');
$appDebug = Config::boolean('app.debug');
$telescopeEnabled = Config::boolean('telescope.enabled', false);
// * For Vite DEV
$host = \parse_url($appUrl, \PHP_URL_HOST);
$viteDevOrigin = $appDebug && \is_string($host) && $host !== ''
? "https://{$host}:4443"
: '';
$viteDevSocket = $appDebug && \is_string($host) && $host !== ''
? "wss://{$host}:4443"
: '';
$nonce = Vite::cspNonce();
$jsBo = $request->routeIs('bo*') ? "'unsafe-eval'" : '';
$jsdelivrUrl = 'https://cdn.jsdelivr.net';
$openStreetMapTiles = 'https://*.tile.openstreetmap.org';
$this->declareNonce($nonce);
$this->removeUnwantedHeaders($this->unwantedHeaderList);
$response = $next($request);
$response->headers->set('Referrer-Policy', 'no-referrer-when-downgrade');
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-XSS-Protection', '1; mode=block');
$response->headers->set('X-Frame-Options', 'SAMEORIGIN');
$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
// * Ignore CSP on some conditions.
if (
$appDebug && (
$response->isClientError() || // Ignition
$response->isServerError() || // Ignition
Vite::isRunningHot() || // Vite
($telescopeEnabled && str_starts_with($request->path(), 'telescope')) // Telescope
)
) {
return $response;
}
$cspPolicies = [
"default-src 'none';",
"base-uri 'self';",
"manifest-src 'self';",
"media-src 'self' blob: {$appUrl};",
// * CKEDITOR Specific https://ckeditor.com/docs/ckeditor5/latest/installation/advanced/csp.html
"connect-src 'self' blob: " . implode(' ', [
'https://yl.s-web.fr/',
// * BAN address autocomplete (api-adresse.data.gouv.fr)
'https://api-adresse.data.gouv.fr',
'https://maps.googleapis.com',
// * VITE DEV
$viteDevSocket,
$viteDevOrigin,
]) . ';',
"img-src 'self' data: blob: " . implode(' ', [
'https://maps.googleapis.com',
'https://maps.gstatic.com',
$viteDevOrigin,
$openStreetMapTiles,
]) . ' data:;',
'child-src ' . implode(' ', []) . ';',
"frame-src 'self' " . implode(' ', [
'https://www.google.com/recaptcha/',
'https://recaptcha.google.com/recaptcha/',
'https://www.google.com/maps/',
'https://www.youtube.com/embed/',
'https://player.vimeo.com/',
'https://e.issuu.com/embed.html?',
]) . ';',
"object-src 'none';",
"font-src 'self' data: " . \implode(' ', [
'https://fonts.gstatic.com',
// * VITE DEV
$viteDevOrigin,
]) . ';',
// ! Using CKEditor will must have unsafe-inline style
"style-src 'self' 'unsafe-inline' " . \implode(' ', [
$jsdelivrUrl,
'https://cdnjs.cloudflare.com',
'https://fonts.googleapis.com',
'https://use.fontawesome.com',
// * File Manager
// * VITE DEV
$viteDevOrigin,
]) . ';',
// ! If you dont use ckeditor use this
// "style-src 'self' 'nonce-$nonce';",
"script-src 'self' 'nonce-$nonce' 'strict-dynamic' " . \implode(' ', [
'https: blob:',
$jsBo,
'https://www.googletagmanager.com',
'https://cdnjs.cloudflare.com',
'https://www.google-analytics.com',
// * VITE DEV
$viteDevOrigin,
]) . ';',
];
$response->headers->set(
'Content-Security-Policy',
implode(' ', $cspPolicies)
);
$response->headers->set('Feature-Policy', 'fullscreen \'self\'; geolocation \'self\';');
return $response;
}
/**
* Remove unwanted headers
*
* @param string[] $headerList List of headers to remove from the response
* @return void
*/
private function removeUnwantedHeaders(array $headerList)
{
foreach ($headerList as $header) {
header_remove($header);
}
}
private function declareNonce(string $value): void
{
Vite::useCspNonce($value);
$this->container->singleton('csp_nonce', $value);
}
}

View file

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Back\Products;
use App\Services\AdminAuthService;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class IndexProductRequest extends FormRequest
{
public function __construct(
private readonly AdminAuthService $authService
) {
}
public function authorize(): bool
{
return $this->authService->check();
}
public function getSearchValue(): ?string
{
return $this->has('search') ?
$this->string('search')->value() : null;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'search' => ['sometimes', 'string'],
];
}
}

View file

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Back\Products;
use App\Services\AdminAuthService;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class StoreProductRequest extends FormRequest
{
public function __construct(
private readonly AdminAuthService $authService
) {}
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return $this->authService->check();
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
//
];
}
}

View file

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Back\Products;
use Illuminate\Contracts\Validation\ValidationRule;
class UpdateProductRequest extends StoreProductRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
//
];
}
}

View file

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Back\Recipes;
use App\Services\AdminAuthService;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class IndexRecipeRequest extends FormRequest
{
public function __construct(
private readonly AdminAuthService $authService
) {
}
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return $this->authService->check();
}
public function getSearchValue(): ?string
{
return $this->has('search') ?
$this->string('search')->value() : null;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'search' => ['sometimes', 'string'],
];
}
}

View file

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Back\Recipes;
use App\Services\AdminAuthService;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class StoreRecipeRequest extends FormRequest
{
public function __construct(
private readonly AdminAuthService $authService
) {}
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return $this->authService->check();
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
//
];
}
}

View file

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Back\Recipes;
use App\Services\AdminAuthService;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class UpdateRecipeRequest extends FormRequest
{
public function __construct(
private readonly AdminAuthService $authService
) {}
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return $this->authService->check();
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
//
];
}
}

View file

@ -16,7 +16,14 @@ use Illuminate\Database\Eloquent\Casts\AsEnumCollection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Str;
use Spatie\Translatable\HasTranslations;
use Uwil\LaravelActivityLogs\Traits\HasActivityLog;
use Uwil\LaravelBackend\Traits\Models\HasCache;
use Uwil\LaravelBackend\Traits\Models\HasModelNaming;
use Uwil\LaravelBackend\Traits\Models\HasUwilImages;
use Uwil\LaravelI18n\Interfaces\TranslatableModel;
/**
* @property int $id
@ -48,11 +55,19 @@ use Illuminate\Support\Str;
*
* @mixin \Eloquent
*/
class Product extends Model
class Product extends Model implements TranslatableModel
{
use HasActivityLog;
use HasCache;
/** @use HasFactory<\Database\Factories\ProductFactory> */
use HasFactory;
use HasModelNaming;
use HasTranslations;
use HasUwilImages;
use SoftDeletes;
protected $fillable = [
'seo_title',
'seo_description',

View file

@ -6,14 +6,21 @@ namespace App\Models;
use App\Lib\Helpers\FileStorageHelper;
use App\Lib\Helpers\ToolboxHelper;
use Domain\Models\ValueObjects\Recipes\Inspiration;
use Domain\Models\ValueObjects\Recipes\Season;
use Domain\Models\ValueObjects\Products\ProductRange;
use Domain\Models\ValueObjects\Recipes\ConsumptionTime;
use Domain\Models\ValueObjects\Recipes\Inspiration;
use Domain\Models\ValueObjects\Recipes\Season;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Str;
use Spatie\Translatable\HasTranslations;
use Uwil\LaravelActivityLogs\Traits\HasActivityLog;
use Uwil\LaravelBackend\Traits\Models\HasCache;
use Uwil\LaravelBackend\Traits\Models\HasModelNaming;
use Uwil\LaravelBackend\Traits\Models\HasUwilImages;
use Uwil\LaravelI18n\Interfaces\TranslatableModel;
/**
* @property int $id
@ -36,14 +43,24 @@ use Illuminate\Support\Str;
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Product> $products
*
* @method static \Database\Factories\RecipeFactory factory($count = null, $state = [])
*
* @mixin \Eloquent
*/
class Recipe extends Model
class Recipe extends Model implements TranslatableModel
{
use HasActivityLog;
use HasCache;
/** @use HasFactory<\Database\Factories\RecipeFactory> */
use HasFactory;
use HasModelNaming;
use HasTranslations;
use HasUwilImages;
use SoftDeletes;
protected $fillable = [
'seo_title',
'seo_description',
@ -59,7 +76,7 @@ class Recipe extends Model
'details',
'ingredients_list',
'preparation_steps',
'tips_and_advices'
'tips_and_advices',
];
protected $casts = [

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Policies;
use App\Models\Product;
use Uwil\LaravelBackend\Models\User;
class ProductPolicy
{
public function viewAny(User $_): bool
{
return true;
}
public function create(User $_): bool
{
return true;
}
public function update(User $_, Product $__): bool
{
return true;
}
public function delete(User $_, Product $__): bool
{
return true;
}
}

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Policies;
use App\Models\Recipe;
use Uwil\LaravelBackend\Models\User;
class RecipePolicy
{
public function viewAny(User $_): bool
{
return true;
}
public function create(User $_): bool
{
return true;
}
public function update(User $_, Recipe $__): bool
{
return true;
}
public function delete(User $_, Recipe $__): bool
{
return true;
}
}

View file

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Providers;
use App\Services\AdminAuthService;
use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(
AdminAuthService::class,
function () {
$guard = Auth::guard('backend');
return new AdminAuthService($guard);
}
);
}
}

View file

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Services;
use App\Exceptions\AuthServiceException;
use Illuminate\Contracts\Auth\StatefulGuard;
use Uwil\LaravelBackend\Models\User;
class AdminAuthService
{
public function __construct(
private readonly StatefulGuard $guard
) {}
public function canWithUser(\UnitEnum | string $abilities, mixed $arguments = []): bool
{
return $this->getUser()?->can($abilities, $arguments) ?? false;
}
public function check(): bool
{
return $this->guard->check();
}
public function guest(): bool
{
return ! $this->check();
}
public function getUser(): ?User
{
$user = $this->guard->user();
if ($user && ! ($user instanceof User)) {
throw new \InvalidArgumentException('Auth service needs a backend guard');
}
return $user ? clone $user : null;
}
public function getUserAsMandatory(): User
{
$user = $this->getUser();
if ($user === null) {
throw new AuthServiceException('Authenticated user is required from here');
}
return clone $user;
}
public function getMandatoryUserId(): string
{
return (string) $this->getUserAsMandatory()->id;
}
}

View file

@ -12,12 +12,15 @@ return Application::configure(basePath: dirname(__DIR__))
->withRouting(
health: '/up',
)
->withProviders([
\App\Providers\AppServiceProvider::class
])
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
\Uwil\LaravelI18n\Middlewares\ForceLocale::class,
\App\Http\Middlewares\HandleInertiaRequests::class,
\Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class,
\Uwil\LaravelCommon\Middlewares\SecureHeaders::class,
\App\Http\Middlewares\SecureHeaders::class,
\Uwil\LaravelCommon\Middlewares\TrimStrings::class,
\Uwil\LaravelCommon\Middlewares\TrustHosts::class,
\Uwil\LaravelCommon\Middlewares\TrustProxies::class,

105
bun.lock
View file

@ -58,6 +58,7 @@
"slugify": "^1.6.9",
"swiper": "^12.2.0",
"tailwind-merge": "^3.6.0",
"typescript": "^6.0.3",
"vaul": "^1.1.2",
"ziggy-js": "^2.6.3",
"zod": "^4.4.3",
@ -86,6 +87,7 @@
"@typescript-eslint/eslint-plugin": "^8.62.0",
"@typescript-eslint/parser": "^8.62.0",
"@uwil/husky-config": "^1.0.1",
"@vitejs/plugin-basic-ssl": "^2.3.0",
"@vitejs/plugin-react": "^6.0.3",
"autoprefixer": "^10.5.2",
"concurrently": "^9.2.3",
@ -113,6 +115,7 @@
"stylelint-scss": "^6.14.0",
"tailwindcss": "^3.4.19",
"tailwindcss-animate": "^1.0.7",
"typescript-eslint": "^8.62.1",
"vite": "^8.1.0",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-stylelint": "^6.3.0",
@ -608,46 +611,6 @@
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.62.0", "", { "dependencies": { "@typescript-eslint/types": "8.62.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-CY3uyFSRbcQv3nnSv8S0+lDftMVz6P963PoRlxrV7ew/Md564g9ut60PYzdLM5qW4jFn93GBF+Soi90ISAN+GQ=="],
"@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.1-rc", "", { "os": "aix", "cpu": "ppc64" }, "sha512-oqq2ZfEJ7BQuufcC3QBQndZLPNyamYNHLao8lKRBeeSkZKypBqxPSgkzrcFZtbYcIaBvpiyUnQP9MT7DEYHWbw=="],
"@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.1-rc", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Slc0yTftT2F/uGDmtPst8ijydneL6uZaLEyr2UjahxZpbhTjHFBJ5agXtVz/TL4A+ldxzjzj+E8QtLZlh/5mXw=="],
"@typescript/typescript-darwin-x64": ["@typescript/typescript-darwin-x64@7.0.1-rc", "", { "os": "darwin", "cpu": "x64" }, "sha512-h68iFW/LbA1/BsGgSRGFw981/3s1f/rY27YrmeZNuN+ly7dI+fiDduwT9ZT9866x2onoKNRq7PTyxSKyKDzfAQ=="],
"@typescript/typescript-freebsd-arm64": ["@typescript/typescript-freebsd-arm64@7.0.1-rc", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-DE+ppd8Ix2c6OMuRkKY4PJ4hngMGJ9M95OQUP17p9xL/1IKXof7npIeuusMN/bgL5o5JzMfSGh+N+5scTYRg0Q=="],
"@typescript/typescript-freebsd-x64": ["@typescript/typescript-freebsd-x64@7.0.1-rc", "", { "os": "freebsd", "cpu": "x64" }, "sha512-ST1ozHMw0u+CLOnWkcTyWDMV4Qn9osZ6fd1V1lnKDM1t0hZIp81mdGpdHxyHJjd7jdGrb6Gb/QXcZ1uqZ0t5zw=="],
"@typescript/typescript-linux-arm": ["@typescript/typescript-linux-arm@7.0.1-rc", "", { "os": "linux", "cpu": "arm" }, "sha512-gHmHwT5Naq5CKM8g9bbaGeEpnwQEvWCLn3fwP4K2m61VQdDKkPk0Dhab/OoZ4LV2SrMddmclYXTzpyg23YGt5g=="],
"@typescript/typescript-linux-arm64": ["@typescript/typescript-linux-arm64@7.0.1-rc", "", { "os": "linux", "cpu": "arm64" }, "sha512-N46pRihK3t5zD5MUtTQcdmQUqr1WI4U2nxno1gLwOtRSsB4krFkRjPHcQNG7h2DtRkX64rQiReX6WKwg2wprMA=="],
"@typescript/typescript-linux-loong64": ["@typescript/typescript-linux-loong64@7.0.1-rc", "", { "os": "linux", "cpu": "none" }, "sha512-G17Sao312rgiPBTh2F4nOpLpa3CcnBSaNhqNghZk2LNhnsp1RaMO5HMq2me21gqu9xLpc6CIgHtOzU6JBgNlfg=="],
"@typescript/typescript-linux-mips64el": ["@typescript/typescript-linux-mips64el@7.0.1-rc", "", { "os": "linux", "cpu": "none" }, "sha512-0FQspOb5UsQ4tQKvWgUO3pS9OIWkP7/8dPRWq+CRazJUeQZ4LBjtYK52jg5iIOrvItrVl2CwvRtrU3/9OihwJg=="],
"@typescript/typescript-linux-ppc64": ["@typescript/typescript-linux-ppc64@7.0.1-rc", "", { "os": "linux", "cpu": "ppc64" }, "sha512-SUmwfVBEv6A2Ld0eWfcvW0FqrgemfQL8jFGOmV1qYxsDqumjE5DekHXqbstgmbE4SHr4rrjHjvmuGCY+kTH/vw=="],
"@typescript/typescript-linux-riscv64": ["@typescript/typescript-linux-riscv64@7.0.1-rc", "", { "os": "linux", "cpu": "none" }, "sha512-rxeqnNnGiYzv/LlPHi/3+4p0ooR1cNJLjRIHXKovtiVmxXGJq6gtw8VSpbHuWPekyFMXgIAoLCZN0SQ51rAALQ=="],
"@typescript/typescript-linux-s390x": ["@typescript/typescript-linux-s390x@7.0.1-rc", "", { "os": "linux", "cpu": "s390x" }, "sha512-RYWCHCiPypxajdRHM2CNK/eM22e4Ex5TTjV2pXf7PTtBowGr0xX8i8kIMknyZS0LX2QfleYHouaoMVsFDSle3g=="],
"@typescript/typescript-linux-x64": ["@typescript/typescript-linux-x64@7.0.1-rc", "", { "os": "linux", "cpu": "x64" }, "sha512-PfLJSu0JzroDkqw2m4nqflPEcn8yev0m/vHFQlY9EzHorzjR6QG0wL8AJHvnD1e6h1s76AZngJ5u+z1K/D/HKw=="],
"@typescript/typescript-netbsd-arm64": ["@typescript/typescript-netbsd-arm64@7.0.1-rc", "", { "os": "none", "cpu": "arm64" }, "sha512-FfbPxH3dTfp8yVIaNM7bdWTixXuyxpzoemluqcqMROSIz+ImpCG3Q9HO9Ptzp9/giv+P9YYEnCMSXh61migj2w=="],
"@typescript/typescript-netbsd-x64": ["@typescript/typescript-netbsd-x64@7.0.1-rc", "", { "os": "none", "cpu": "x64" }, "sha512-FzdTfSzhRYb6hlav6K3cI5RVgcvCTvNAu/vc+t7B6AmZkThQ+t/1ntnvT5fnHmY1Az2RIBw7/b+qtCEG61HJTQ=="],
"@typescript/typescript-openbsd-arm64": ["@typescript/typescript-openbsd-arm64@7.0.1-rc", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-PQGhlxfNig+0YQ9Wwzd0USPBkt6w/ZqkBQWsU7G/0JkTzunJel+jSWwhKw4947pak/m7hGSeYiI04xReDLGZww=="],
"@typescript/typescript-openbsd-x64": ["@typescript/typescript-openbsd-x64@7.0.1-rc", "", { "os": "openbsd", "cpu": "x64" }, "sha512-WJ7NYgO2mHmLUkI/tZ+hl8lFd26QPJqO8ONOHNuYbdsybLvSB6B6sep222JIVrOfPRDGvFinbGGB+l3m1FWRWA=="],
"@typescript/typescript-sunos-x64": ["@typescript/typescript-sunos-x64@7.0.1-rc", "", { "os": "sunos", "cpu": "x64" }, "sha512-UYjDeUxd765V9qcwlUPk4pEXyL0i3G76CJm9baK4i99u1pGO1psf3nXDw4MMmElVOPvGbZag99ZR/O59E2OX6w=="],
"@typescript/typescript-win32-arm64": ["@typescript/typescript-win32-arm64@7.0.1-rc", "", { "os": "win32", "cpu": "arm64" }, "sha512-KzXzFSXZOm7zvEt2Aw0MsB2LbTL88znAiVqTDNAOHdlEb7brgmUQh/X2wM/8Be+N0fjEqWKl65cBKNwpWEbJiw=="],
"@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.1-rc", "", { "os": "win32", "cpu": "x64" }, "sha512-98R3+OqDr/r0/PLWEoXu88AE0lGVLNd335Ew8ONgzK1JWkNs4ou/5BGt3Or1ij4iXjH+c7PRL+jFjCbtWze+EA=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.2", "", {}, "sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA=="],
"@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.12.2", "", { "os": "android", "cpu": "arm" }, "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w=="],
@ -696,6 +659,8 @@
"@uwil/husky-config": ["@uwil/husky-config@1.0.1", "https://lab.s-web.fr/api/v4/projects/259/packages/npm/@uwil/husky-config/-/@uwil/husky-config-1.0.1.tgz", { "dependencies": { "@commitlint/cli": "^20.5.0", "@commitlint/config-conventional": "^20.5.0", "husky": "^9.1.7" } }, "sha1-VGchal1LcEFXNFsQZxh5pn4495Q="],
"@vitejs/plugin-basic-ssl": ["@vitejs/plugin-basic-ssl@2.3.0", "", { "peerDependencies": { "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-bdyo8rB3NnQbikdMpHaML9Z1OZPBu6fFOBo+OtxsBlvMJtysWskmBcnbIDhUqgC8tcxNv/a+BcV5U+2nQMm1OQ=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.3", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg=="],
"acorn": ["acorn@8.17.0", "", { "bin": "bin/acorn" }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="],
@ -1672,7 +1637,9 @@
"typed-array-length": ["typed-array-length@1.0.8", "", { "dependencies": { "call-bind": "^1.0.9", "for-each": "^0.3.5", "gopd": "^1.2.0", "is-typed-array": "^1.1.15", "possible-typed-array-names": "^1.1.0", "reflect.getprototypeof": "^1.0.10" } }, "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g=="],
"typescript": ["typescript@7.0.1-rc", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.1-rc", "@typescript/typescript-darwin-arm64": "7.0.1-rc", "@typescript/typescript-darwin-x64": "7.0.1-rc", "@typescript/typescript-freebsd-arm64": "7.0.1-rc", "@typescript/typescript-freebsd-x64": "7.0.1-rc", "@typescript/typescript-linux-arm": "7.0.1-rc", "@typescript/typescript-linux-arm64": "7.0.1-rc", "@typescript/typescript-linux-loong64": "7.0.1-rc", "@typescript/typescript-linux-mips64el": "7.0.1-rc", "@typescript/typescript-linux-ppc64": "7.0.1-rc", "@typescript/typescript-linux-riscv64": "7.0.1-rc", "@typescript/typescript-linux-s390x": "7.0.1-rc", "@typescript/typescript-linux-x64": "7.0.1-rc", "@typescript/typescript-netbsd-arm64": "7.0.1-rc", "@typescript/typescript-netbsd-x64": "7.0.1-rc", "@typescript/typescript-openbsd-arm64": "7.0.1-rc", "@typescript/typescript-openbsd-x64": "7.0.1-rc", "@typescript/typescript-sunos-x64": "7.0.1-rc", "@typescript/typescript-win32-arm64": "7.0.1-rc", "@typescript/typescript-win32-x64": "7.0.1-rc" }, "bin": { "tsc": "bin/tsc" } }, "sha512-drEP77wK7CCDlPfXZH4e008UUQOsw1DFmHmZOZjuNA+yoDLLnSNMZRXi90NbV/1LVo7SbNLq1bs3jjvk49TEqQ=="],
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
"typescript-eslint": ["typescript-eslint@8.62.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.62.1", "@typescript-eslint/parser": "8.62.1", "@typescript-eslint/typescript-estree": "8.62.1", "@typescript-eslint/utils": "8.62.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-vymnnM5g0AKQDSAyfP12nMIBvgwgA42syg74kkuZ4x1VuTzwQKwc5h9rGxeShCjny5o+zWAb6OEoz7XLgrIkIw=="],
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
@ -1846,6 +1813,14 @@
"tailwindcss/resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="],
"typescript-eslint/@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.62.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.62.1", "@typescript-eslint/type-utils": "8.62.1", "@typescript-eslint/utils": "8.62.1", "@typescript-eslint/visitor-keys": "8.62.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.62.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA=="],
"typescript-eslint/@typescript-eslint/parser": ["@typescript-eslint/parser@8.62.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.62.1", "@typescript-eslint/types": "8.62.1", "@typescript-eslint/typescript-estree": "8.62.1", "@typescript-eslint/visitor-keys": "8.62.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA=="],
"typescript-eslint/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.62.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.62.1", "@typescript-eslint/tsconfig-utils": "8.62.1", "@typescript-eslint/types": "8.62.1", "@typescript-eslint/visitor-keys": "8.62.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA=="],
"typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.62.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.62.1", "@typescript-eslint/types": "8.62.1", "@typescript-eslint/typescript-estree": "8.62.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g=="],
"vite-plugin-full-reload/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
"vite-plugin-stylelint/@rollup/pluginutils": ["@rollup/pluginutils@5.4.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" } }, "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg=="],
@ -1864,6 +1839,54 @@
"table/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.62.1", "", { "dependencies": { "@typescript-eslint/types": "8.62.1", "@typescript-eslint/visitor-keys": "8.62.1" } }, "sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg=="],
"typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.62.1", "", { "dependencies": { "@typescript-eslint/types": "8.62.1", "@typescript-eslint/typescript-estree": "8.62.1", "@typescript-eslint/utils": "8.62.1", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg=="],
"typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.62.1", "", { "dependencies": { "@typescript-eslint/types": "8.62.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g=="],
"typescript-eslint/@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.62.1", "", { "dependencies": { "@typescript-eslint/types": "8.62.1", "@typescript-eslint/visitor-keys": "8.62.1" } }, "sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg=="],
"typescript-eslint/@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.62.1", "", {}, "sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q=="],
"typescript-eslint/@typescript-eslint/parser/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.62.1", "", { "dependencies": { "@typescript-eslint/types": "8.62.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g=="],
"typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.62.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.62.1", "@typescript-eslint/types": "^8.62.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg=="],
"typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.62.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g=="],
"typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.62.1", "", {}, "sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q=="],
"typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.62.1", "", { "dependencies": { "@typescript-eslint/types": "8.62.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g=="],
"typescript-eslint/@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
"typescript-eslint/@typescript-eslint/typescript-estree/semver": ["semver@7.8.5", "", { "bin": "bin/semver.js" }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.62.1", "", { "dependencies": { "@typescript-eslint/types": "8.62.1", "@typescript-eslint/visitor-keys": "8.62.1" } }, "sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg=="],
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.62.1", "", {}, "sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.62.1", "", {}, "sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q=="],
"typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.62.1", "", {}, "sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q=="],
"typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.62.1", "", {}, "sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q=="],
"typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
"typescript-eslint/@typescript-eslint/parser/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
"typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
"typescript-eslint/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.62.1", "", { "dependencies": { "@typescript-eslint/types": "8.62.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g=="],
"typescript-eslint/@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
}
}

View file

@ -6,7 +6,7 @@ use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class() extends Migration
return new class () extends Migration
{
public function up(): void
{
@ -42,6 +42,8 @@ return new class() extends Migration
$table->string('pdf')->nullable();
$table->softDeletes();
$table->timestamps();
});
}

View file

@ -6,7 +6,7 @@ use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class() extends Migration
return new class () extends Migration
{
public function up(): void
{
@ -35,6 +35,8 @@ return new class() extends Migration
$table->text('preparation_steps');
$table->text('tips_and_advices')->nullable();
$table->softDeletes();
$table->timestamps();
});
}

View file

@ -3,7 +3,8 @@
declare(strict_types=1);
return [
"classes" => [
// "users" => "utilisateur|utilisateurs",
'classes' => [
'product' => 'product|products',
'recipe' => 'recipe|recipes',
],
];

View file

@ -3,7 +3,8 @@
declare(strict_types=1);
return [
"classes" => [
// "users" => "utilisateur|utilisateurs",
'classes' => [
'product' => 'produit|produits',
'recipe' => 'recette|recettes',
],
];

View file

@ -168,12 +168,14 @@ return [
'available' => 'disponible',
'birthday' => 'anniversaire',
'body' => 'corps',
'category' => 'catégorie',
'city' => 'ville',
'color' => 'color',
'company' => 'entreprise',
'compilation' => 'compilation',
'concept' => 'concept',
'conditions' => 'conditions',
'consomption_time' => 'moment de consommation',
'content' => 'contenu',
'contest' => 'contest',
'country' => 'pays',
@ -206,7 +208,9 @@ return [
'image_main' => 'image principale',
'image_mobile' => 'image mobile',
'images' => 'images',
'inspiration' => 'inspiration',
'is_audience_winner' => 'est le gagnant du public',
'is_egalim' => 'EGalim',
'is_hidden' => 'est caché',
'is_subscribed' => 'est abonné',
'is_visible' => 'est visible',
@ -235,6 +239,7 @@ return [
'preview' => 'aperçu',
'price' => 'prix',
'product_id' => 'identifiant du produit',
'product_range' => 'gamme',
'product_uid' => 'UID du produit',
'product_uuid' => 'UUID du produit',
'promo_code' => 'code promo',
@ -252,6 +257,7 @@ return [
'role' => 'rôle',
'rule' => 'règle',
'rules' => 'règles',
'season' => 'saison',
'second' => 'seconde',
'sex' => 'sexe',
'shipment' => 'expédition',
@ -269,6 +275,7 @@ return [
'tag' => 'mot clé',
'tags' => 'mots clés',
'teacher' => 'professeur',
'technology' => 'technologie',
'terms' => 'conditions',
'test_description' => 'description du test',
'test_locale' => 'localisation du test',

486
package-lock.json generated
View file

@ -60,6 +60,7 @@
"slugify": "^1.6.9",
"swiper": "^12.2.0",
"tailwind-merge": "^3.6.0",
"typescript": "^6.0.3",
"vaul": "^1.1.2",
"ziggy-js": "^2.6.3",
"zod": "^4.4.3",
@ -88,6 +89,7 @@
"@typescript-eslint/eslint-plugin": "^8.62.0",
"@typescript-eslint/parser": "^8.62.0",
"@uwil/husky-config": "^1.0.1",
"@vitejs/plugin-basic-ssl": "^2.3.0",
"@vitejs/plugin-react": "^6.0.3",
"autoprefixer": "^10.5.2",
"concurrently": "^9.2.3",
@ -115,6 +117,7 @@
"stylelint-scss": "^6.14.0",
"tailwindcss": "^3.4.19",
"tailwindcss-animate": "^1.0.7",
"typescript-eslint": "^8.62.1",
"vite": "^8.1.0",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-stylelint": "^6.3.0"
@ -4343,17 +4346,17 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.0.tgz",
"integrity": "sha512-o+mpz7EYiMzXoySXiKmzlabIvTVqUuK5yLrAedRPRDA0IpPFMUV1IXt6OqljIxX/kumN6EjUYp41Hqelh6p/Dw==",
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.1.tgz",
"integrity": "sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.62.0",
"@typescript-eslint/type-utils": "8.62.0",
"@typescript-eslint/utils": "8.62.0",
"@typescript-eslint/visitor-keys": "8.62.0",
"@typescript-eslint/scope-manager": "8.62.1",
"@typescript-eslint/type-utils": "8.62.1",
"@typescript-eslint/utils": "8.62.1",
"@typescript-eslint/visitor-keys": "8.62.1",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@ -4366,21 +4369,103 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.62.0",
"@typescript-eslint/parser": "^8.62.1",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": {
"version": "8.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.62.0.tgz",
"integrity": "sha512-+g5O3j0w2ldzC86Pv6fvbO/xhAonbJFIdf/MKQ1d30gndlsVzUOE83ldfSE15Qrl9fhFjK6AovHs5Wpp6vx86w==",
"node_modules/@typescript-eslint/parser": {
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.1.tgz",
"integrity": "sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.62.0",
"@typescript-eslint/typescript-estree": "8.62.0",
"@typescript-eslint/utils": "8.62.0",
"@typescript-eslint/scope-manager": "8.62.1",
"@typescript-eslint/types": "8.62.1",
"@typescript-eslint/typescript-estree": "8.62.1",
"@typescript-eslint/visitor-keys": "8.62.1",
"debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.1.tgz",
"integrity": "sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.62.1",
"@typescript-eslint/types": "^8.62.1",
"debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.62.1.tgz",
"integrity": "sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.62.1",
"@typescript-eslint/visitor-keys": "8.62.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.1.tgz",
"integrity": "sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.62.1.tgz",
"integrity": "sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.62.1",
"@typescript-eslint/typescript-estree": "8.62.1",
"@typescript-eslint/utils": "8.62.1",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
@ -4396,17 +4481,31 @@
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": {
"version": "8.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.0.tgz",
"integrity": "sha512-+hVbNxtW64pIcZWDPGbyaKF7vp2IBTVY5ma1blwwksrjdsbdqqEKvJWMGbBofei4F6Dovx1M0RJgoFeNu2279A==",
"node_modules/@typescript-eslint/types": {
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.1.tgz",
"integrity": "sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.1.tgz",
"integrity": "sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.62.0",
"@typescript-eslint/tsconfig-utils": "8.62.0",
"@typescript-eslint/types": "8.62.0",
"@typescript-eslint/visitor-keys": "8.62.0",
"@typescript-eslint/project-service": "8.62.1",
"@typescript-eslint/tsconfig-utils": "8.62.1",
"@typescript-eslint/types": "8.62.1",
"@typescript-eslint/visitor-keys": "8.62.1",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@ -4424,56 +4523,17 @@
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": {
"version": "8.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.0.tgz",
"integrity": "sha512-wexnCqiTg7BOGtbLDftYpRWlmLq4xfoMd7BKFR6Y75sZS3QmRKLdN3yWLhmIYgqMmP/OXWpj3H8odkb5nGURCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.62.0",
"@typescript-eslint/types": "^8.62.0",
"debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.0.tgz",
"integrity": "sha512-y2GAdB6ykaXUvuspbYnizQc4oDDz0Tz/Yc7iWrXf9mx8vm/L/0vLHCe0tS2boG96Zy+DivnVDQ9ZUEWoHqqx1g==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": {
"version": "8.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.62.0.tgz",
"integrity": "sha512-82r66fi9zYwZ+mTq3vKgwjbZ1PVk/DJzrXFLpG6RnBbdvH8TEGVHIs9H4d2drhkOzf0syZuD/OZvvlu6GDbP4g==",
"node_modules/@typescript-eslint/utils": {
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.62.1.tgz",
"integrity": "sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.62.0",
"@typescript-eslint/types": "8.62.0",
"@typescript-eslint/typescript-estree": "8.62.0"
"@typescript-eslint/scope-manager": "8.62.1",
"@typescript-eslint/types": "8.62.1",
"@typescript-eslint/typescript-estree": "8.62.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -4487,231 +4547,14 @@
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": {
"version": "8.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.0.tgz",
"integrity": "sha512-+hVbNxtW64pIcZWDPGbyaKF7vp2IBTVY5ma1blwwksrjdsbdqqEKvJWMGbBofei4F6Dovx1M0RJgoFeNu2279A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.62.0",
"@typescript-eslint/tsconfig-utils": "8.62.0",
"@typescript-eslint/types": "8.62.0",
"@typescript-eslint/visitor-keys": "8.62.0",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
"tinyglobby": "^0.2.15",
"ts-api-utils": "^2.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": {
"version": "8.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.0.tgz",
"integrity": "sha512-wexnCqiTg7BOGtbLDftYpRWlmLq4xfoMd7BKFR6Y75sZS3QmRKLdN3yWLhmIYgqMmP/OXWpj3H8odkb5nGURCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.62.0",
"@typescript-eslint/types": "^8.62.0",
"debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.0.tgz",
"integrity": "sha512-y2GAdB6ykaXUvuspbYnizQc4oDDz0Tz/Yc7iWrXf9mx8vm/L/0vLHCe0tS2boG96Zy+DivnVDQ9ZUEWoHqqx1g==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ts-api-utils": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
"integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.12"
},
"peerDependencies": {
"typescript": ">=4.8.4"
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.0.tgz",
"integrity": "sha512-dzHeT2gySzZtLDsuqxU9AkYgIsQoHAHtRBpOqM+Ofzx1Bwrd2RcCjQJ+6iQbsHOIR6NS33bF2W1k3blN1zLDrA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.62.0",
"@typescript-eslint/types": "8.62.0",
"@typescript-eslint/typescript-estree": "8.62.0",
"@typescript-eslint/visitor-keys": "8.62.0",
"debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": {
"version": "8.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.0.tgz",
"integrity": "sha512-+hVbNxtW64pIcZWDPGbyaKF7vp2IBTVY5ma1blwwksrjdsbdqqEKvJWMGbBofei4F6Dovx1M0RJgoFeNu2279A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.62.0",
"@typescript-eslint/tsconfig-utils": "8.62.0",
"@typescript-eslint/types": "8.62.0",
"@typescript-eslint/visitor-keys": "8.62.0",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
"tinyglobby": "^0.2.15",
"ts-api-utils": "^2.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": {
"version": "8.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.0.tgz",
"integrity": "sha512-wexnCqiTg7BOGtbLDftYpRWlmLq4xfoMd7BKFR6Y75sZS3QmRKLdN3yWLhmIYgqMmP/OXWpj3H8odkb5nGURCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.62.0",
"@typescript-eslint/types": "^8.62.0",
"debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.0.tgz",
"integrity": "sha512-y2GAdB6ykaXUvuspbYnizQc4oDDz0Tz/Yc7iWrXf9mx8vm/L/0vLHCe0tS2boG96Zy+DivnVDQ9ZUEWoHqqx1g==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree/node_modules/ts-api-utils": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
"integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.12"
},
"peerDependencies": {
"typescript": ">=4.8.4"
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.62.0.tgz",
"integrity": "sha512-1lX38kNxXIRb8mEc3lbq5mdHq1Pf2+U0nFU65KfT18mtPxxl0fvjuEE92mHuXPuCtElJhOrddOpyMlM3Z0umEA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.62.0",
"@typescript-eslint/visitor-keys": "8.62.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.0.tgz",
"integrity": "sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.0.tgz",
"integrity": "sha512-CY3uyFSRbcQv3nnSv8S0+lDftMVz6P963PoRlxrV7ew/Md564g9ut60PYzdLM5qW4jFn93GBF+Soi90ISAN+GQ==",
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.1.tgz",
"integrity": "sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.62.0",
"@typescript-eslint/types": "8.62.1",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@ -5070,6 +4913,19 @@
"node": ">=18.0.0"
}
},
"node_modules/@vitejs/plugin-basic-ssl": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.3.0.tgz",
"integrity": "sha512-bdyo8rB3NnQbikdMpHaML9Z1OZPBu6fFOBo+OtxsBlvMJtysWskmBcnbIDhUqgC8tcxNv/a+BcV5U+2nQMm1OQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
},
"peerDependencies": {
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/@vitejs/plugin-react": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz",
@ -12753,6 +12609,19 @@
"tree-kill": "cli.js"
}
},
"node_modules/ts-api-utils": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
"integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.12"
},
"peerDependencies": {
"typescript": ">=4.8.4"
}
},
"node_modules/ts-interface-checker": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
@ -12883,6 +12752,43 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/typescript": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/typescript-eslint": {
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.62.1.tgz",
"integrity": "sha512-vymnnM5g0AKQDSAyfP12nMIBvgwgA42syg74kkuZ4x1VuTzwQKwc5h9rGxeShCjny5o+zWAb6OEoz7XLgrIkIw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.62.1",
"@typescript-eslint/parser": "8.62.1",
"@typescript-eslint/typescript-estree": "8.62.1",
"@typescript-eslint/utils": "8.62.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/unbox-primitive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",

View file

@ -38,6 +38,7 @@
"@typescript-eslint/eslint-plugin": "^8.62.0",
"@typescript-eslint/parser": "^8.62.0",
"@uwil/husky-config": "^1.0.1",
"@vitejs/plugin-basic-ssl": "^2.3.0",
"@vitejs/plugin-react": "^6.0.3",
"autoprefixer": "^10.5.2",
"concurrently": "^9.2.3",
@ -65,6 +66,7 @@
"stylelint-scss": "^6.14.0",
"tailwindcss": "^3.4.19",
"tailwindcss-animate": "^1.0.7",
"typescript-eslint": "^8.62.1",
"vite": "^8.1.0",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-stylelint": "^6.3.0"
@ -124,6 +126,7 @@
"slugify": "^1.6.9",
"swiper": "^12.2.0",
"tailwind-merge": "^3.6.0",
"typescript": "^6.0.3",
"vaul": "^1.1.2",
"ziggy-js": "^2.6.3",
"zod": "^4.4.3",

View file

@ -1,28 +1,23 @@
// * Packages
import { Link, usePage } from "@inertiajs/react";
import { ElementType, ReactNode, useEffect, useRef } from "react";
import {
ChevronDownIcon,
MoreHorizontalIcon,
CookingPotIcon,
EggIcon,
HomeIcon,
MoreHorizontalIcon,
SquareArrowOutUpRightIcon,
TelescopeIcon,
TelescopeIcon
} from "lucide-react";
import { ElementType, ReactNode, useEffect, useRef } from "react";
import { route } from "ziggy-js";
// * Components shadcn
import { Button } from "@/components/ui/button";
import {
Sidebar,
SidebarHeader,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuAction,
useSidebar,
} from "@/components/ui/sidebar";
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
DropdownMenu,
DropdownMenuContent,
@ -30,25 +25,32 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Collapsible,
CollapsibleTrigger,
CollapsibleContent,
} from "@/components/ui/collapsible";
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button";
// * Other
import ApplicationLogo from "@/back/Components/ApplicationLogo";
import { SidebarRegistry } from "@/back/Components/Layout/Sidebar/SidebarRegistry";
import Views from "@/back/lib/actions";
import { AuthProp } from "@/back/types/global";
import str from "@/hooks/use-string";
import useTrans from "@/hooks/use-translations";
import { AppEnv, LaravelEnum } from "@/types/global";
import { AuthProp } from "@/back/types/global";
import { GetTranslation } from "@/lib/utils";
import { AppEnv, LaravelEnum } from "@/types/global";
export type NavListLinksProps = {
/** Title of the button. */
@ -137,6 +139,8 @@ export default function AppSidebar() {
const transModules = useTrans("uwil-backend::back.modules");
const transAdmin = useTrans("uwil-backend::enums.user-role.admin");
const transTelescope = useTrans("uwil-backend::back.telescope");
const transProducts = useTrans("models.classes.product", { choice: Infinity });
const transRecipes = useTrans("models.classes.recipe", { choice: Infinity });
const navList: NavListProps = [
{
@ -156,6 +160,18 @@ export default function AppSidebar() {
icon: SquareArrowOutUpRightIcon,
},
...SidebarRegistry.getAll(auth, "pages"),
{
title: str(transProducts).ucFirst().value(),
model: "products",
show: true,
icon: EggIcon,
},
{
title: str(transRecipes).ucFirst().value(),
model: "recipes",
show: true,
icon: CookingPotIcon,
},
],
},
{

View file

@ -1,23 +1,23 @@
// * Packages
import { AlertCircleIcon, SquareArrowOutUpRightIcon } from "lucide-react";
import { usePage } from "@inertiajs/react";
import { AlertCircleIcon, SquareArrowOutUpRightIcon } from "lucide-react";
// * Components shadcn
import AuthenticatedLayout from "@/back/Layouts/AuthenticatedLayout";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import {
Card,
CardContent,
CardDescription,
CardHeader,
} from "@/components/ui/card";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
// * Other
import logoUwilReportDark from "&/images/logos/uwil-report-dark.svg";
import logoUwilReportLight from "&/images/logos/uwil-report-light.svg";
import logoUwilSupportDark from "&/images/logos/uwil-support-dark.svg";
import logoUwilSupportLight from "&/images/logos/uwil-support-light.svg";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import useTrans from "@/hooks/use-translations";
import { AppEnv } from "@/types/global";
import logoUwilReportLight from "&/images/logos/uwil-report-light.svg";
import logoUwilReportDark from "&/images/logos/uwil-report-dark.svg";
import logoUwilSupportLight from "&/images/logos/uwil-support-light.svg";
import logoUwilSupportDark from "&/images/logos/uwil-support-dark.svg";
export default function Dashboard() {
const appEnv = usePage().props.appEnv as AppEnv;

View file

@ -0,0 +1,190 @@
// * Packages
import { Row } from "@tanstack/react-table";
import { CheckIcon, PlusIcon, XIcon } from "lucide-react";
import { route } from "ziggy-js";
// * Components shadcn
import { Badge } from "@/components/ui/badge";
import { usePage } from "@inertiajs/react";
// * Other
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import DataTable from "@/back/Components/Table/DataTable";
import DataTableFilterButton from "@/back/Components/Table/DataTableFilterButton";
import DataTableSearch from "@/back/Components/Table/DataTableSearch";
import AuthenticatedLayout from "@/back/Layouts/AuthenticatedLayout";
import Views from "@/back/lib/actions";
import { AuthProp, DefaultModel, Resource } from "@/back/types/global";
import str from "@/hooks/use-string";
import useTrans from "@/hooks/use-translations";
import { GetFormatedDate, GetTranslation } from "@/lib/utils";
const getColumns = () => {
return [
{
accessorKey: "name",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(
GetTranslation(`validation.attributes.${column.id}`),
)
.ucFirst()
.value()}
field={column.id}
/>
),
},
{
accessorKey: "category",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(
GetTranslation(`validation.attributes.${column.id}`),
)
.ucFirst()
.value()}
field={column.id}
/>
),
cell: ({ row }: { row: Row<DefaultModel & Record<string, any>> }) => {
// Handle enum object or string
const val = row.getValue("category") as any;
const label = typeof val === 'object' && val !== null ? val.label : val;
return (
<Badge variant="outline">
{str(String(label)).ucFirst().value()}
</Badge>
);
},
},
{
accessorKey: "technology",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(
GetTranslation(`validation.attributes.${column.id}`),
)
.ucFirst()
.value()}
field={column.id}
/>
),
cell: ({ row }: { row: Row<DefaultModel & Record<string, any>> }) => {
// Handle enum object or string
const val = row.getValue("technology") as any;
const label = typeof val === 'object' && val !== null ? val.label : val;
return (
<Badge variant="outline">
{str(String(label)).ucFirst().value()}
</Badge>
);
},
},
{
accessorKey: "is_egalim",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(
GetTranslation(`validation.attributes.${column.id}`),
)
.ucFirst()
.value()}
field={column.id}
/>
),
cell: ({ row }: { row: Row<DefaultModel & Record<string, any>> }) => (
<Badge
variant="outline"
className={`border-0 ${
row.getValue("is_egalim")
? "bg-green-200 dark:bg-green-900"
: "bg-red-200 dark:bg-red-900"
}`}
>
{row.getValue("is_egalim") ? (
<CheckIcon size={15} className="mr-1" />
) : (
<XIcon size={15} className="mr-1" />
)}
{str(
GetTranslation(
row.getValue("is_egalim")
? "uwil-backend::back.yes"
: "uwil-backend::back.no",
),
)
.ucFirst()
.value()}
</Badge>
),
},
{
accessorKey: "updated_at",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(
GetTranslation(`validation.attributes.${column.id}`),
)
.ucFirst()
.value()}
field={column.id}
/>
),
cell: ({ row }: { row: Row<DefaultModel & Record<string, any>> }) => (
<Badge variant="secondary">
{GetFormatedDate(row.getValue("updated_at"))}
</Badge>
),
},
];
};
export default function Index({
paginatedModels,
searchFields,
}: Readonly<{
paginatedModels: Resource;
searchFields: string;
}>) {
const { auth } = usePage().props as unknown as {
auth: AuthProp;
};
const { create } = auth.policies.products || { create: true };
const actionModels = [
Views.VISUALIZATION,
Views.EDITION,
Views.DUPLICATION,
Views.DELETION,
];
const columns = getColumns();
const transCrudAddEntry = useTrans("uwil-backend::back.crud_add_entry");
const afterNode = () => {
return (
create && (
<BtnOptionnalLinkWithTooltip
link={route('back.products.create')}
content={<PlusIcon />}
tooltipContent={<p>{transCrudAddEntry}</p>}
/>
)
);
};
return (
<AuthenticatedLayout
title={str(
useTrans('models.classes.product', { choice: Infinity }),
)
.ucFirst()
.value()}
afterNode={afterNode()}
>
<DataTableSearch modelRoute='products' searchFields={searchFields} />
<DataTable
columns={columns}
modelRoute='products'
modelsPaginate={paginatedModels}
actions={actionModels}
/>
</AuthenticatedLayout>
);
}

View file

@ -0,0 +1,194 @@
// * Packages
import { Row } from "@tanstack/react-table";
import { CheckIcon, PlusIcon, XIcon } from "lucide-react";
import { route } from "ziggy-js";
// * Components shadcn
import { Badge } from "@/components/ui/badge";
import { usePage } from "@inertiajs/react";
// * Other
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import DataTable from "@/back/Components/Table/DataTable";
import DataTableFilterButton from "@/back/Components/Table/DataTableFilterButton";
import DataTableSearch from "@/back/Components/Table/DataTableSearch";
import AuthenticatedLayout from "@/back/Layouts/AuthenticatedLayout";
import Views from "@/back/lib/actions";
import { AuthProp, DefaultModel, Resource } from "@/back/types/global";
import str from "@/hooks/use-string";
import useTrans from "@/hooks/use-translations";
import { GetFormatedDate, GetTranslation } from "@/lib/utils";
const getColumns = () => {
return [
{
accessorKey: "name",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(
GetTranslation(`validation.attributes.${column.id}`),
)
.ucFirst()
.value()}
field={column.id}
/>
),
},
{
accessorKey: "product_range",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(
GetTranslation(`validation.attributes.${column.id}`),
)
.ucFirst()
.value()}
field={column.id}
/>
),
cell: ({ row }: { row: Row<DefaultModel & Record<string, any>> }) => {
const val = row.getValue("product_range") as any;
const label = typeof val === 'object' && val !== null ? val.label : val;
return (
<Badge variant="outline">
{str(String(label)).ucFirst().value()}
</Badge>
);
},
},
{
accessorKey: "consomption_time",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(
GetTranslation(`validation.attributes.${column.id}`),
)
.ucFirst()
.value()}
field={column.id}
/>
),
cell: ({ row }: { row: Row<DefaultModel & Record<string, any>> }) => {
const val = row.getValue("consomption_time") as any;
const label = typeof val === 'object' && val !== null ? val.label : val;
return (
<Badge variant="outline">
{str(String(label)).ucFirst().value()}
</Badge>
);
},
},
{
accessorKey: "inspiration",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(
GetTranslation(`validation.attributes.${column.id}`),
)
.ucFirst()
.value()}
field={column.id}
/>
),
cell: ({ row }: { row: Row<DefaultModel & Record<string, any>> }) => {
const val = row.getValue("inspiration") as any;
const label = typeof val === 'object' && val !== null ? val.label : val;
return (
<Badge variant="outline">
{str(String(label)).ucFirst().value()}
</Badge>
);
},
},
{
accessorKey: "season",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(
GetTranslation(`validation.attributes.${column.id}`),
)
.ucFirst()
.value()}
field={column.id}
/>
),
cell: ({ row }: { row: Row<DefaultModel & Record<string, any>> }) => {
const val = row.getValue("season") as any;
const label = typeof val === 'object' && val !== null ? val.label : val;
return (
<Badge variant="outline">
{str(String(label)).ucFirst().value()}
</Badge>
);
},
},
{
accessorKey: "updated_at",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(
GetTranslation(`validation.attributes.${column.id}`),
)
.ucFirst()
.value()}
field={column.id}
/>
),
cell: ({ row }: { row: Row<DefaultModel & Record<string, any>> }) => (
<Badge variant="secondary">
{GetFormatedDate(row.getValue("updated_at"))}
</Badge>
),
},
];
};
export default function Index({
paginatedModels,
searchFields,
}: Readonly<{
paginatedModels: Resource;
searchFields: string;
}>) {
const { auth } = usePage().props as unknown as {
auth: AuthProp;
};
const { create } = auth.policies.recipes || { create: true };
const actionModels = [
Views.VISUALIZATION,
Views.EDITION,
Views.DUPLICATION,
Views.DELETION,
];
const columns = getColumns();
const transCrudAddEntry = useTrans("uwil-backend::back.crud_add_entry");
const afterNode = () => {
return (
create && (
<BtnOptionnalLinkWithTooltip
link={route('back.recipes.create')}
content={<PlusIcon />}
tooltipContent={<p>{transCrudAddEntry}</p>}
/>
)
);
};
return (
<AuthenticatedLayout
title={str(
useTrans('models.classes.recipe', { choice: Infinity }),
)
.ucFirst()
.value()}
afterNode={afterNode()}
>
<DataTableSearch modelRoute='recipes' searchFields={searchFields} />
<DataTable
columns={columns}
modelRoute='recipes'
modelsPaginate={paginatedModels}
actions={actionModels}
/>
</AuthenticatedLayout>
);
}

View file

@ -13,12 +13,12 @@
<x-uwil-common::head.og />
<x-uwil-common::head.favicon mask-icon-color="#0150a8" tile-color="#0150a8" theme-color="#0150a8" />
<!-- Scripts -->
{{-- Scripts --}}
@routes(nonce: $nonce ?? '')
@if (app()->hasDebugModeEnabled())
@viteReactRefresh
@endif
@vite(["resources/ts/{$page['component']}.tsx", 'resources/sass/back/app.scss'])
@vite(['resources/ts/app.tsx', "resources/ts/{$page['component']}.tsx", 'resources/sass/back/app.scss'])
@inertiaHead
</head>

View file

@ -12,12 +12,12 @@
<x-uwil-common::head.hreflang />
<x-uwil-common::head.favicon mask-icon-color="#ff0000" tile-color="#ff0000" theme-color="#ff0000" />
<!-- Scripts -->
{{-- Scripts --}}
@routes(nonce: $nonce ?? '')
@if (app()->hasDebugModeEnabled())
@viteReactRefresh
@endif
@vite(["resources/ts/{$page['component']}.tsx", 'resources/sass/front/app.scss'])
@vite(['resources/ts/app.tsx', "resources/ts/{$page['component']}.tsx", 'resources/sass/front/app.scss'])
@inertiaHead
</head>

View file

@ -1,4 +1,10 @@
<?php
// Set specific back routes here.
// ex: Route::resource('stories', OriginalStoryController::class);.
declare(strict_types=1);
use App\Http\Controllers\Back\ProductController;
use App\Http\Controllers\Back\RecipeController;
use Illuminate\Support\Facades\Route;
Route::resource('products', ProductController::class);
Route::resource('recipes', RecipeController::class);

View file

@ -1,4 +1,5 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import basicSsl from "@vitejs/plugin-basic-ssl";
import react from "@vitejs/plugin-react";
import laravel from "laravel-vite-plugin";
import path from "node:path";
@ -104,6 +105,7 @@ export default defineConfig({
},
},
plugins: [
basicSsl(),
laravel({
input: [
"resources/ts/app.tsx",