Download frontend/src/components/workspace/EditableCell.jsx from Seth0330/qualifier: direct link, hf CLI and curl.
- Browser
- Download file 5.76 kB
-
https://huggingface.co/spaces/Seth0330/qualifier/resolve/main/frontend/src/components/workspace/EditableCell.jsx
- Command line
-
hf download hf://spaces/Seth0330/qualifier/frontend/src/components/workspace/EditableCell.jsx
-
curl -L -o EditableCell.jsx https://huggingface.co/spaces/Seth0330/qualifier/resolve/main/frontend/src/components/workspace/EditableCell.jsx
5.76 kB
| 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} | |
| /> | |
| ); | |
| } | |