mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-09 16:20:28 -06:00
253 lines
7.9 KiB
TypeScript
253 lines
7.9 KiB
TypeScript
"use client";
|
|
|
|
import { FilterIcon, PlusIcon, UsersIcon } from "lucide-react";
|
|
import { useRouter } from "next/navigation";
|
|
import { useMemo, useState } from "react";
|
|
import toast from "react-hot-toast";
|
|
|
|
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
|
|
import { TActionClass } from "@formbricks/types/actionClasses";
|
|
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
|
import { TBaseFilter, TSegment, ZSegmentFilters } from "@formbricks/types/segment";
|
|
import { Button } from "@formbricks/ui/Button";
|
|
import { Input } from "@formbricks/ui/Input";
|
|
import { Modal } from "@formbricks/ui/Modal";
|
|
|
|
import { createSegmentAction } from "../lib/actions";
|
|
import { AddFilterModal } from "./AddFilterModal";
|
|
import { SegmentEditor } from "./SegmentEditor";
|
|
|
|
type TCreateSegmentModalProps = {
|
|
environmentId: string;
|
|
segments: TSegment[];
|
|
attributeClasses: TAttributeClass[];
|
|
actionClasses: TActionClass[];
|
|
};
|
|
|
|
export const CreateSegmentModal = ({
|
|
environmentId,
|
|
actionClasses,
|
|
attributeClasses,
|
|
segments,
|
|
}: TCreateSegmentModalProps) => {
|
|
const router = useRouter();
|
|
const initialSegmentState = {
|
|
title: "",
|
|
description: "",
|
|
isPrivate: false,
|
|
filters: [],
|
|
environmentId,
|
|
id: "",
|
|
surveys: [],
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
const [open, setOpen] = useState(false);
|
|
const [addFilterModalOpen, setAddFilterModalOpen] = useState(false);
|
|
const [segment, setSegment] = useState<TSegment>(initialSegmentState);
|
|
const [isCreatingSegment, setIsCreatingSegment] = useState(false);
|
|
|
|
const handleResetState = () => {
|
|
setSegment(initialSegmentState);
|
|
setOpen(false);
|
|
};
|
|
|
|
const handleAddFilterInGroup = (filter: TBaseFilter) => {
|
|
const updatedSegment = structuredClone(segment);
|
|
if (updatedSegment?.filters?.length === 0) {
|
|
updatedSegment.filters.push({
|
|
...filter,
|
|
connector: null,
|
|
});
|
|
} else {
|
|
updatedSegment?.filters.push(filter);
|
|
}
|
|
|
|
setSegment(updatedSegment);
|
|
};
|
|
|
|
const handleCreateSegment = async () => {
|
|
if (!segment.title) {
|
|
toast.error("Title is required.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setIsCreatingSegment(true);
|
|
await createSegmentAction({
|
|
title: segment.title,
|
|
description: segment.description ?? "",
|
|
isPrivate: segment.isPrivate,
|
|
filters: segment.filters,
|
|
environmentId,
|
|
surveyId: "",
|
|
});
|
|
|
|
setIsCreatingSegment(false);
|
|
toast.success("Segment created successfully!");
|
|
} catch (err: any) {
|
|
// parse the segment filters to check if they are valid
|
|
const parsedFilters = ZSegmentFilters.safeParse(segment.filters);
|
|
if (!parsedFilters.success) {
|
|
toast.error("Invalid filters. Please check the filters and try again.");
|
|
} else {
|
|
toast.error("Something went wrong. Please try again.");
|
|
}
|
|
setIsCreatingSegment(false);
|
|
return;
|
|
}
|
|
|
|
handleResetState();
|
|
setIsCreatingSegment(false);
|
|
router.refresh();
|
|
};
|
|
|
|
const isSaveDisabled = useMemo(() => {
|
|
// check if title is empty
|
|
|
|
if (!segment.title) {
|
|
return true;
|
|
}
|
|
|
|
// parse the filters to check if they are valid
|
|
const parsedFilters = ZSegmentFilters.safeParse(segment.filters);
|
|
if (!parsedFilters.success) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}, [segment]);
|
|
|
|
return (
|
|
<>
|
|
<Button variant="darkCTA" size="sm" onClick={() => setOpen(true)} EndIcon={PlusIcon}>
|
|
Create segment
|
|
</Button>
|
|
|
|
<Modal
|
|
open={open}
|
|
setOpen={() => {
|
|
handleResetState();
|
|
}}
|
|
noPadding
|
|
closeOnOutsideClick={false}
|
|
className="md:w-full"
|
|
size="lg">
|
|
<div className="rounded-lg bg-slate-50">
|
|
<div className="rounded-t-lg bg-slate-100">
|
|
<div className="flex w-full items-center gap-4 p-6">
|
|
<div className="flex items-center space-x-2">
|
|
<div className="mr-1.5 h-6 w-6 text-slate-500">
|
|
<UsersIcon className="h-5 w-5" />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-base font-medium">Create Segment</h3>
|
|
<p className="text-sm text-slate-600">
|
|
Segments help you target the users with the same characteristics easily.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col overflow-auto rounded-lg bg-white p-6">
|
|
<div className="flex w-full items-center gap-4">
|
|
<div className="flex w-1/2 flex-col gap-2">
|
|
<label className="text-sm font-medium text-slate-900">Title</label>
|
|
<div className="relative flex flex-col gap-1">
|
|
<Input
|
|
placeholder="Ex. Power Users"
|
|
onChange={(e) => {
|
|
setSegment((prev) => ({
|
|
...prev,
|
|
title: e.target.value,
|
|
}));
|
|
}}
|
|
className="w-auto"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex w-1/2 flex-col gap-2">
|
|
<label className="text-sm font-medium text-slate-900">Description</label>
|
|
<Input
|
|
placeholder="Ex. Fully activated recurring users"
|
|
onChange={(e) => {
|
|
setSegment((prev) => ({
|
|
...prev,
|
|
description: e.target.value,
|
|
}));
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<label className="my-4 text-sm font-medium text-slate-900">Targeting</label>
|
|
<div className="filter-scrollbar flex w-full flex-col gap-4 overflow-auto rounded-lg border border-slate-200 bg-slate-50 p-4">
|
|
{segment?.filters?.length === 0 && (
|
|
<div className="-mb-2 flex items-center gap-1">
|
|
<FilterIcon className="h-5 w-5 text-slate-700" />
|
|
<h3 className="text-sm font-medium text-slate-700">Add your first filter to get started</h3>
|
|
</div>
|
|
)}
|
|
|
|
<SegmentEditor
|
|
environmentId={environmentId}
|
|
segment={segment}
|
|
setSegment={setSegment}
|
|
group={segment.filters}
|
|
actionClasses={actionClasses}
|
|
attributeClasses={attributeClasses}
|
|
segments={segments}
|
|
/>
|
|
|
|
<Button
|
|
className="w-fit"
|
|
variant="secondary"
|
|
size="sm"
|
|
onClick={() => setAddFilterModalOpen(true)}>
|
|
Add Filter
|
|
</Button>
|
|
|
|
<AddFilterModal
|
|
onAddFilter={(filter) => {
|
|
handleAddFilterInGroup(filter);
|
|
}}
|
|
open={addFilterModalOpen}
|
|
setOpen={setAddFilterModalOpen}
|
|
actionClasses={actionClasses}
|
|
attributeClasses={attributeClasses}
|
|
segments={segments}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex justify-end pt-4">
|
|
<div className="flex space-x-2">
|
|
<Button
|
|
type="button"
|
|
variant="minimal"
|
|
onClick={() => {
|
|
handleResetState();
|
|
}}>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
variant="darkCTA"
|
|
type="submit"
|
|
loading={isCreatingSegment}
|
|
disabled={isSaveDisabled}
|
|
onClick={() => {
|
|
handleCreateSegment();
|
|
}}>
|
|
Create segment
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
</>
|
|
);
|
|
};
|