mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 15:55:24 +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;
|
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) {
|
export function EditPanel({ row, rowIndex, onSave, onClose, onCaptureState }: EditPanelProps) {
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
longDe: row[COLUMNS.LONG_DE] || '',
|
longDe: row[COLUMNS.LONG_DE] || '',
|
||||||
@@ -163,70 +253,10 @@ export function EditPanel({ row, rowIndex, onSave, onClose, onCaptureState }: Ed
|
|||||||
onSave(rowIndex, newRow);
|
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) => {
|
const handleInputKeyDown = (e: React.KeyboardEvent) => {
|
||||||
e.stopPropagation();
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="fixed inset-0 bg-slate-950/50 backdrop-blur-sm z-40" onClick={onClose} />
|
<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="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>
|
<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="Length" value={formData.innerL} field="innerL" isModified={isModified('innerL')} onChange={(val) => setFormData(p => ({...p, innerL: val}))} placeholder="L" />
|
||||||
<DimensionInput label="Width" field="innerW" placeholder="W" />
|
<DimensionInput label="Width" value={formData.innerW} field="innerW" isModified={isModified('innerW')} onChange={(val) => setFormData(p => ({...p, innerW: val}))} placeholder="W" />
|
||||||
<DimensionInput label="Height" field="innerH" placeholder="H" />
|
<DimensionInput label="Height" value={formData.innerH} field="innerH" isModified={isModified('innerH')} onChange={(val) => setFormData(p => ({...p, innerH: val}))} placeholder="H" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-3 gap-3">
|
<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>
|
<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="Length" value={formData.outerL} field="outerL" isModified={isModified('outerL')} onChange={(val) => setFormData(p => ({...p, outerL: val}))} placeholder="L" />
|
||||||
<DimensionInput label="Width" field="outerW" placeholder="W" />
|
<DimensionInput label="Width" value={formData.outerW} field="outerW" isModified={isModified('outerW')} onChange={(val) => setFormData(p => ({...p, outerW: val}))} placeholder="W" />
|
||||||
<DimensionInput label="Height" field="outerH" placeholder="H" />
|
<DimensionInput label="Height" value={formData.outerH} field="outerH" isModified={isModified('outerH')} onChange={(val) => setFormData(p => ({...p, outerH: val}))} placeholder="H" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<DimensionInput label="Units per Outer" field="unitsOuter" />
|
<DimensionInput label="Units per Outer" value={formData.unitsOuter} field="unitsOuter" isModified={isModified('unitsOuter')} onChange={(val) => setFormData(p => ({...p, unitsOuter: val}))} />
|
||||||
<DimensionInput label="MOQ" field="moq" />
|
<DimensionInput label="MOQ" value={formData.moq} field="moq" isModified={isModified('moq')} onChange={(val) => setFormData(p => ({...p, moq: val}))} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FieldEditor title="Long Description (DE)" field="longDe" />
|
<FieldEditor
|
||||||
<FieldEditor title="Long Description (EN)" field="longEn" />
|
title="Long Description (DE)"
|
||||||
<FieldEditor title="Short Description (DE)" field="shortDe" />
|
field="longDe"
|
||||||
<FieldEditor title="Short Description (EN)" field="shortEn" />
|
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>
|
||||||
|
|
||||||
<div className="p-6 border-t border-slate-700 bg-slate-800/50 flex justify-end gap-3">
|
<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