feat: add change history and fix description editing focus bug

This commit is contained in:
Christian Vidal Wolf
2026-04-09 10:57:39 +02:00
parent cf851e97c4
commit 3f3c7eb3b2
+146 -72
View File
@@ -13,6 +13,96 @@ interface EditPanelProps {
onCaptureState: (message: string) => void;
}
interface DimensionInputProps {
label: string;
field: string;
value: string;
isModified: boolean;
onChange: (val: string) => void;
placeholder?: string;
}
const DimensionInput = ({ label, value, isModified, onChange, placeholder }: DimensionInputProps) => (
<div className="flex flex-col gap-1.5">
<label className="text-[10px] font-medium text-slate-500 uppercase tracking-wider">{label}</label>
<input
type="text"
value={value}
onChange={e => onChange(e.target.value)}
placeholder={placeholder}
className={cn(
"w-full bg-slate-900 border rounded p-2 text-sm text-white focus:outline-none focus:ring-1 transition-colors",
isModified ? "border-blue-500 focus:ring-blue-500" : "border-slate-700 focus:border-slate-500"
)}
/>
</div>
);
interface FieldEditorProps {
title: string;
field: string;
value: string;
isModified: boolean;
canTranslate: boolean;
isGenerated: boolean;
isLoading: boolean;
onGenerate: () => void;
onChange: (val: string) => void;
onKeyDown: (e: React.KeyboardEvent) => void;
}
const FieldEditor = ({
title,
field,
value,
isModified,
canTranslate,
isGenerated,
isLoading,
onGenerate,
onChange,
onKeyDown
}: FieldEditorProps) => (
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<label className="text-sm font-medium text-slate-300">{title}</label>
<div className="flex items-center gap-2">
{canTranslate && (
<div className="flex items-center gap-1 text-[10px] text-slate-500 italic">
<Languages className="w-3 h-3" />
Can translate from existing
</div>
)}
<button
onClick={onGenerate}
disabled={isLoading}
className="flex items-center gap-1.5 text-xs font-medium bg-blue-600/20 text-blue-400 hover:bg-blue-600 hover:text-white px-2 py-1 rounded transition-colors disabled:opacity-50"
>
{isLoading ? <Loader2 className="w-3 h-3 animate-spin" /> : <Sparkles className="w-3 h-3" />}
{canTranslate ? 'Translate with Gemini' : 'Generate with Gemini'}
</button>
</div>
{isGenerated && (
<div className="flex items-center gap-1.5 text-[10px] text-emerald-400 bg-emerald-400/10 px-2 py-0.5 rounded w-fit animate-in fade-in slide-in-from-top-1 duration-300">
<CheckCircle2 className="w-3 h-3" />
AI Generated - You can still edit manually
</div>
)}
</div>
<textarea
id={`field-${field}`}
value={value}
onChange={e => onChange(e.target.value)}
onKeyDown={onKeyDown}
className={cn(
"w-full h-32 bg-slate-900 border rounded-md p-3 text-sm text-white focus:outline-none focus:ring-1 transition-colors resize-none",
isModified ? "border-blue-500 focus:ring-blue-500" : "border-slate-700 focus:border-slate-500 focus:ring-slate-500"
)}
placeholder={`Enter ${title}...`}
/>
</div>
);
export function EditPanel({ row, rowIndex, onSave, onClose, onCaptureState }: EditPanelProps) {
const [formData, setFormData] = useState({
longDe: row[COLUMNS.LONG_DE] || '',
@@ -163,70 +253,10 @@ export function EditPanel({ row, rowIndex, onSave, onClose, onCaptureState }: Ed
onSave(rowIndex, newRow);
};
const DimensionInput = ({ label, field, placeholder }: { label: string, field: keyof typeof formData, placeholder?: string }) => (
<div className="flex flex-col gap-1.5">
<label className="text-[10px] font-medium text-slate-500 uppercase tracking-wider">{label}</label>
<input
type="text"
value={formData[field]}
onChange={e => setFormData(prev => ({ ...prev, [field]: e.target.value }))}
placeholder={placeholder}
className={cn(
"w-full bg-slate-900 border rounded p-2 text-sm text-white focus:outline-none focus:ring-1 transition-colors",
isModified(field) ? "border-blue-500 focus:ring-blue-500" : "border-slate-700 focus:border-slate-500"
)}
/>
</div>
);
const handleInputKeyDown = (e: React.KeyboardEvent) => {
e.stopPropagation();
};
const FieldEditor = ({ title, field }: { title: string, field: keyof typeof formData }) => (
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<label className="text-sm font-medium text-slate-300">{title}</label>
<div className="flex items-center gap-2">
{((field === 'longDe' && formData.longEn) ||
(field === 'longEn' && formData.longDe) ||
(field === 'shortDe' && (formData.longDe || formData.shortEn)) ||
(field === 'shortEn' && (formData.shortDe || formData.longEn))) && (
<div className="flex items-center gap-1 text-[10px] text-slate-500 italic">
<Languages className="w-3 h-3" />
Can translate from existing
</div>
)}
<button
onClick={() => handleGenerate(field)}
disabled={loadingField !== null}
className="flex items-center gap-1.5 text-xs font-medium bg-blue-600/20 text-blue-400 hover:bg-blue-600 hover:text-white px-2 py-1 rounded transition-colors disabled:opacity-50"
>
{loadingField === field ? <Loader2 className="w-3 h-3 animate-spin" /> : <Sparkles className="w-3 h-3" />}
{((field === 'longDe' && formData.longEn) || (field === 'longEn' && formData.longDe) || (field === 'shortDe' && formData.shortEn) || (field === 'shortEn' && formData.shortDe)) ? 'Translate with Gemini' : 'Generate with Gemini'}
</button>
</div>
{generatedFields.has(field) && (
<div className="flex items-center gap-1.5 text-[10px] text-emerald-400 bg-emerald-400/10 px-2 py-0.5 rounded w-fit animate-in fade-in slide-in-from-top-1 duration-300">
<CheckCircle2 className="w-3 h-3" />
AI Generated - You can still edit manually
</div>
)}
</div>
<textarea
id={`field-${String(field)}`}
value={formData[field]}
onChange={e => setFormData(prev => ({ ...prev, [field]: e.target.value }))}
onKeyDown={handleInputKeyDown}
className={cn(
"w-full h-32 bg-slate-900 border rounded-md p-3 text-sm text-white focus:outline-none focus:ring-1 transition-colors resize-none",
isModified(field) ? "border-blue-500 focus:ring-blue-500" : "border-slate-700 focus:border-slate-500 focus:ring-slate-500"
)}
placeholder={`Enter ${title}...`}
/>
</div>
);
return (
<>
<div className="fixed inset-0 bg-slate-950/50 backdrop-blur-sm z-40" onClick={onClose} />
@@ -290,28 +320,72 @@ export function EditPanel({ row, rowIndex, onSave, onClose, onCaptureState }: Ed
<div className="grid grid-cols-3 gap-3">
<div className="col-span-3 text-[10px] text-slate-500 font-medium">INNER BOX (L × W × H) cm</div>
<DimensionInput label="Length" field="innerL" placeholder="L" />
<DimensionInput label="Width" field="innerW" placeholder="W" />
<DimensionInput label="Height" field="innerH" placeholder="H" />
<DimensionInput label="Length" value={formData.innerL} field="innerL" isModified={isModified('innerL')} onChange={(val) => setFormData(p => ({...p, innerL: val}))} placeholder="L" />
<DimensionInput label="Width" value={formData.innerW} field="innerW" isModified={isModified('innerW')} onChange={(val) => setFormData(p => ({...p, innerW: val}))} placeholder="W" />
<DimensionInput label="Height" value={formData.innerH} field="innerH" isModified={isModified('innerH')} onChange={(val) => setFormData(p => ({...p, innerH: val}))} placeholder="H" />
</div>
<div className="grid grid-cols-3 gap-3">
<div className="col-span-3 text-[10px] text-slate-500 font-medium">OUTER BOX (L × W × H) cm</div>
<DimensionInput label="Length" field="outerL" placeholder="L" />
<DimensionInput label="Width" field="outerW" placeholder="W" />
<DimensionInput label="Height" field="outerH" placeholder="H" />
<DimensionInput label="Length" value={formData.outerL} field="outerL" isModified={isModified('outerL')} onChange={(val) => setFormData(p => ({...p, outerL: val}))} placeholder="L" />
<DimensionInput label="Width" value={formData.outerW} field="outerW" isModified={isModified('outerW')} onChange={(val) => setFormData(p => ({...p, outerW: val}))} placeholder="W" />
<DimensionInput label="Height" value={formData.outerH} field="outerH" isModified={isModified('outerH')} onChange={(val) => setFormData(p => ({...p, outerH: val}))} placeholder="H" />
</div>
<div className="grid grid-cols-2 gap-3">
<DimensionInput label="Units per Outer" field="unitsOuter" />
<DimensionInput label="MOQ" field="moq" />
<DimensionInput label="Units per Outer" value={formData.unitsOuter} field="unitsOuter" isModified={isModified('unitsOuter')} onChange={(val) => setFormData(p => ({...p, unitsOuter: val}))} />
<DimensionInput label="MOQ" value={formData.moq} field="moq" isModified={isModified('moq')} onChange={(val) => setFormData(p => ({...p, moq: val}))} />
</div>
</div>
<FieldEditor title="Long Description (DE)" field="longDe" />
<FieldEditor title="Long Description (EN)" field="longEn" />
<FieldEditor title="Short Description (DE)" field="shortDe" />
<FieldEditor title="Short Description (EN)" field="shortEn" />
<FieldEditor
title="Long Description (DE)"
field="longDe"
value={formData.longDe}
isModified={isModified('longDe')}
canTranslate={!!formData.longEn}
isGenerated={generatedFields.has('longDe')}
isLoading={loadingField === 'longDe'}
onGenerate={() => handleGenerate('longDe')}
onChange={(val) => setFormData(p => ({...p, longDe: val}))}
onKeyDown={handleInputKeyDown}
/>
<FieldEditor
title="Long Description (EN)"
field="longEn"
value={formData.longEn}
isModified={isModified('longEn')}
canTranslate={!!formData.longDe}
isGenerated={generatedFields.has('longEn')}
isLoading={loadingField === 'longEn'}
onGenerate={() => handleGenerate('longEn')}
onChange={(val) => setFormData(p => ({...p, longEn: val}))}
onKeyDown={handleInputKeyDown}
/>
<FieldEditor
title="Short Description (DE)"
field="shortDe"
value={formData.shortDe}
isModified={isModified('shortDe')}
canTranslate={!!formData.shortEn || !!formData.longDe}
isGenerated={generatedFields.has('shortDe')}
isLoading={loadingField === 'shortDe'}
onGenerate={() => handleGenerate('shortDe')}
onChange={(val) => setFormData(p => ({...p, shortDe: val}))}
onKeyDown={handleInputKeyDown}
/>
<FieldEditor
title="Short Description (EN)"
field="shortEn"
value={formData.shortEn}
isModified={isModified('shortEn')}
canTranslate={!!formData.shortDe || !!formData.longEn}
isGenerated={generatedFields.has('shortEn')}
isLoading={loadingField === 'shortEn'}
onGenerate={() => handleGenerate('shortEn')}
onChange={(val) => setFormData(p => ({...p, shortEn: val}))}
onKeyDown={handleInputKeyDown}
/>
</div>
<div className="p-6 border-t border-slate-700 bg-slate-800/50 flex justify-end gap-3">