refactor: ui

This commit is contained in:
SonPhung
2026-07-10 01:33:35 +07:00
parent 63d570801a
commit 628d87a55b
7 changed files with 74 additions and 72 deletions

View File

@@ -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) => (
<RuleGroup
key={group.id}
{...group}
onDeleteGroup={handleDeleteGroup}
onOperatorChange={handleOperatorChange}
/>
<div className="flex flex-col gap-4 mb-4">
{groups.map((group: any, index: number) => (
<Fragment key={group.id}>
<RuleGroup
{...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>

View File

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

View File

@@ -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

View File

@@ -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.

View File

@@ -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}>

View File

@@ -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">
@@ -24,20 +24,20 @@ export function GlobalSummaryPanel() {
</h4>
<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>
)}
@@ -57,33 +57,37 @@ export function GlobalSummaryPanel() {
</h4>
<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>

View File

@@ -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 />}