ldc-ovoteam/resources/ts/back/Pages/Recipes/Form.tsx

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>
);
}