376 lines
12 KiB
TypeScript
376 lines
12 KiB
TypeScript
// * Packages
|
|
import { useForm, usePage } from "@inertiajs/react";
|
|
import { BookTextIcon, CheckIcon, CogIcon, ImageIcon } from "lucide-react";
|
|
import { route } from "ziggy-js";
|
|
// * Components
|
|
import FieldsetWrapper from "@/back/Components/FieldsetWrapper";
|
|
import InputError from "@/back/Components/InputError";
|
|
import EnumSelect from "@/back/Components/Inputs/EnumSelect";
|
|
import SimpleString from "@/back/Components/Inputs/SimpleString";
|
|
import SimpleSwitch from "@/back/Components/Inputs/SimpleSwitch";
|
|
import SimpleWysiwyg from "@/back/Components/Inputs/Wysiwyg/SimpleWysiwyg";
|
|
import SubmitButton from "@/back/Components/SubmitButton";
|
|
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 { ModelId } from "@/domain";
|
|
import str from "@/hooks/use-string";
|
|
import trans from "@/hooks/use-translations";
|
|
import { LaravelEnumMap } from "@/types/global";
|
|
import ProductsSelectionModal from "./Partials/ProductsSelectionModal";
|
|
|
|
interface FormData {
|
|
name: string;
|
|
slug: string;
|
|
seo_title: string;
|
|
seo_description: string;
|
|
is_published: boolean;
|
|
product_category: string;
|
|
consomption_time: string;
|
|
inspiration: string;
|
|
season: string;
|
|
description: string;
|
|
details: string;
|
|
ingredients_list: string;
|
|
preparation_steps: string;
|
|
tips_and_advices: string;
|
|
picture: string | File | null;
|
|
picture_alt: string;
|
|
picture_title: string;
|
|
products: string[];
|
|
}
|
|
|
|
export default function Form({
|
|
model,
|
|
modelName,
|
|
modelRoute,
|
|
action,
|
|
method,
|
|
}: Readonly<{
|
|
model?: RawRecipeModel & { id?: ModelId };
|
|
modelName: string;
|
|
modelRoute: string;
|
|
action: string;
|
|
method: number;
|
|
}>) {
|
|
const enumProductCategory = (usePage().props.enumProductCategory ??
|
|
{}) as LaravelEnumMap;
|
|
const enumConsumptionTime = (usePage().props.enumConsumptionTime ??
|
|
{}) as LaravelEnumMap;
|
|
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;
|
|
pictureUrl?: string | null;
|
|
}[];
|
|
|
|
const precognitionMethod = action === "store" ? "post" : "put";
|
|
const precognitionUrl = route(`back.${modelRoute}.${action}`, model?.id);
|
|
|
|
const form = useForm<FormData>({
|
|
name: model?.name ?? "",
|
|
slug: model?.slug ?? "",
|
|
seo_title: model?.seo_title ?? "",
|
|
seo_description: model?.seo_description ?? "",
|
|
is_published: model?.is_published ?? true,
|
|
product_category: model?.product_category ?? "",
|
|
consomption_time: model?.consomption_time ?? "",
|
|
inspiration: model?.inspiration ?? "",
|
|
season: model?.season ?? "",
|
|
description: model?.description ?? "",
|
|
details: model?.details ?? "",
|
|
ingredients_list: model?.ingredients_list ?? "",
|
|
preparation_steps: model?.preparation_steps ?? "",
|
|
tips_and_advices: model?.tips_and_advices ?? "",
|
|
picture: model?.picture ?? null,
|
|
picture_alt: model?.picture_alt ?? "",
|
|
picture_title: model?.picture_title ?? "",
|
|
products:
|
|
(usePage().props.selectedProductIds as string[] | undefined) ??
|
|
model?.products?.map((p) => String(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 = trans(`models.classes.${modelName}`, {
|
|
choice: 1,
|
|
});
|
|
|
|
const submit = useCrudSubmit({
|
|
method,
|
|
modelRoute,
|
|
action,
|
|
model,
|
|
post,
|
|
data,
|
|
modelNameTrans,
|
|
transform: form.transform,
|
|
fileFields: ["picture"],
|
|
});
|
|
|
|
const transNameLabel = str(trans("validation.attributes.name"))
|
|
.ucFirst()
|
|
.value();
|
|
|
|
return (
|
|
<form onSubmit={submit} className="flex flex-col gap-6">
|
|
<InputError all />
|
|
|
|
{/* Informations de base */}
|
|
<FieldsetWrapper
|
|
title="Informations générales"
|
|
icon={<BookTextIcon className="mr-1" />}
|
|
description="Informations de base de la recette"
|
|
className="grid sm:grid-cols-2 gap-4"
|
|
>
|
|
<SimpleString
|
|
name="name"
|
|
limits={{ min: 3, max: 255 }}
|
|
label={transNameLabel}
|
|
value={data.name}
|
|
setData={setData}
|
|
onBlur={() => validate("name")}
|
|
parentErrors={formErrors}
|
|
/>
|
|
<div className="hidden sm:block" />
|
|
<SimpleString
|
|
name="seo_title"
|
|
limits={{ min: 45, max: 70 }}
|
|
label="Titre SEO"
|
|
value={data.seo_title ?? ""}
|
|
setData={setData}
|
|
onBlur={() => validate("seo_title")}
|
|
parentErrors={formErrors}
|
|
/>
|
|
<SimpleString
|
|
name="seo_description"
|
|
limits={{ min: 50, max: 160 }}
|
|
label="Description SEO"
|
|
value={data.seo_description ?? ""}
|
|
setData={setData}
|
|
onBlur={() => validate("seo_description")}
|
|
parentErrors={formErrors}
|
|
/>
|
|
|
|
<EnumSelect
|
|
name="product_category"
|
|
options={enumProductCategory}
|
|
label="Gamme"
|
|
selected={data.product_category}
|
|
setData={setData}
|
|
parentErrors={formErrors}
|
|
/>
|
|
<EnumSelect
|
|
name="consomption_time"
|
|
options={enumConsumptionTime}
|
|
label="Moment de consommation"
|
|
selected={data.consomption_time}
|
|
setData={setData}
|
|
parentErrors={formErrors}
|
|
/>
|
|
<EnumSelect
|
|
name="inspiration"
|
|
options={enumInspiration}
|
|
label="Inspiration"
|
|
selected={data.inspiration}
|
|
setData={setData}
|
|
parentErrors={formErrors}
|
|
/>
|
|
<EnumSelect
|
|
name="season"
|
|
options={enumSeason}
|
|
label="Saison"
|
|
selected={data.season}
|
|
setData={setData}
|
|
parentErrors={formErrors}
|
|
/>
|
|
|
|
<SimpleSwitch
|
|
name="is_published"
|
|
label="Publié"
|
|
checked={data.is_published}
|
|
setData={setData}
|
|
/>
|
|
</FieldsetWrapper>
|
|
|
|
{/* Textes Longs */}
|
|
<FieldsetWrapper
|
|
title="Détails de la recette"
|
|
icon={<CogIcon className="mr-1" />}
|
|
description="Description, ingrédients, étapes..."
|
|
className="gap-4"
|
|
>
|
|
<SimpleWysiwyg
|
|
name="description"
|
|
limits={{ min: 15, max: 800 }}
|
|
tableName="recipes"
|
|
label="Description"
|
|
value={data.description ?? ""}
|
|
setData={setData}
|
|
required={false}
|
|
/>
|
|
|
|
<SimpleWysiwyg
|
|
name="details"
|
|
limits={{ min: 15, max: 255 }}
|
|
tableName="recipes"
|
|
label="Détails"
|
|
value={data.details ?? ""}
|
|
setData={setData}
|
|
required={false}
|
|
/>
|
|
|
|
<SimpleWysiwyg
|
|
name="ingredients_list"
|
|
limits={{ min: 15, max: 1000 }}
|
|
tableName="recipes"
|
|
label="Liste des ingrédients"
|
|
value={data.ingredients_list ?? ""}
|
|
setData={setData}
|
|
/>
|
|
|
|
<SimpleWysiwyg
|
|
name="preparation_steps"
|
|
limits={{ min: 15, max: 2000 }}
|
|
tableName="recipes"
|
|
label="Étapes de préparation"
|
|
value={data.preparation_steps ?? ""}
|
|
setData={setData}
|
|
/>
|
|
|
|
<SimpleWysiwyg
|
|
name="tips_and_advices"
|
|
limits={{ min: 15, max: 500 }}
|
|
tableName="recipes"
|
|
label="Trucs et astuces"
|
|
value={data.tips_and_advices ?? ""}
|
|
setData={setData}
|
|
required={false}
|
|
/>
|
|
</FieldsetWrapper>
|
|
|
|
{/* Medias */}
|
|
<FieldsetWrapper
|
|
title="Médias"
|
|
icon={<ImageIcon className="mr-1" />}
|
|
description="Fichiers et référencement"
|
|
className="grid sm:grid-cols-2 gap-4"
|
|
>
|
|
<CompleteInputFile
|
|
name="picture"
|
|
label="Image de la recette"
|
|
inputType="image"
|
|
value={data.picture}
|
|
originalData={model?.picture ?? undefined}
|
|
setData={setData}
|
|
validationRules={{ height: "800", width: "800" }}
|
|
reset={reset as (...fields: string[]) => void}
|
|
required={!model?.picture}
|
|
maxFileSize="5120"
|
|
allowedFormats="jpg,jpeg,png,webp"
|
|
/>
|
|
|
|
<SimpleString
|
|
name="picture_title"
|
|
limits={{ min: 4, max: 75 }}
|
|
label="Titre de l'image"
|
|
value={data.picture_title ?? ""}
|
|
setData={setData}
|
|
onBlur={() => validate("picture_title")}
|
|
parentErrors={formErrors}
|
|
/>
|
|
|
|
<SimpleString
|
|
name="picture_alt"
|
|
limits={{ min: 4, max: 75 }}
|
|
label="Texte alternatif de l'image"
|
|
value={data.picture_alt ?? ""}
|
|
setData={setData}
|
|
onBlur={() => validate("picture_alt")}
|
|
parentErrors={formErrors}
|
|
/>
|
|
</FieldsetWrapper>
|
|
|
|
{/* Associated Products */}
|
|
<FieldsetWrapper
|
|
title="Produits associés"
|
|
icon={<CheckIcon className="mr-1" />}
|
|
description="Lier des produits à cette recette"
|
|
className="flex flex-col gap-4"
|
|
>
|
|
<div className="flex justify-between items-center">
|
|
<p className="text-sm text-gray-500">
|
|
{data.products.length} produit(s) sélectionné(s)
|
|
</p>
|
|
<ProductsSelectionModal
|
|
allProducts={allProducts}
|
|
selectedProducts={data.products}
|
|
onToggleProduct={toggleProduct}
|
|
/>
|
|
</div>
|
|
|
|
{data.products.length > 0 && (
|
|
<div className="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-6 gap-4 mt-4 border-t pt-4">
|
|
{data.products.map((productId) => {
|
|
const product = allProducts.find((p) => p.id === productId);
|
|
if (!product) return null;
|
|
return (
|
|
<div
|
|
key={product.id}
|
|
className="flex flex-col items-center justify-between h-full bg-gray-50 p-2 rounded-lg border"
|
|
>
|
|
<img
|
|
src={
|
|
product.pictureUrl ??
|
|
(product.picture
|
|
? product.picture.startsWith("http")
|
|
? product.picture
|
|
: `/${product.picture}`
|
|
: "/assets/img/placeholder.jpg")
|
|
}
|
|
alt={product.name}
|
|
className="w-12 h-12 object-cover rounded-md mb-2"
|
|
/>
|
|
<span
|
|
className="text-xs text-center font-medium line-clamp-2 mb-2 flex-grow"
|
|
title={product.name}
|
|
>
|
|
{product.name}
|
|
</span>
|
|
<Button
|
|
type="button"
|
|
variant="destructive"
|
|
size="sm"
|
|
className="w-full mt-auto"
|
|
onClick={() => toggleProduct(product.id)}
|
|
>
|
|
Retirer
|
|
</Button>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</FieldsetWrapper>
|
|
|
|
<div className="text-center mt-6">
|
|
<SubmitButton modelTrans={modelNameTrans} />
|
|
</div>
|
|
</form>
|
|
);
|
|
}
|