refactor: ui
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useDroppable } from "@dnd-kit/core";
|
||||
import { useCampaignRule } from "@/context/CampaignRuleContext";
|
||||
import { RuleGroup } from "./RuleGroup";
|
||||
import { Fragment } from "react";
|
||||
|
||||
export function Canvas() {
|
||||
const { state, updateState } = useCampaignRule();
|
||||
@@ -23,14 +24,30 @@ export function Canvas() {
|
||||
|
||||
return (
|
||||
<div className="flex-1 p-6 bg-gray-50/50 overflow-y-auto">
|
||||
<div className="space-y-4 mb-4">
|
||||
{groups.map((group: any) => (
|
||||
<div className="flex flex-col gap-4 mb-4">
|
||||
{groups.map((group: any, index: number) => (
|
||||
<Fragment key={group.id}>
|
||||
<RuleGroup
|
||||
key={group.id}
|
||||
{...group}
|
||||
onDeleteGroup={handleDeleteGroup}
|
||||
onOperatorChange={handleOperatorChange}
|
||||
/>
|
||||
{index < groups.length - 1 && (
|
||||
<div className="flex items-center justify-center relative -my-3 z-10 py-1">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-dashed border-muted-foreground/30" />
|
||||
</div>
|
||||
<div className="relative flex justify-center">
|
||||
<button
|
||||
onClick={() => updateState("criteria", { globalOperator: state.criteria.globalOperator === "AND" ? "OR" : "AND" })}
|
||||
className="bg-muted border border-muted-foreground/20 text-[10px] font-bold px-3 py-1 rounded-full text-muted-foreground hover:bg-white hover:text-foreground hover:border-primary transition-all shadow-sm tracking-wider"
|
||||
>
|
||||
{state.criteria.globalOperator}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -40,13 +40,13 @@ export function Condition({ condition, groupId }: ConditionProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3 p-3 bg-gray-50 border rounded-md">
|
||||
<div className="flex-1 font-medium text-sm">
|
||||
<div className="flex items-center gap-2 p-1.5 bg-gray-50/50 hover:bg-gray-50 border rounded-md group transition-colors">
|
||||
<div className="flex-1 font-medium text-xs px-1 text-foreground/90 truncate">
|
||||
{condition.label}
|
||||
</div>
|
||||
|
||||
<select
|
||||
className="flex h-9 w-[120px] items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
className="flex h-7 w-[110px] items-center justify-between whitespace-nowrap rounded-md border border-input bg-white px-2 py-1 text-xs shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
value={condition.operator}
|
||||
onChange={(e) => handleUpdate("operator", e.target.value)}
|
||||
>
|
||||
@@ -57,14 +57,14 @@ export function Condition({ condition, groupId }: ConditionProps) {
|
||||
</select>
|
||||
|
||||
<Input
|
||||
className="flex-1"
|
||||
className="flex-1 h-7 text-xs px-2 bg-white"
|
||||
placeholder="Value"
|
||||
value={condition.value}
|
||||
onChange={(e) => handleUpdate("value", e.target.value)}
|
||||
/>
|
||||
|
||||
<Button variant="ghost" size="icon" onClick={handleDelete} className="text-muted-foreground hover:text-destructive h-8 w-8">
|
||||
<X className="w-4 h-4" />
|
||||
<Button variant="ghost" size="icon" onClick={handleDelete} className="text-muted-foreground/40 hover:text-destructive hover:bg-destructive/10 h-6 w-6 opacity-0 group-hover:opacity-100 transition-all">
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -21,11 +21,11 @@ export function RuleGroup({ id, operator, conditions, onDeleteGroup, onOperatorC
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
className={`relative p-4 border rounded-lg bg-white mb-4 transition-colors ${
|
||||
className={`relative p-2.5 border rounded-lg bg-white transition-colors ${
|
||||
isOver ? "border-primary ring-1 ring-primary/20" : "border-muted"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="bg-muted p-1 rounded-md flex items-center text-xs font-semibold">
|
||||
<button
|
||||
@@ -47,7 +47,7 @@ export function RuleGroup({ id, operator, conditions, onDeleteGroup, onOperatorC
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1.5">
|
||||
{conditions.length === 0 ? (
|
||||
<div className="text-center py-4 border border-dashed rounded-md text-sm text-muted-foreground">
|
||||
Drop condition here
|
||||
|
||||
@@ -195,8 +195,8 @@ export function Sidebar() {
|
||||
}, [search, selectedGroupIds]);
|
||||
|
||||
return (
|
||||
<div className="w-72 border-r bg-gray-50/50 flex flex-col h-full">
|
||||
<div className="p-4 border-b bg-white flex flex-col gap-3">
|
||||
<div className="w-60 shrink-0 border-r bg-gray-50/50 flex flex-col h-full">
|
||||
<div className="p-3 border-b bg-white flex flex-col gap-3">
|
||||
{/* Ô tìm kiếm */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
@@ -256,7 +256,7 @@ export function Sidebar() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4 flex flex-col gap-6">
|
||||
<div className="flex-1 overflow-y-auto p-3 flex flex-col gap-5">
|
||||
{filteredGroups.length === 0 ? (
|
||||
<div className="text-center text-sm text-muted-foreground mt-4">
|
||||
Không tìm thấy trường nào.
|
||||
|
||||
@@ -67,24 +67,7 @@ export function CriteriaBuilder() {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-sm font-medium">Match</span>
|
||||
<div className="bg-muted p-1 rounded-md flex items-center text-xs font-semibold">
|
||||
<button
|
||||
onClick={() => updateState("criteria", { globalOperator: "AND" })}
|
||||
className={`px-3 py-1 rounded-sm transition-colors ${state.criteria.globalOperator === "AND" ? "bg-white shadow-sm" : "text-muted-foreground"}`}
|
||||
>
|
||||
ALL
|
||||
</button>
|
||||
<button
|
||||
onClick={() => updateState("criteria", { globalOperator: "OR" })}
|
||||
className={`px-3 py-1 rounded-sm transition-colors ${state.criteria.globalOperator === "OR" ? "bg-white shadow-sm" : "text-muted-foreground"}`}
|
||||
>
|
||||
ANY
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-sm font-medium">of the following groups:</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex flex-1 min-h-[600px] border rounded-xl overflow-hidden bg-background">
|
||||
<DndContext sensors={sensors} onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
|
||||
|
||||
@@ -6,13 +6,13 @@ export function GlobalSummaryPanel() {
|
||||
const { general, criteria, formula, notification } = state;
|
||||
|
||||
return (
|
||||
<div className="w-80 lg:w-96 bg-white border rounded-xl shadow-sm flex flex-col h-full overflow-hidden shrink-0">
|
||||
<div className="p-5 border-b bg-slate-50/50">
|
||||
<h3 className="font-semibold text-lg">Campaign Summary</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">Live preview of your configurations</p>
|
||||
<div className="w-64 lg:w-72 bg-white border rounded-xl shadow-sm flex flex-col h-full overflow-hidden shrink-0">
|
||||
<div className="p-4 border-b bg-slate-50/50">
|
||||
<h3 className="font-semibold text-[15px]">Campaign Summary</h3>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">Live preview of configurations</p>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-5 space-y-6">
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-5">
|
||||
{/* General Info */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -25,19 +25,19 @@ export function GlobalSummaryPanel() {
|
||||
<button onClick={() => setStep(0)} className="text-xs text-primary hover:underline">Edit</button>
|
||||
</div>
|
||||
|
||||
<div className="pl-7 space-y-2 text-sm">
|
||||
<div className="pt-2 space-y-1.5 text-xs">
|
||||
{general.name ? (
|
||||
<>
|
||||
<p><span className="text-muted-foreground">Name:</span> <span className="font-medium">{general.name}</span></p>
|
||||
{general.type && <p><span className="text-muted-foreground">Type:</span> {general.type}</p>}
|
||||
<div className="flex justify-between"><span className="text-muted-foreground">Name:</span> <span className="font-medium truncate ml-2 text-right">{general.name}</span></div>
|
||||
{general.type && <div className="flex justify-between"><span className="text-muted-foreground">Type:</span> <span className="truncate ml-2 text-right">{general.type}</span></div>}
|
||||
{general.effectiveDateStart && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
<div className="text-muted-foreground mt-1">
|
||||
{general.effectiveDateStart} {general.effectiveDateEnd && `to ${general.effectiveDateEnd}`}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<p className="text-muted-foreground italic text-xs flex items-center gap-1.5">
|
||||
<p className="text-muted-foreground italic flex items-center gap-1.5">
|
||||
<AlertCircle size={12} /> Not configured yet
|
||||
</p>
|
||||
)}
|
||||
@@ -58,32 +58,36 @@ export function GlobalSummaryPanel() {
|
||||
<button onClick={() => setStep(1)} className="text-xs text-primary hover:underline">Edit</button>
|
||||
</div>
|
||||
|
||||
<div className="pl-7">
|
||||
<div className="pt-2">
|
||||
{criteria.groups.length > 0 ? (
|
||||
<div className="space-y-3 text-sm">
|
||||
<p className="text-xs text-muted-foreground mb-2">Match <strong className="text-foreground">{criteria.globalOperator}</strong> of:</p>
|
||||
<div className="space-y-2.5 text-xs">
|
||||
{criteria.groups.map((g, i) => (
|
||||
<div key={g.id} className="bg-slate-50/50 border border-slate-200 rounded-lg p-3 text-xs shadow-sm">
|
||||
<p className="font-semibold text-slate-500 mb-2.5 flex items-center gap-1.5">
|
||||
<span className="bg-slate-200 text-slate-700 px-1.5 py-0.5 rounded uppercase text-[10px] font-bold">Group {i + 1}</span>
|
||||
<span className="text-primary font-medium">{g.operator}</span>
|
||||
</p>
|
||||
<ul className="space-y-2">
|
||||
<div key={g.id} className="bg-slate-50/50 border border-slate-200 rounded-md p-2 shadow-sm">
|
||||
<div className="flex items-center justify-between mb-1.5 pb-1.5 border-b border-slate-200/60">
|
||||
<span className="text-slate-500 uppercase text-[9px] font-bold tracking-wider">Group {i + 1}</span>
|
||||
<span className="text-[9px] font-bold bg-slate-200/50 text-slate-600 px-1.5 py-0.5 rounded">{g.operator}</span>
|
||||
</div>
|
||||
<ul className="space-y-1">
|
||||
{g.conditions.map(c => (
|
||||
<li key={c.id} className="flex flex-col sm:flex-row sm:items-center justify-between gap-1.5 bg-white border border-slate-200/60 p-2 rounded-md shadow-sm transition-all hover:border-slate-300">
|
||||
<span className="font-medium text-slate-700 shrink-0">{c.label}</span>
|
||||
<div className="flex items-center gap-2 text-right justify-end">
|
||||
<span className="text-muted-foreground font-mono bg-slate-100 px-1.5 py-0.5 rounded text-[10px]">{c.operator}</span>
|
||||
<span className="text-primary font-mono bg-primary/10 px-2 py-0.5 rounded text-xs font-semibold break-all max-w-[120px] lg:max-w-[150px]">
|
||||
<li key={c.id} className="flex items-center justify-between gap-2 py-0.5">
|
||||
<span className="font-medium text-slate-700 truncate">{c.label}</span>
|
||||
<div className="flex items-center gap-1.5 shrink-0">
|
||||
<span className="text-muted-foreground font-mono text-[9px]">{c.operator}</span>
|
||||
<span className="text-primary font-mono bg-primary/10 px-1.5 py-0.5 rounded text-[10px] font-bold max-w-[70px] truncate" title={c.value}>
|
||||
{c.value || '...'}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
{g.conditions.length === 0 && <span className="italic text-muted-foreground block text-center p-2 bg-white/50 rounded border border-dashed">Empty</span>}
|
||||
{g.conditions.length === 0 && <span className="italic text-muted-foreground block text-center text-[10px] py-1">Empty</span>}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
{criteria.groups.length > 1 && (
|
||||
<p className="text-[10px] text-muted-foreground text-center mt-2">
|
||||
Groups are matched by <strong className="text-foreground">{criteria.globalOperator}</strong>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground italic text-xs flex items-center gap-1.5">
|
||||
@@ -104,7 +108,7 @@ export function GlobalSummaryPanel() {
|
||||
</h4>
|
||||
<button onClick={() => setStep(2)} className="text-xs text-primary hover:underline">Edit</button>
|
||||
</div>
|
||||
<div className="pl-7">
|
||||
<div className="pt-2">
|
||||
<p className="text-muted-foreground italic text-xs flex items-center gap-1.5">
|
||||
<AlertCircle size={12} /> Pending configuration
|
||||
</p>
|
||||
@@ -122,7 +126,7 @@ export function GlobalSummaryPanel() {
|
||||
</h4>
|
||||
<button onClick={() => setStep(3)} className="text-xs text-primary hover:underline">Edit</button>
|
||||
</div>
|
||||
<div className="pl-7">
|
||||
<div className="pt-2">
|
||||
<p className="text-muted-foreground italic text-xs flex items-center gap-1.5">
|
||||
<AlertCircle size={12} /> Pending configuration
|
||||
</p>
|
||||
|
||||
@@ -24,10 +24,8 @@ function WizardContent() {
|
||||
|
||||
<div className="flex flex-1 gap-4 items-start min-h-0">
|
||||
<div className="flex-1 flex flex-col min-w-0 pb-4">
|
||||
<Stepper steps={STEPS} currentStep={currentStep} />
|
||||
|
||||
<div className="bg-white rounded-xl shadow-sm border mt-3 flex flex-col">
|
||||
<div className="p-4">
|
||||
<div className="bg-white rounded-xl shadow-sm border flex flex-col h-full">
|
||||
<div className="p-4 flex-1">
|
||||
{currentStep === 0 && <GeneralStep />}
|
||||
{currentStep === 1 && <CriteriaStep />}
|
||||
{currentStep === 2 && <FormulaStep />}
|
||||
|
||||
Reference in New Issue
Block a user