Some checks failed
Deploy MyApp on Same Server / build-and-deploy (push) Has been cancelled
Avoid converting empty string to 0 in value1 and value2 number inputs, which caused unexpected behavior in some browsers when users cleared the field.
148 lines
8.1 KiB
TypeScript
148 lines
8.1 KiB
TypeScript
'use client'
|
|
|
|
import { Trash2 } from 'lucide-react'
|
|
import { SENSOR_TYPES, COMPARISON_TYPES, SensorType, ComparisonType } from './constants'
|
|
import { getSensorIcon, getSensorLabel, getSensorUnit, getComparisonLabel } from './utils'
|
|
import { CreateAlertRuleRequest } from '@/lib/api'
|
|
|
|
type RuleFormCardProps = {
|
|
rule: CreateAlertRuleRequest
|
|
index: number
|
|
totalRules: number
|
|
onUpdate: (index: number, updates: Partial<CreateAlertRuleRequest>) => void
|
|
onRemove: (index: number) => void
|
|
}
|
|
|
|
export function RuleFormCard({ rule, index, totalRules, onUpdate, onRemove }: RuleFormCardProps) {
|
|
const needsMax = COMPARISON_TYPES.find(c => c.value === rule.comparisonType)?.needsMax
|
|
const SensorIcon = getSensorIcon(rule.sensorType)
|
|
|
|
return (
|
|
<div className="bg-gradient-to-br from-orange-50 to-red-50 border border-orange-200 rounded-xl p-3 relative shadow-sm">
|
|
{/* Header - Compact */}
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-xs font-bold text-orange-700 bg-white/80 px-2 py-0.5 rounded-full shadow-sm">
|
|
{totalRules === 1 ? '⚡ شرط' : `⚡ شرط ${index + 1}`}
|
|
</span>
|
|
{/* Compact summary of current selection */}
|
|
<span className="text-xs text-gray-500 hidden sm:inline">
|
|
<SensorIcon className="w-3 h-3 inline ml-1" />
|
|
{getSensorLabel(rule.sensorType)}
|
|
{' | '}
|
|
{getComparisonLabel(rule.comparisonType)}
|
|
{' | '}
|
|
{rule.value1}{needsMax ? ` - ${rule.value2 ?? '?'}` : ''} {getSensorUnit(rule.sensorType)}
|
|
</span>
|
|
</div>
|
|
{totalRules > 1 && (
|
|
<button
|
|
type="button"
|
|
onClick={() => onRemove(index)}
|
|
className="p-1.5 text-red-500 hover:bg-red-100 rounded-lg transition-colors"
|
|
title="حذف این شرط"
|
|
>
|
|
<Trash2 className="w-3.5 h-3.5" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Compact row: Sensor | Comparison | Value */}
|
|
<div className="flex flex-col sm:flex-row gap-2">
|
|
{/* Sensor Type - Compact Select */}
|
|
<div className="flex-1 min-w-0">
|
|
<select
|
|
value={rule.sensorType}
|
|
onChange={(e) => onUpdate(index, { sensorType: Number(e.target.value) as SensorType })}
|
|
className="w-full appearance-none px-3 py-2 pr-8 bg-white border border-orange-200 rounded-lg text-sm font-medium text-gray-700 focus:ring-2 focus:ring-orange-500 focus:border-orange-500 cursor-pointer"
|
|
style={{
|
|
backgroundImage: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e")`,
|
|
backgroundPosition: 'left 0.5rem center',
|
|
backgroundRepeat: 'no-repeat',
|
|
backgroundSize: '1.25rem'
|
|
}}
|
|
>
|
|
{SENSOR_TYPES.map(sensor => (
|
|
<option key={sensor.value} value={sensor.value}>
|
|
{sensor.label} ({sensor.unit})
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Comparison Type - Compact Select */}
|
|
<div className="flex-1 min-w-0">
|
|
<select
|
|
value={rule.comparisonType}
|
|
onChange={(e) => onUpdate(index, { comparisonType: Number(e.target.value) as ComparisonType })}
|
|
className="w-full appearance-none px-3 py-2 pr-8 bg-white border border-orange-200 rounded-lg text-sm font-medium text-gray-700 focus:ring-2 focus:ring-orange-500 focus:border-orange-500 cursor-pointer"
|
|
style={{
|
|
backgroundImage: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e")`,
|
|
backgroundPosition: 'left 0.5rem center',
|
|
backgroundRepeat: 'no-repeat',
|
|
backgroundSize: '1.25rem'
|
|
}}
|
|
>
|
|
{COMPARISON_TYPES.map(comp => (
|
|
<option key={comp.value} value={comp.value}>
|
|
{comp.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Threshold Values - Compact Inputs */}
|
|
<div className="flex gap-1.5 items-center flex-1">
|
|
<div className="relative flex-1 min-w-0">
|
|
<input
|
|
type="number"
|
|
step="0.01"
|
|
value={rule.value1}
|
|
onChange={(e) => {
|
|
const val = e.target.value
|
|
// Only update if value is not empty; otherwise keep previous value
|
|
// to avoid Number('') → 0 breaking the input in some browsers
|
|
if (val !== '') {
|
|
onUpdate(index, { value1: Number(val) })
|
|
}
|
|
}}
|
|
className="w-full px-3 py-2 pl-10 bg-white border border-orange-200 rounded-lg text-sm font-medium focus:ring-2 focus:ring-orange-500 focus:border-orange-500"
|
|
placeholder={needsMax ? 'از' : 'مقدار'}
|
|
required
|
|
/>
|
|
<span className="absolute left-2 top-1/2 -translate-y-1/2 text-xs text-gray-400 font-medium">
|
|
{getSensorUnit(rule.sensorType)}
|
|
</span>
|
|
</div>
|
|
{needsMax && (
|
|
<>
|
|
<span className="text-gray-400 text-sm flex-shrink-0">-</span>
|
|
<div className="relative flex-1 min-w-0">
|
|
<input
|
|
type="number"
|
|
step="0.01"
|
|
value={rule.value2 ?? ''}
|
|
onChange={(e) => {
|
|
const val = e.target.value
|
|
// Only update if value is not empty; otherwise keep previous value
|
|
// to avoid Number('') → 0 breaking the input in some browsers
|
|
if (val !== '') {
|
|
onUpdate(index, { value2: Number(val) })
|
|
}
|
|
}}
|
|
className="w-full px-3 py-2 pl-10 bg-white border border-orange-200 rounded-lg text-sm font-medium focus:ring-2 focus:ring-orange-500 focus:border-orange-500"
|
|
placeholder="تا"
|
|
required
|
|
/>
|
|
<span className="absolute left-2 top-1/2 -translate-y-1/2 text-xs text-gray-400 font-medium">
|
|
{getSensorUnit(rule.sensorType)}
|
|
</span>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|