mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 15:05:23 +02:00
feat: add change history and fix description editing focus bug
This commit is contained in:
+146
-72
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user