diff --git a/app/Contracts/Directories/ProductDirectory.php b/app/Contracts/Directories/ProductDirectory.php index 6d42d65d..4ad6337b 100644 --- a/app/Contracts/Directories/ProductDirectory.php +++ b/app/Contracts/Directories/ProductDirectory.php @@ -8,6 +8,7 @@ use App\Contracts\Directory; use App\Dtos\CategoryProducts; use Domain\Models\Entities\Product; use Domain\Models\ValueObjects\Entities\ProductId; +use Domain\Models\ValueObjects\Entities\RecipeId; use Domain\Models\ValueObjects\Pagination; use Domain\Models\ValueObjects\Products\ProductCategory; use Illuminate\Pagination\LengthAwarePaginator; @@ -19,6 +20,11 @@ interface ProductDirectory extends Directory public function findBySlug(string $slug): ?Product; + /** + * @return \App\Dtos\ProductSummary[] + */ + public function getAllProductsSummary(): array; + /** * @return LengthAwarePaginator */ @@ -41,4 +47,7 @@ interface ProductDirectory extends Directory string $search = '', int $limitPerCategory = 6 ): ?CategoryProducts; + + /** @return \Domain\Models\Entities\Product[] */ + public function getRelatedProducts(RecipeId $recipeId, int $limit = 4): array; } diff --git a/app/Contracts/Directories/RecipeDirectory.php b/app/Contracts/Directories/RecipeDirectory.php index b6d6d7a4..5e9abb5b 100644 --- a/app/Contracts/Directories/RecipeDirectory.php +++ b/app/Contracts/Directories/RecipeDirectory.php @@ -6,6 +6,7 @@ namespace App\Contracts\Directories; use App\Contracts\Directory; use Domain\Models\Entities\Recipe; +use Domain\Models\ValueObjects\Entities\ProductId; use Domain\Models\ValueObjects\Entities\RecipeId; use Domain\Models\ValueObjects\Pagination; use Illuminate\Pagination\LengthAwarePaginator; @@ -32,4 +33,7 @@ interface RecipeDirectory extends Directory array $seasons = [], string $search = '' ): LengthAwarePaginator; + + /** @return \Domain\Models\Entities\Recipe[] */ + public function getRelatedRecipes(ProductId $productId, int $limit = 4): array; } diff --git a/app/Directories/EloquentProductDirectory.php b/app/Directories/EloquentProductDirectory.php index 2734c242..7f570a45 100644 --- a/app/Directories/EloquentProductDirectory.php +++ b/app/Directories/EloquentProductDirectory.php @@ -6,10 +6,12 @@ namespace App\Directories; use App\Contracts\Directories\ProductDirectory; use App\Dtos\CategoryProducts; +use App\Dtos\ProductSummary; use App\Models\Product; use App\Models\Recipe; use Domain\Models\Entities\Product as DomainProduct; use Domain\Models\ValueObjects\Entities\ProductId; +use Domain\Models\ValueObjects\Entities\RecipeId; use Domain\Models\ValueObjects\Pagination; use Domain\Models\ValueObjects\Products\ProductCategory; use Illuminate\Database\Eloquent\Builder; @@ -41,6 +43,16 @@ class EloquentProductDirectory implements ProductDirectory ->first(); } + public function getAllProductsSummary(): array + { + return Product::query() + ->select(['id', 'name', 'picture']) + ->orderBy('name', 'asc') + ->get() + ->map(fn (Product $product) => $this->mapToSummary($product)) + ->all(); + } + public function getPaginatedProducts( Pagination $pagination, string $search = '', @@ -152,6 +164,15 @@ class EloquentProductDirectory implements ProductDirectory ); } + private function mapToSummary(Product $product): ProductSummary + { + return new ProductSummary( + id: (string) $product->id, + name: $product->name, + picture: $product->picture, + ); + } + private function mapToEntity(Product $product): DomainProduct { return new DomainProduct( @@ -179,4 +200,22 @@ class EloquentProductDirectory implements ProductDirectory )->all() ); } + + public function getRelatedProducts(RecipeId $recipeId, int $limit = 4): array + { + $recipe = Recipe::query() + ->where('id', $recipeId->value) + ->first(); + + if (! $recipe) { + return []; + } + + return $recipe->products() + ->with(['recipes']) + ->limit($limit) + ->get() + ->map(fn (Product $product) => $this->mapToEntity($product)) + ->all(); + } } diff --git a/app/Directories/EloquentRecipeDirectory.php b/app/Directories/EloquentRecipeDirectory.php index 6703abd9..0b70e7cf 100644 --- a/app/Directories/EloquentRecipeDirectory.php +++ b/app/Directories/EloquentRecipeDirectory.php @@ -5,8 +5,10 @@ declare(strict_types=1); namespace App\Directories; use App\Contracts\Directories\RecipeDirectory; +use App\Models\Product; use App\Models\Recipe; use Domain\Models\Entities\Recipe as DomainRecipe; +use Domain\Models\ValueObjects\Entities\ProductId; use Domain\Models\ValueObjects\Entities\RecipeId; use Domain\Models\ValueObjects\Pagination; use Illuminate\Database\Eloquent\Builder; @@ -118,4 +120,22 @@ class EloquentRecipeDirectory implements RecipeDirectory associatedProducts: [] ); } + + public function getRelatedRecipes(ProductId $productId, int $limit = 4): array + { + $product = Product::query() + ->where('id', (string) $productId->value) + ->first(); + + if (! $product) { + return []; + } + + return $product->recipes() + ->with(['products']) + ->limit($limit) + ->get() + ->map(fn (Recipe $recipe) => $this->mapToEntity($recipe)) + ->all(); + } } diff --git a/app/Dtos/ProductSummary.php b/app/Dtos/ProductSummary.php new file mode 100644 index 00000000..adc6cd85 --- /dev/null +++ b/app/Dtos/ProductSummary.php @@ -0,0 +1,15 @@ +inertiaFill($request->validated()); + $product->fill($request->only( + $product->getFillable() + )); if ($product->saveOrFail()) { return redirect()->route('back.products.edit', $product) @@ -137,7 +139,9 @@ class ProductController extends BackendController $this->authorize('update', $product); return DB::transaction(function () use ($request, $product) { - $product->inertiaFill($request->validated()); + $product->fill($request->only( + $product->getFillable() + )); if ($product->saveOrFail()) { return redirect()->route('back.products.edit', $product) diff --git a/app/Http/Controllers/Back/RecipeController.php b/app/Http/Controllers/Back/RecipeController.php index 45c3e2c4..503a0e84 100644 --- a/app/Http/Controllers/Back/RecipeController.php +++ b/app/Http/Controllers/Back/RecipeController.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Http\Controllers\Back; +use App\Contracts\Directories\ProductDirectory; use App\Http\Requests\Back\Recipes\IndexRecipeRequest; use App\Http\Requests\Back\Recipes\StoreRecipeRequest; use App\Http\Requests\Back\Recipes\UpdateRecipeRequest; @@ -25,6 +26,7 @@ class RecipeController extends BackendController public function __construct( Request $request, Session $session, + private readonly ProductDirectory $productDirectory, ) { parent::__construct( $request, @@ -91,6 +93,7 @@ class RecipeController extends BackendController 'enumConsumptionTime' => ConsumptionTime::toArray(), 'enumInspiration' => Inspiration::toArray(), 'enumSeason' => Season::toArray(), + 'allProducts' => $this->productDirectory->getAllProductsSummary(), ]); } @@ -100,9 +103,14 @@ class RecipeController extends BackendController return DB::transaction(function () use ($request) { $recipe = new Recipe(); - $recipe->inertiaFill($request->validated()); + $recipe->fill($request->only( + $recipe->getFillable() + )); if ($recipe->saveOrFail()) { + $recipe->products() + ->sync($request->array('products')); + return redirect()->route('back.recipes.edit', $recipe) ->with('success', trans('uwil-backend::crud.messages.has_been_created', [ 'model' => Str::ucfirst(trans_choice('models.classes.recipes', 1)), @@ -120,12 +128,15 @@ class RecipeController extends BackendController { $this->authorize('update', $recipe); + $recipe->load('products'); + return $this->inertiaRender('back/Pages/Recipes/Edit', [ 'data' => CommonResource::make($recipe), 'enumProductCategory' => ProductCategory::toArray(), 'enumConsumptionTime' => ConsumptionTime::toArray(), 'enumInspiration' => Inspiration::toArray(), 'enumSeason' => Season::toArray(), + 'allProducts' => $this->productDirectory->getAllProductsSummary(), ]); } @@ -136,9 +147,14 @@ class RecipeController extends BackendController $this->authorize('update', $recipe); return DB::transaction(function () use ($request, $recipe) { - $recipe->inertiaFill($request->validated()); + $recipe->fill($request->only( + $recipe->getFillable() + )); if ($recipe->saveOrFail()) { + $recipe->products() + ->sync($request->array('products')); + return redirect()->route('back.recipes.edit', $recipe) ->with('success', trans('uwil-backend::crud.messages.has_been_updated', [ 'model' => Str::ucfirst(trans_choice('models.classes.recipe', 1)), diff --git a/app/Http/Controllers/Front/ProductController.php b/app/Http/Controllers/Front/ProductController.php index 82245fbf..7cb1b0cb 100644 --- a/app/Http/Controllers/Front/ProductController.php +++ b/app/Http/Controllers/Front/ProductController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Http\Controllers\Front; use App\Contracts\Directories\ProductDirectory; +use App\Contracts\Directories\RecipeDirectory; use App\Http\Controllers\Controller; use App\Http\Requests\Front\Products\IndexProductRequest; use Domain\Models\Entities\Product; @@ -16,6 +17,7 @@ class ProductController extends Controller { public function __construct( private readonly ProductDirectory $productDirectory, + private readonly RecipeDirectory $recipeDirectory, ) { } @@ -62,8 +64,12 @@ class ProductController extends Controller public function show(Product $product): Response { + $relatedRecipes = $this->recipeDirectory + ->getRelatedRecipes($product->id(), 10); + return Inertia::render('front/Pages/Products/Show', [ - 'product' => $product, + 'product' => $product, + 'relatedRecipes' => $relatedRecipes, ]); } } diff --git a/app/Http/Controllers/Front/RecipeController.php b/app/Http/Controllers/Front/RecipeController.php index c72a8104..67f373e6 100644 --- a/app/Http/Controllers/Front/RecipeController.php +++ b/app/Http/Controllers/Front/RecipeController.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Http\Controllers\Front; +use App\Contracts\Directories\ProductDirectory; use App\Contracts\Directories\RecipeDirectory; use App\Http\Controllers\Controller; use App\Http\Requests\Front\Recipes\IndexRecipeRequest; @@ -17,6 +18,7 @@ class RecipeController extends Controller { public function __construct( private readonly RecipeDirectory $recipeDirectory, + private readonly ProductDirectory $productDirectory, ) { } @@ -66,8 +68,12 @@ class RecipeController extends Controller public function show(Recipe $recipe): Response { + $relatedProducts = $this->productDirectory + ->getRelatedProducts($recipe->id(), 10); + return Inertia::render('front/Pages/Recipes/Show', [ - 'recipe' => $recipe, + 'recipe' => $recipe, + 'relatedProducts' => $relatedProducts, ]); } } diff --git a/app/Http/Requests/Back/Recipes/StoreRecipeRequest.php b/app/Http/Requests/Back/Recipes/StoreRecipeRequest.php index ab2ff340..8f8a41aa 100644 --- a/app/Http/Requests/Back/Recipes/StoreRecipeRequest.php +++ b/app/Http/Requests/Back/Recipes/StoreRecipeRequest.php @@ -35,21 +35,23 @@ class StoreRecipeRequest extends FormRequest public function rules(): array { return [ - 'name' => ['required', 'string', 'max:255'], - 'seo_title' => ['required', 'string', 'between:45,70'], - 'seo_description' => ['required', 'string', 'between:50,160'], - 'product_category' => ['required', new Enum(ProductCategory::class)], - 'consomption_time' => ['required', new Enum(ConsumptionTime::class)], - 'inspiration' => ['required', new Enum(Inspiration::class)], - 'season' => ['required', new Enum(Season::class)], - 'description' => ['nullable', 'string'], - 'details' => ['nullable', 'string'], - 'ingredients_list' => ['required', 'string'], - 'preparation_steps' => ['required', 'string'], - 'tips_and_advices' => ['nullable', 'string'], - 'picture' => ['required', 'image', 'max:50000', 'dimensions:width=800,height=800'], - 'picture_alt' => ['required', 'string', 'min:4', 'max:75'], - 'picture_title' => ['required', 'string', 'min:4', 'max:75'], + 'name' => ['required', 'string', 'max:255'], + 'seo_title' => ['required', 'string', 'between:45,70'], + 'seo_description' => ['required', 'string', 'between:50,160'], + 'product_category' => ['required', new Enum(ProductCategory::class)], + 'consomption_time' => ['required', new Enum(ConsumptionTime::class)], + 'inspiration' => ['required', new Enum(Inspiration::class)], + 'season' => ['required', new Enum(Season::class)], + 'description' => ['nullable', 'string'], + 'details' => ['nullable', 'string'], + 'ingredients_list' => ['required', 'string'], + 'preparation_steps' => ['required', 'string'], + 'tips_and_advices' => ['nullable', 'string'], + 'picture' => ['required', 'image', 'max:50000', 'dimensions:width=800,height=800'], + 'picture_alt' => ['required', 'string', 'min:4', 'max:75'], + 'picture_title' => ['required', 'string', 'min:4', 'max:75'], + 'products' => ['nullable', 'array'], + 'products.*' => ['string', 'uuid', 'exists:products,id'], ]; } @@ -61,21 +63,22 @@ class StoreRecipeRequest extends FormRequest public function attributes(): array { return [ - 'name' => trans('validation.attributes.name'), - 'seo_title' => trans('validation.attributes.seo_title'), - 'seo_description' => trans('validation.attributes.seo_description'), - 'product_category' => trans('validation.attributes.product_category'), - 'consomption_time' => trans('validation.attributes.consomption_time'), - 'inspiration' => trans('validation.attributes.inspiration'), - 'season' => trans('validation.attributes.season'), - 'description' => trans('validation.attributes.description'), - 'details' => trans('validation.attributes.details'), - 'ingredients_list' => trans('validation.attributes.ingredients_list'), - 'preparation_steps' => trans('validation.attributes.preparation_steps'), - 'tips_and_advices' => trans('validation.attributes.tips_and_advices'), - 'picture' => trans('validation.attributes.picture'), - 'picture_alt' => trans('validation.attributes.picture_alt'), - 'picture_title' => trans('validation.attributes.picture_title'), + 'name' => trans('validation.attributes.name'), + 'seo_title' => trans('validation.attributes.seo_title'), + 'seo_description' => trans('validation.attributes.seo_description'), + 'product_category' => trans('validation.attributes.product_category'), + 'consomption_time' => trans('validation.attributes.consomption_time'), + 'inspiration' => trans('validation.attributes.inspiration'), + 'season' => trans('validation.attributes.season'), + 'description' => trans('validation.attributes.description'), + 'details' => trans('validation.attributes.details'), + 'ingredients_list' => trans('validation.attributes.ingredients_list'), + 'preparation_steps' => trans('validation.attributes.preparation_steps'), + 'tips_and_advices' => trans('validation.attributes.tips_and_advices'), + 'picture' => trans('validation.attributes.picture'), + 'picture_alt' => trans('validation.attributes.picture_alt'), + 'picture_title' => trans('validation.attributes.picture_title'), + 'products' => trans('validation.attributes.associated_products'), ]; } } diff --git a/lang/fr/validation.php b/lang/fr/validation.php index fe697c67..2e74fa61 100644 --- a/lang/fr/validation.php +++ b/lang/fr/validation.php @@ -254,9 +254,11 @@ return [ 'preview' => 'aperçu', 'price' => 'prix', 'product_category' => 'gamme', + 'associated_products' => 'produits associés', 'product_id' => 'identifiant du produit', 'product_uid' => 'UID du produit', 'product_uuid' => 'UUID du produit', + 'products' => 'produits', 'promo_code' => 'code promo', 'province' => 'région', 'quantity' => 'quantité', diff --git a/resources/ts/back/Pages/Recipes/Form.tsx b/resources/ts/back/Pages/Recipes/Form.tsx index 3920579b..7e04ba26 100644 --- a/resources/ts/back/Pages/Recipes/Form.tsx +++ b/resources/ts/back/Pages/Recipes/Form.tsx @@ -1,6 +1,6 @@ // * Packages import { useForm, usePage } from "@inertiajs/react"; -import { BookTextIcon, CogIcon, ImageIcon } from "lucide-react"; +import { BookTextIcon, CheckIcon, CogIcon, ImageIcon } from "lucide-react"; import { route } from "ziggy-js"; // * Components import FieldsetWrapper from "@/back/Components/FieldsetWrapper"; @@ -13,6 +13,8 @@ import useCrudSubmit from "@/back/hooks/use-crud-submit"; // * Types & Global import CompleteInputFile from "@/back/Components/Inputs/CompleteInputFile"; import { RawRecipeModel } from "@/back/types/recipes"; +import { Button } from "@/components/ui/button"; +import ProductsSelectionModal from "./Partials/ProductsSelectionModal"; import { ModelId } from "@/domain"; import str from "@/hooks/use-string"; import useTrans from "@/hooks/use-translations"; @@ -35,6 +37,7 @@ interface FormData { picture: string | File | null; picture_alt: string; picture_title: string; + products: string[]; } export default function Form({ @@ -57,6 +60,11 @@ export default function Form({ const enumInspiration = (usePage().props.enumInspiration ?? {}) as LaravelEnumMap; const enumSeason = (usePage().props.enumSeason ?? {}) as LaravelEnumMap; + const allProducts = (usePage().props.allProducts ?? []) as { + id: string; + name: string; + picture: string | null; + }[]; const precognitionMethod = action === "store" ? "post" : "put"; const precognitionUrl = route(`back.${modelRoute}.${action}`, model?.id); @@ -78,10 +86,20 @@ export default function Form({ picture: model?.picture ?? null, picture_alt: model?.picture_alt ?? "", picture_title: model?.picture_title ?? "", + products: model?.products?.map((p) => p.id) ?? [], }).withPrecognition(precognitionMethod, precognitionUrl); const { post, setData, data, reset, errors: formErrors, validate } = form; + const toggleProduct = (productId: string) => { + setData( + "products", + data.products.includes(productId) + ? data.products.filter((id) => id !== productId) + : [...data.products, productId], + ); + }; + const modelNameTrans = useTrans(`models.classes.${modelName}`, { choice: 1, }); @@ -260,6 +278,65 @@ export default function Form({ /> + {/* Associated Products */} + } + description="Lier des produits à cette recette" + className="flex flex-col gap-4" + > +
+

+ {data.products.length} produit(s) sélectionné(s) +

+ +
+ + {data.products.length > 0 && ( +
+ {data.products.map((productId) => { + const product = allProducts.find((p) => p.id === productId); + if (!product) return null; + return ( +
+ {product.name} + + {product.name} + + +
+ ); + })} +
+ )} +
+
diff --git a/resources/ts/back/Pages/Recipes/Partials/ProductsSelectionModal.tsx b/resources/ts/back/Pages/Recipes/Partials/ProductsSelectionModal.tsx new file mode 100644 index 00000000..9279f7cc --- /dev/null +++ b/resources/ts/back/Pages/Recipes/Partials/ProductsSelectionModal.tsx @@ -0,0 +1,88 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { CheckIcon } from "lucide-react"; + +interface Product { + id: string; + name: string; + picture: string | null; +} + +interface ProductsSelectionModalProps { + allProducts: Product[]; + selectedProducts: string[]; + onToggleProduct: (productId: string) => void; +} + +export default function ProductsSelectionModal({ + allProducts, + selectedProducts, + onToggleProduct, +}: Readonly) { + return ( + + + + + + + Sélectionner des produits associés + + +
+
+ {allProducts.map((product) => { + const isSelected = selectedProducts.includes(product.id); + return ( + + ); + })} +
+
+ + + + + + +
+
+ ); +} diff --git a/resources/ts/back/types/recipes.ts b/resources/ts/back/types/recipes.ts index 3a305c8f..22e1e09e 100644 --- a/resources/ts/back/types/recipes.ts +++ b/resources/ts/back/types/recipes.ts @@ -28,6 +28,7 @@ export interface RawRecipeModel extends ModelWithPolicies { ingredients_list: string; preparation_steps: string; tips_and_advices: string | null; + products?: { id: ModelId }[]; } export interface RecipeModel extends RawRecipeModel { @@ -56,10 +57,13 @@ export const rawRecipeModelSchema = zod.object({ ingredients_list: zod.string(), preparation_steps: zod.string(), tips_and_advices: zod.string().nullable(), + products: zod + .array(zod.object({ id: zod.uuid().transform((v) => v as ModelId) })) + .optional(), }); export const recipeModelSchema = rawRecipeModelSchema.extend({ - id: zod.union([zod.uuid(), zod.number()]).transform((v) => v as ModelId), + id: zod.uuid().transform((v) => v as ModelId), created_at: zod.coerce.date(), updated_at: zod.coerce.date(), deleted_at: zod.coerce.date().nullable(), diff --git a/resources/ts/front/Components/Base/Carousel/CarouselContent.tsx b/resources/ts/front/Components/Base/Carousel/CarouselContent.tsx index 5b1394b1..87a69172 100644 --- a/resources/ts/front/Components/Base/Carousel/CarouselContent.tsx +++ b/resources/ts/front/Components/Base/Carousel/CarouselContent.tsx @@ -9,6 +9,7 @@ import { Pagination } from "swiper/modules"; import { Swiper, SwiperClass, SwiperSlide } from "swiper/react"; import { PaginationOptions } from "swiper/types"; /** THEME */ +import EggArrowIcon from "$/uwil/Icons/EggArrowIcon"; import ItemCard from "$/uwil/ItemCard"; import { bgColors, imageShapes } from "@/front/lib/constants"; import { cn } from "@/lib/utils"; @@ -24,9 +25,9 @@ function NextButton({ swiperRef, disabled, icon }: SwiperButtonProps) { type="button" onClick={() => swiperRef.current?.slideNext()} disabled={disabled} - className="hidden md:block disabled:opacity-0" + className="hidden md:block disabled:opacity-0 z-10 hover:scale-105 transition-transform" > - {icon} + {icon ?? } ); } @@ -37,9 +38,9 @@ function PrevButton({ swiperRef, disabled, icon }: SwiperButtonProps) { type="button" onClick={() => swiperRef.current?.slidePrev()} disabled={disabled} - className="hidden md:block disabled:opacity-0" + className="hidden md:block disabled:opacity-0 z-10 hover:scale-105 transition-transform" > - {icon} + {icon ?? } ); } diff --git a/resources/ts/front/Components/Theme/Icons/EggArrowIcon.tsx b/resources/ts/front/Components/Theme/Icons/EggArrowIcon.tsx new file mode 100644 index 00000000..144ceadd --- /dev/null +++ b/resources/ts/front/Components/Theme/Icons/EggArrowIcon.tsx @@ -0,0 +1,78 @@ +import { cn } from "@/lib/utils"; + +interface EggArrowIconProps { + direction: "left" | "right"; + className?: string; +} + +export default function EggArrowIcon({ + direction, + className, +}: Readonly) { + const leftArrowPath = + "M25.507 21.7106L16.9809 30.6754C16.8256 30.846 16.7039 31.047 16.6228 31.2671C16.4522 31.7036 16.4522 32.1933 16.6228 32.6298C16.7039 32.8499 16.8256 33.0509 16.9809 33.2215L25.507 42.1863C25.666 42.3535 25.8547 42.4861 26.0624 42.5766C26.2702 42.6671 26.4928 42.7136 26.7177 42.7136C27.1718 42.7136 27.6073 42.5239 27.9284 42.1863C28.2495 41.8487 28.4299 41.3908 28.4299 40.9133C28.4299 40.4358 28.2495 39.9779 27.9284 39.6403L22.3012 33.7414L35.2437 33.7414C35.696 33.7414 36.1297 33.5525 36.4495 33.2163C36.7693 32.88 36.949 32.424 36.949 31.9484C36.949 31.4729 36.7693 31.0169 36.4495 30.6806C36.1297 30.3444 35.696 30.1555 35.2437 30.1555L22.3012 30.1555L27.9284 24.2566C28.0882 24.0899 28.2151 23.8916 28.3016 23.6731C28.3882 23.4546 28.4328 23.2203 28.4328 22.9836C28.4328 22.7469 28.3882 22.5125 28.3016 22.2941C28.2151 22.0756 28.0882 21.8773 27.9284 21.7106C27.7698 21.5425 27.5812 21.4091 27.3735 21.3181C27.1657 21.2271 26.9428 21.1802 26.7177 21.1802C26.4926 21.1802 26.2697 21.2271 26.0619 21.3181C25.8541 21.4091 25.6655 21.5425 25.507 21.7106Z"; + const rightArrowPath = + "M28.4461 42.1832L37.1536 33.2184C37.3121 33.0478 37.4364 32.8468 37.5193 32.6267C37.6934 32.1902 37.6934 31.7005 37.5193 31.264C37.4364 31.0439 37.3121 30.8429 37.1536 30.6723L28.4461 21.7075C28.2837 21.5403 28.0909 21.4077 27.8788 21.3172C27.6666 21.2267 27.4392 21.1802 27.2096 21.1802C26.7458 21.1802 26.3011 21.3699 25.9731 21.7075C25.6452 22.0451 25.461 22.503 25.461 22.9805C25.461 23.458 25.6452 23.9159 25.9731 24.2535L31.7201 30.1524L18.5021 30.1524C18.0402 30.1524 17.5973 30.3413 17.2707 30.6775C16.9441 31.0138 16.7606 31.4698 16.7606 31.9453C16.7606 32.4209 16.9441 32.8769 17.2707 33.2132C17.5973 33.5494 18.0402 33.7383 18.5021 33.7383L31.7201 33.7383L25.9731 39.6372C25.8099 39.8039 25.6804 40.0022 25.5919 40.2207C25.5035 40.4392 25.458 40.6735 25.458 40.9102C25.458 41.1469 25.5035 41.3813 25.5919 41.5997C25.6804 41.8182 25.8099 42.0165 25.9731 42.1832C26.135 42.3513 26.3276 42.4847 26.5399 42.5757C26.7521 42.6667 26.9797 42.7136 27.2096 42.7136C27.4395 42.7136 27.6671 42.6667 27.8793 42.5757C28.0916 42.4847 28.2842 42.3513 28.4461 42.1832Z"; + + return ( + + + + + + + + + + + + + + + + + + + ); +} diff --git a/resources/ts/front/Components/Theme/Image.tsx b/resources/ts/front/Components/Theme/Image.tsx index e6c1d5a8..d1cf48ac 100644 --- a/resources/ts/front/Components/Theme/Image.tsx +++ b/resources/ts/front/Components/Theme/Image.tsx @@ -58,6 +58,7 @@ export default function Image({ size="xs" font={fonts.DELIUS_SWASH} isBold={false} + tabIndex={-1} link={{ label: action.label, href: action.href, diff --git a/resources/ts/front/Components/Theme/Sections/CarouselSection.tsx b/resources/ts/front/Components/Theme/Sections/CarouselSection.tsx index cd147e55..1cd0857f 100644 --- a/resources/ts/front/Components/Theme/Sections/CarouselSection.tsx +++ b/resources/ts/front/Components/Theme/Sections/CarouselSection.tsx @@ -1,9 +1,13 @@ /** THEME */ -import { CarouselProps, TextProps } from "@/types/front"; -import Section, { ButtonSectionProps } from "$/uwil/Section"; -import { BgColorName, ImageShapeName, imageShapes } from "@/front/lib/constants"; import Carousel from "$/base/Carousel/Carousel"; +import Section, { ButtonSectionProps } from "$/uwil/Section"; +import { + BgColorName, + ImageShapeName, + imageShapes, +} from "@/front/lib/constants"; import { cn } from "@/lib/utils"; +import { CarouselProps, TextProps } from "@/types/front"; type CarouselSectionProps = CarouselProps & { title?: TextProps; @@ -62,22 +66,22 @@ export default function CarouselSection(props: CarouselSectionProps) { items, carousel, sectionClassName, - shape = imageShapes.ROUNDED + shape = imageShapes.ROUNDED, } = props; const buttonProps = button ? { - button: { - href: button.href, - label: button.label, - ...(button.icon - ? { - icon: { - icon: button.icon.icon, - }, - } - : {}), - }, - } + button: { + href: button.href, + label: button.label, + ...(button.icon + ? { + icon: { + icon: button.icon.icon, + }, + } + : {}), + }, + } : {}; return (
) { +export default function Show({ + product, + relatedRecipes = [], +}: Readonly) { const [showAllFormats, setShowAllFormats] = useState(false); const LaravelEnumMapSchema = ( enumObj: Record, @@ -384,6 +393,67 @@ export default function Show({ product }: Readonly) {
)} + + {/* Related Recipes Section */} + {relatedRecipes.length > 0 && ( +
+
+

+ Recettes Ovoteam +

+ + ({ + image: { + src: r.picture + ? `/${r.picture}` + : "/assets/img/placeholder.jpg", + alt: r.name, + }, + title: { text: r.name, font: "font-delius-swash" }, + button: { + label: "Voir la recette", + href: route("front.recipes.show", { recipe: r.slug }), + }, + }) as ThemeItemCardProps, + )} + variant="bg-primary" + carousel={{ + slidesPerView: 1, + spaceBetween: 30, + breakpoints: { + 768: { slidesPerView: 2 }, + 1024: { slidesPerView: 3 }, + 1248: { slidesPerView: 4 }, + }, + pagination: true, + navigation: true, + }} + /> + +
+
+ +
+
+ )} ); } diff --git a/resources/ts/front/Pages/Recipes/Show.tsx b/resources/ts/front/Pages/Recipes/Show.tsx index ddc3ad24..101fbcda 100644 --- a/resources/ts/front/Pages/Recipes/Show.tsx +++ b/resources/ts/front/Pages/Recipes/Show.tsx @@ -19,8 +19,14 @@ import { import { Recipe } from "@/domain/entities/recipe"; import { DownloadIcon, Lightbulb } from "lucide-react"; +import Button from "$/uwil/Button"; +import { Product } from "@/domain/entities/product"; +import CarouselContent from "@/front/Components/Base/Carousel/CarouselContent"; +import { ThemeItemCardProps } from "@/types/front"; + interface ShowProps { recipe: Recipe; + relatedProducts?: Product[]; } type EnumOption = { @@ -31,7 +37,10 @@ type EnumOption = { type EnumMap = Record; -export default function Show({ recipe }: Readonly) { +export default function Show({ + recipe, + relatedProducts = [], +}: Readonly) { const LaravelEnumMapSchema = ( enumObj: Record, ) => @@ -284,6 +293,67 @@ export default function Show({ recipe }: Readonly) { Télécharger le pdf + + {/* Related Products Section */} + {relatedProducts.length > 0 && ( +
+
+

+ Produits Ovoteam +

+ + ({ + image: { + src: p.picture + ? `/${p.picture}` + : "/assets/img/placeholder.jpg", + alt: p.name, + }, + title: { text: p.name }, + button: { + label: "Voir le produit", + href: route("front.products.show", { product: p.slug }), + }, + }) as ThemeItemCardProps, + )} + variant="bg-primary" + carousel={{ + slidesPerView: 1, + spaceBetween: 30, + breakpoints: { + 768: { slidesPerView: 2 }, + 1024: { slidesPerView: 3 }, + 1248: { slidesPerView: 4 }, + }, + pagination: true, + navigation: true, + }} + /> + +
+
+ +
+
+ )} ); }