97 lines
2.5 KiB
TypeScript
97 lines
2.5 KiB
TypeScript
/* eslint-disable promise/prefer-await-to-then */
|
|
'use client';
|
|
|
|
import { type CustomerInput } from '@repo/graphql/types';
|
|
import { Input } from '@repo/ui/components/ui/input';
|
|
import { Label } from '@repo/ui/components/ui/label';
|
|
import { type ChangeEvent, useEffect, useRef, useState } from 'react';
|
|
import { useDebouncedCallback } from 'use-debounce';
|
|
|
|
type ProfileFieldProps = {
|
|
readonly disabled?: boolean;
|
|
readonly fieldName?: keyof CustomerInput;
|
|
readonly id: string;
|
|
readonly label: string;
|
|
readonly onChange?: (value: CustomerInput) => Promise<void> | void;
|
|
readonly readOnly?: boolean;
|
|
readonly value: string;
|
|
};
|
|
|
|
export function DataField({
|
|
disabled = false,
|
|
fieldName,
|
|
id,
|
|
label,
|
|
onChange,
|
|
readOnly,
|
|
value: initialValue,
|
|
}: ProfileFieldProps) {
|
|
const [value, setValue] = useState(initialValue);
|
|
const { debouncedCallback, isPending } = useDebouncedOnChangeCallback(onChange, fieldName);
|
|
const inputRef = useFocus(isPending);
|
|
|
|
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
const newValue = event.target.value;
|
|
setValue(newValue);
|
|
debouncedCallback(newValue);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-2">
|
|
<Label htmlFor={id}>{label}</Label>
|
|
<Input
|
|
className="bg-secondary outline-none focus:ring-0 focus:ring-offset-0"
|
|
disabled={disabled || isPending}
|
|
id={id}
|
|
onChange={handleChange}
|
|
readOnly={readOnly}
|
|
ref={inputRef}
|
|
value={value}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function useDebouncedOnChangeCallback(
|
|
callback: ((value: CustomerInput) => Promise<void> | void) | undefined,
|
|
fieldName: string | undefined,
|
|
) {
|
|
const [isPending, setIsPending] = useState(false);
|
|
|
|
const debouncedCallback = useDebouncedCallback((newValue: string) => {
|
|
if (!callback || !fieldName) return;
|
|
|
|
setIsPending(true);
|
|
const result = callback({ [fieldName]: newValue });
|
|
|
|
if (result instanceof Promise) {
|
|
result.finally(() => setIsPending(false));
|
|
} else {
|
|
setIsPending(false);
|
|
}
|
|
}, 300);
|
|
|
|
return {
|
|
debouncedCallback,
|
|
isPending,
|
|
};
|
|
}
|
|
|
|
function useFocus(isPending: boolean) {
|
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
const [isInitialRender, setIsInitialRender] = useState(true);
|
|
|
|
useEffect(() => {
|
|
if (isInitialRender) {
|
|
setIsInitialRender(false);
|
|
return;
|
|
}
|
|
|
|
if (inputRef.current && isPending) {
|
|
inputRef.current.focus();
|
|
}
|
|
}, [isInitialRender, isPending]);
|
|
|
|
return inputRef;
|
|
}
|