EMAILOUT / frontend /src /components /workspace /EditableCell.jsx
Seth
update
03b24dc
import React, { useEffect, useState } from 'react';
import { cn } from '@/lib/utils';
const baseInput =
'w-full min-w-0 rounded-md border border-transparent bg-transparent px-1.5 py-0.5 text-sm text-slate-800 ' +
'hover:border-slate-200 focus:border-violet-300 focus:bg-white focus:outline-none focus:ring-1 focus:ring-violet-200';
/**
* Inline text / email / number field: blur or Enter commits if the value changed.
*/
export function EditableCell({
value,
onCommit,
className = '',
inputClassName = '',
type = 'text',
multiline = false,
disabled = false,
}) {
const str = value == null || value === undefined ? '' : String(value);
const [local, setLocal] = useState(str);
useEffect(() => {
setLocal(str);
}, [str]);
const commit = () => {
if (disabled) return;
const next = multiline ? local : local.trim();
const prev = multiline ? str : str.trim();
if (next !== prev) onCommit(next);
};
const stop = (e) => e.stopPropagation();
if (multiline) {
return (
<textarea
className={cn(baseInput, 'resize-y min-h-[2.5rem] max-w-full', className, inputClassName)}
value={local}
onChange={(e) => setLocal(e.target.value)}
onBlur={commit}
onKeyDown={stop}
disabled={disabled}
rows={2}
/>
);
}
return (
<input
type={type}
className={cn(baseInput, className, inputClassName)}
value={local}
onChange={(e) => setLocal(e.target.value)}
onBlur={commit}
onKeyDown={(e) => {
stop(e);
if (e.key === 'Enter') {
e.preventDefault();
e.currentTarget.blur();
}
}}
disabled={disabled}
/>
);
}
function usdWholeString(value) {
if (value == null || value === '') return '';
const n = Math.round(Number(value));
return Number.isFinite(n) ? String(n) : '';
}
function formatUsdWholeDisplay(value) {
const s = usdWholeString(value);
if (s === '') return '';
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
maximumFractionDigits: 0,
minimumFractionDigits: 0,
}).format(Number(s));
}
function parseUsdWholeInput(raw) {
const cleaned = String(raw ?? '')
.replace(/[$,\s]/g, '')
.trim();
if (cleaned === '') return null;
const n = Math.round(Number(cleaned));
return Number.isFinite(n) ? n : NaN;
}
/**
* USD whole dollars: shows $1,234 when blurred; plain digits while focused (no number spinners).
* onCommit receives '' to clear, or a digits string for the integer amount.
*/
export function EditableCurrencyCell({
value,
onCommit,
className = '',
inputClassName = '',
disabled = false,
}) {
const canonical = usdWholeString(value);
const [focused, setFocused] = useState(false);
const [local, setLocal] = useState(() =>
canonical ? formatUsdWholeDisplay(canonical) : ''
);
useEffect(() => {
if (focused) return;
setLocal(canonical ? formatUsdWholeDisplay(canonical) : '');
}, [canonical, focused]);
const commit = () => {
if (disabled) return;
const parsed = parseUsdWholeInput(local);
if (parsed === null) {
if (canonical !== '') onCommit('');
setLocal('');
setFocused(false);
return;
}
if (Number.isNaN(parsed)) {
setLocal(canonical ? formatUsdWholeDisplay(canonical) : '');
setFocused(false);
return;
}
const next = String(parsed);
if (next !== canonical) onCommit(next);
setLocal(formatUsdWholeDisplay(next));
setFocused(false);
};
const handleFocus = () => {
if (disabled) return;
setFocused(true);
setLocal(canonical);
};
const handleBlur = () => {
commit();
};
return (
<input
type="text"
inputMode="decimal"
autoComplete="off"
className={cn(baseInput, className, inputClassName)}
value={local}
onChange={(e) => setLocal(e.target.value)}
onFocus={handleFocus}
onBlur={handleBlur}
onKeyDown={(e) => {
e.stopPropagation();
if (e.key === 'Enter') {
e.preventDefault();
e.currentTarget.blur();
}
}}
disabled={disabled}
/>
);
}
function toDateInputValue(iso) {
if (!iso) return '';
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return '';
return d.toISOString().slice(0, 10);
}
/**
* ISO datetime from API → date input; onCommit receives YYYY-MM-DD or empty string for clear.
*/
export function EditableDateCell({ value, onCommit, className = '', disabled = false }) {
const [local, setLocal] = useState(() => toDateInputValue(value));
useEffect(() => {
setLocal(toDateInputValue(value));
}, [value]);
const commit = () => {
if (disabled) return;
const prev = toDateInputValue(value);
if (local !== prev) onCommit(local);
};
return (
<input
type="date"
className={cn(baseInput, 'max-w-[11rem]', className)}
value={local}
onChange={(e) => setLocal(e.target.value)}
onBlur={commit}
onKeyDown={(e) => e.stopPropagation()}
disabled={disabled}
/>
);
}