Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/modules/onboarding/blocks/OnboardingBlockForm.tsx
T

439 lines
18 KiB
TypeScript

import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import EntityIcon from "~/components/layouts/icons/EntityIcon";
import ButtonTertiary from "~/components/ui/buttons/ButtonTertiary";
import InputGroup from "~/components/ui/forms/InputGroup";
import QuestionMarkFilledIcon from "~/components/ui/icons/QuestionMarkFilledIcon";
import XIcon from "~/components/ui/icons/XIcon";
import InputCheckbox from "~/components/ui/input/InputCheckbox";
import InputRadioGroup from "~/components/ui/input/InputRadioGroup";
import InputSelect from "~/components/ui/input/InputSelect";
import InputSelector from "~/components/ui/input/InputSelector";
import InputText from "~/components/ui/input/InputText";
import CollapsibleRow from "~/components/ui/tables/CollapsibleRow";
import OnboardingStepUtils from "~/modules/onboarding/utils/OnboardingStepUtils";
import OnboardingGridForm from "./grid/OnboardingGridForm";
import StringUtils from "~/utils/shared/StringUtils";
import { defaultOnboardingStepBlocks } from "./defaultOnboarding/defaultOnboardingStepBlocks";
import {
defaultOnboardingBlock,
defaultOnboardingStepBlock,
OnboardingBlockDto,
OnboardingBlockStyle,
OnboardingBlockStyles,
OnboardingInputOptionDto,
OnboardingStepBlockDto,
} from "./OnboardingBlockUtils";
import ReactSortablePkg from "react-sortablejs";
const { ReactSortable } = ReactSortablePkg;
export default function OnboardingBlockForm({ item, onUpdate }: { item?: OnboardingBlockDto; onUpdate: (item: OnboardingBlockDto) => void }) {
const { t } = useTranslation();
const [state, setState] = useState<OnboardingBlockDto>(item || defaultOnboardingBlock);
useEffect(() => {
onUpdate(state);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state]);
return (
<div className="space-y-4">
<InputGroup title={t("onboarding.block.design")}>
<div className="grid grid-cols-2 gap-2">
<InputSelector
withSearch={false}
className="col-span-2"
title={t("onboarding.block.style")}
value={state.style}
setValue={(value) => setState({ ...state, style: value as OnboardingBlockStyle })}
options={OnboardingBlockStyles.map((f) => f)}
/>
<InputSelector
withSearch={false}
title={t("onboarding.block.height")}
value={state.height}
disabled={state.style !== "modal"}
setValue={(value) => setState({ ...state, height: value as any })}
options={[
{ value: "xs", name: "xs" },
{ value: "sm", name: "sm" },
{ value: "md", name: "md" },
{ value: "lg", name: "lg" },
{ value: "xl", name: "xl" },
{ value: "auto", name: "auto" },
]}
/>
<InputSelector
withSearch={false}
title={t("onboarding.block.canBeDismissed")}
value={state.canBeDismissed ? "true" : "false"}
disabled={state.style !== "modal"}
setValue={(value) => setState({ ...state, canBeDismissed: value === "true" })}
options={[
{ value: "true", name: t("shared.yes") },
{ value: "false", name: t("shared.no") },
]}
/>
</div>
</InputGroup>
<InputGroup title={t("onboarding.step.plural")} description={t("onboarding.block.stepsDescription")}>
<div className="space-y-2">
<ReactSortable className="flex flex-col space-y-2" animation={200} list={state.steps} setList={(steps) => setState({ ...state, steps })}>
{state.steps.map((item, index) => (
<CollapsibleRow
draggable
key={item.id}
title={item.title || t("onboarding.block.untitledStep")}
value={OnboardingStepUtils.getStepDescription(item)}
initial={!item.title}
onRemove={() => {
const steps = state.steps ?? [];
const index = steps.findIndex((e) => e.id === item.id);
if (index > -1) {
steps.splice(index, 1);
setState({ ...state, steps });
}
}}
>
<StepForm
item={item}
onUpdate={(item) => {
const steps = state.steps ?? [];
steps[index] = item;
setState({ ...state, steps });
}}
/>
</CollapsibleRow>
))}
</ReactSortable>
<div className="flex justify-between space-x-2">
<ButtonTertiary
onClick={() =>
setState({
...state,
steps: [
...(state.steps ?? []),
{
id: Math.floor(Math.random() * 10000).toString(),
// ...defaultOnboardingStepBlock,
title: "",
description: "",
links: [],
gallery: [],
input: [],
},
],
})
}
>
{t("onboarding.block.actions.addStep")}
</ButtonTertiary>
<div>
{state.steps.length > 0 ? (
<ButtonTertiary
destructive
onClick={() => {
if (prompt(t("onboarding.block.confirmClearPrompt")) === "yes") setState({ ...state, steps: [] });
}}
>
{t("shared.clear")}
</ButtonTertiary>
) : (
<ButtonTertiary onClick={() => setState({ ...state, steps: defaultOnboardingStepBlocks })}>{t("onboarding.block.actions.loadSampleSteps")}</ButtonTertiary>
)}
</div>
</div>
</div>
</InputGroup>
</div>
);
}
function StepForm({ item, onUpdate }: { item: OnboardingStepBlockDto; onUpdate: (item: OnboardingStepBlockDto) => void }) {
const { t } = useTranslation();
const [state, setState] = useState<OnboardingStepBlockDto>(item || defaultOnboardingStepBlock);
useEffect(() => {
onUpdate(state);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state]);
return (
<div className="grid grid-cols-1 gap-4 p-2">
<InputText title={t("onboarding.step.title")} value={state.title} setValue={(e) => setState({ ...state, title: e?.toString() })} required />
<InputText
title={t("shared.description")}
value={state.description}
setValue={(e) => setState({ ...state, description: e?.toString() })}
rows={5}
hint={t("onboarding.block.markdownSupported")}
/>
<InputText
title={t("shared.icon")}
value={state.icon}
setValue={(e) => setState({ ...state, icon: e?.toString() })}
hint={<div className="text-muted-foreground">{t("onboarding.block.iconHint")}</div>}
button={
<div className="absolute inset-y-0 right-0 flex py-0.5 pr-0.5 ">
<kbd className="border-border text-muted-foreground bg-secondary inline-flex w-10 items-center justify-center rounded border px-1 text-center font-sans text-xs font-medium">
{state.icon ? (
<EntityIcon className="text-muted-foreground h-7 w-7" icon={state.icon} title={state.title} />
) : (
<QuestionMarkFilledIcon className="text-muted-foreground h-5 w-5" aria-hidden="true" />
)}
</kbd>
</div>
}
/>
<InputGroup title={t("onboarding.block.links")}>
<div className="flex flex-col space-y-2">
{state.links.map((link, index) => (
<div key={index} className="group relative grid grid-cols-4 gap-2">
<button
onClick={() => {
const links = state.links;
links.splice(index, 1);
setState({ ...state, links });
}}
type="button"
className="bg-background text-muted-foreground absolute right-0 top-0 -mt-3 hidden origin-top-right justify-center rounded-full hover:text-red-500 group-hover:flex"
>
<XIcon className="h-6 w-6" />
</button>
<InputText
title={t("shared.link")}
value={link.text}
setValue={(e) => setState({ ...state, links: state.links.map((step, i) => (i === index ? { ...step, text: e.toString() } : step)) })}
/>
<InputText
title={t("onboarding.block.href")}
value={link.href}
setValue={(e) => setState({ ...state, links: state.links.map((step, i) => (i === index ? { ...step, href: e.toString() } : step)) })}
/>
<InputCheckbox
title={t("onboarding.block.isPrimary")}
value={link.isPrimary}
setValue={(e) => setState({ ...state, links: state.links.map((link, i) => (i === index ? { ...link, isPrimary: Boolean(e) } : link)) })}
/>
<InputCheckbox
title={t("onboarding.block.targetBlank")}
value={link.target === "_blank"}
setValue={(e) => setState({ ...state, links: state.links.map((link, i) => (i === index ? { ...link, target: e ? "_blank" : "" } : link)) })}
/>
</div>
))}
<ButtonTertiary onClick={() => setState({ ...state, links: [...state.links, { text: t("shared.link"), href: "#", isPrimary: false }] })}>
{t("onboarding.block.actions.addLink")}
</ButtonTertiary>
</div>
</InputGroup>
<InputGroup title={t("onboarding.block.gallery")}>
<div className="flex flex-col space-y-2">
<ReactSortable className="flex flex-col space-y-2" animation={200} list={state.gallery} setList={(gallery) => setState({ ...state, gallery })}>
{state.gallery.map((item, index) => (
<CollapsibleRow
key={index}
title={item.title || t("onboarding.block.untitledGalleryItem")}
value={item.title || t("onboarding.block.untitledGalleryItem")}
initial={!item.title}
onRemove={() => {
const gallery = state.gallery ?? [];
gallery.splice(index, 1);
setState({ ...state, gallery });
}}
>
<div className="grid grid-cols-1 gap-2">
<InputRadioGroup
title={t("onboarding.step.type")}
value={item.type}
setValue={(e) =>
setState({
...state,
gallery: state.gallery.map((item, i) => (i === index ? { ...item, type: e?.toString() as "image" | "video" } : item)),
})
}
options={[
{ name: t("onboarding.block.mediaTypes.image"), value: "image" },
{ name: t("onboarding.block.mediaTypes.video"), value: "video" },
]}
/>
<InputText
title={t("onboarding.step.title")}
type="text"
value={item.title}
setValue={(e) => setState({ ...state, gallery: state.gallery.map((item, i) => (i === index ? { ...item, title: e.toString() } : item)) })}
/>
<InputText
title={t("onboarding.block.src")}
type="text"
value={item.src}
setValue={(e) => setState({ ...state, gallery: state.gallery.map((item, i) => (i === index ? { ...item, src: e.toString() } : item)) })}
/>
</div>
</CollapsibleRow>
))}
</ReactSortable>
<ButtonTertiary
onClick={() =>
setState({
...state,
gallery: [
...(state.gallery ?? []),
{
id: Math.floor(Math.random() * 10000).toString(),
type: "image",
title: t("onboarding.block.defaultGalleryTitle", { number: state.gallery.length + 1 }),
src: "",
},
],
})
}
>
{t("onboarding.block.actions.addGalleryItem")}
</ButtonTertiary>
</div>
</InputGroup>
<InputGroup title={t("onboarding.block.input")}>
<div className="flex flex-col space-y-2">
<OnboardingGridForm item={state.inputGrid} onUpdate={(item) => setState({ ...state, inputGrid: item })} />
<ReactSortable className="flex flex-col space-y-2" animation={200} list={state.input} setList={(input) => setState({ ...state, input })}>
{state.input.map((item, index) => (
<CollapsibleRow
key={index}
title={item.title || t("onboarding.block.untitledInput")}
value={item.title || t("onboarding.block.untitledInput")}
initial={!item.title}
onRemove={() => {
const input = state.input ?? [];
input.splice(index, 1);
setState({ ...state, input });
}}
>
<div className="grid grid-cols-3 gap-2">
<InputText
title={t("onboarding.step.title")}
type="text"
value={item.title}
setValue={(e) =>
setState({
...state,
input: state.input.map((item, i) =>
i === index ? { ...item, title: e.toString(), name: StringUtils.toCamelCase(e?.toString().toLowerCase()) } : item
),
})
}
/>
<InputText
title={t("shared.name")}
type="text"
value={item.name}
setValue={(e) => setState({ ...state, input: state.input.map((item, i) => (i === index ? { ...item, name: e.toString() } : item)) })}
/>
<InputCheckbox
title={t("onboarding.block.isRequired")}
value={item.isRequired}
setValue={(e) => setState({ ...state, input: state.input.map((item, i) => (i === index ? { ...item, isRequired: Boolean(e) } : item)) })}
/>
<InputSelect
className="col-span-3"
title={t("onboarding.step.type")}
value={item.type}
setValue={(e) =>
setState({
...state,
input: state.input.map((item, i) => (i === index ? { ...item, type: e?.toString() as "text" | "select" } : item)),
})
}
options={[
{ name: t("onboarding.block.inputTypes.text"), value: "text" },
{ name: t("onboarding.block.inputTypes.select"), value: "select" },
]}
/>
{item.type === "select" && (
<div className="col-span-3">
<SelectOptionsForm
options={item.options}
setOptions={(options) => setState({ ...state, input: state.input.map((item, i) => (i === index ? { ...item, options } : item)) })}
/>
</div>
)}
</div>
</CollapsibleRow>
))}
</ReactSortable>
<ButtonTertiary
onClick={() =>
setState({
...state,
input: [
...(state.input ?? []),
{
id: Math.floor(Math.random() * 10000).toString(),
type: "text",
name: "",
title: "",
isRequired: false,
options: [],
},
],
})
}
>
{t("onboarding.block.actions.addInput")}
</ButtonTertiary>
</div>
</InputGroup>
</div>
);
}
function SelectOptionsForm({ options, setOptions }: { options: OnboardingInputOptionDto[]; setOptions: (options: OnboardingInputOptionDto[]) => void }) {
const { t } = useTranslation();
const [state, setState] = useState<OnboardingInputOptionDto[]>(options ?? []);
useEffect(() => {
setOptions(state);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state]);
return (
<div className="flex flex-col space-y-2">
{options.map((option, index) => (
<div key={index} className="group relative grid grid-cols-2 gap-2">
<button
onClick={() => {
const options = state ?? [];
options.splice(index, 1);
setState(options);
}}
type="button"
className="bg-background text-muted-foreground absolute right-0 top-0 -mt-3 hidden origin-top-right justify-center rounded-full hover:text-red-500 group-hover:flex"
>
<XIcon className="h-6 w-6" />
</button>
<InputText
title={t("onboarding.step.title")}
value={option.name}
setValue={(e) =>
setState(
options.map((option, i) =>
i === index ? { ...option, name: e.toString(), value: StringUtils.toCamelCase(e?.toString().toLowerCase()) } : option
)
)
}
/>
<InputText
title={t("shared.value")}
value={option.value}
setValue={(e) => setState(options.map((option, i) => (i === index ? { ...option, value: e.toString() } : option)))}
/>
</div>
))}
<ButtonTertiary onClick={() => setState([...options, { value: "", name: "" }])}>{t("onboarding.block.actions.addOption")}</ButtonTertiary>
</div>
);
}