From 586182e69b3dd48f4285abbf06f54e159a54e38f Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Sat, 16 Mar 2024 20:02:03 +0100 Subject: [PATCH 1/3] N-FIN-30: set currency input mode to 'numeric' --- src/components/ui/currency-input.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/ui/currency-input.tsx b/src/components/ui/currency-input.tsx index 5e41c9e..acc15f3 100644 --- a/src/components/ui/currency-input.tsx +++ b/src/components/ui/currency-input.tsx @@ -53,6 +53,7 @@ export default function CurrencyInput(props: TextInputProps) { { setValue(ev.target.value); From 3454c8a03a915e1f8830c574e7ea6a2a375a3632 Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Sat, 16 Mar 2024 20:37:37 +0100 Subject: [PATCH 2/3] N-FIN-30: add new auto-complete component --- src/components/ui/auto-complete-input.tsx | 100 ++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 src/components/ui/auto-complete-input.tsx diff --git a/src/components/ui/auto-complete-input.tsx b/src/components/ui/auto-complete-input.tsx new file mode 100644 index 0000000..29363e6 --- /dev/null +++ b/src/components/ui/auto-complete-input.tsx @@ -0,0 +1,100 @@ +'use client'; + +import * as React from 'react'; +import { useEffect, useState } from 'react'; +import { cn } from '@/lib/utils'; + +export interface AutoCompleteInputProps + extends React.InputHTMLAttributes { + items: { label: string, value: any }[]; + next?: React.RefObject; +} + +const AutoCompleteInput = React.forwardRef( + ({className, type, ...props}, ref) => { + + const [value, setValue] = useState(getInitialValue()); + const [open, setOpen] = useState(false); + const [lastKey, setLastKey] = useState(''); + const [filteredItems, setFilteredItems] = useState(props.items); + + function getInitialValue() { + + if (!props.items) { + return ''; + } + + const item = props.items?.find(item => item.value === props.value); + return item?.label || ''; + } + + function handleChange(e: React.ChangeEvent) { + + props.onChange?.(undefined as any); + const value = e.target.value; + + setFilteredItems(props?.items?.filter((item) => { + return item.label.toLowerCase().includes(value.toLowerCase()); + })); + + setValue(value); + setOpen(value.length > 0); + } + + useEffect(() => { + if (filteredItems.length === 1 && /^[a-zA-Z0-9]$/.test(lastKey)) { + setValue(filteredItems[0].label); + setOpen(false); + props.onChange?.({target: {value: filteredItems[0].value}} as any); + props.next && props.next.current?.focus(); + } + }, [filteredItems]); + + return ( +
+ { + if (e.metaKey || e.ctrlKey || e.altKey) { + props.onKeyDown?.(e); + return; + } + setLastKey(e.key); + props.onKeyDown?.(e); + }} + /> + { + open && ( +
+ {filteredItems?.map((item) => +
{ + props.onChange?.({target: {value: item.value}} as any); + props.next && props.next.current?.focus(); + setValue(item.label); + setOpen(false); + }} + key={item.value}> + {item.label} +
, + )} +
+ ) + } +
+ ); + }, +); +AutoCompleteInput.displayName = 'Input'; + +export { AutoCompleteInput }; From 75c1b82eff7397b3bdd44148de8cf435d5807230 Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Sat, 16 Mar 2024 20:39:10 +0100 Subject: [PATCH 3/3] N-FIN-30: apply new component to payment form --- src/components/form/paymentForm.tsx | 204 ++++------------------------ 1 file changed, 28 insertions(+), 176 deletions(-) diff --git a/src/components/form/paymentForm.tsx b/src/components/form/paymentForm.tsx index d6323dd..705587e 100644 --- a/src/components/form/paymentForm.tsx +++ b/src/components/form/paymentForm.tsx @@ -5,7 +5,7 @@ import { useForm } from 'react-hook-form'; import { z } from 'zod'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; -import React, { useState } from 'react'; +import React, { useRef } from 'react'; import { Button } from '@/components/ui/button'; import { ActionResponse } from '@/lib/types/actionResponse'; import { useRouter } from 'next/navigation'; @@ -16,11 +16,11 @@ import { paymentFormSchema } from '@/lib/form-schemas/paymentFormSchema'; import CurrencyInput from '@/components/ui/currency-input'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { cn } from '@/lib/utils'; -import { CalendarIcon, Check, ChevronsUpDown } from 'lucide-react'; +import { CalendarIcon } from 'lucide-react'; import { format } from 'date-fns'; import { Calendar } from '@/components/ui/calendar'; -import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'; import { Textarea } from '@/components/ui/textarea'; +import { AutoCompleteInput } from '@/components/ui/auto-complete-input'; export default function PaymentForm({value, entities, categories, onSubmit, className}: { value: Payment | undefined, @@ -32,12 +32,6 @@ export default function PaymentForm({value, entities, categories, onSubmit, clas const router = useRouter(); - const [filter, setFilter] = useState(''); - - const [payorOpen, setPayorOpen] = useState(false); - const [payeeOpen, setPayeeOpen] = useState(false); - const [categoryOpen, setCategoryOpen] = useState(false); - const form = useForm>({ resolver: zodResolver(paymentFormSchema), defaultValues: { @@ -73,6 +67,10 @@ export default function PaymentForm({value, entities, categories, onSubmit, clas }; }) ?? []; + const payeeRef = useRef(null); + const categoryRef = useRef(null); + const noteRef = useRef(null); + return (
@@ -145,61 +143,13 @@ export default function PaymentForm({value, entities, categories, onSubmit, clas render={({field}) => ( Payor - { - setPayorOpen(open); - setFilter(''); - }}> - - - - - - - setFilter(e.target.value)} - className="flex h-10 w-full rounded-md border-b border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50" - placeholder="Search..."/> - - {entitiesMapped - .filter((entity) => entity.label.toLowerCase().includes(filter.toLowerCase())) - .map((item) => ( -
{ - field.onChange(item.value); - setPayorOpen(false); - }}> - - {item.label} -
- ))} - -
-
-
+ + +
)} @@ -211,62 +161,13 @@ export default function PaymentForm({value, entities, categories, onSubmit, clas render={({field}) => ( Payee - { - setPayeeOpen(open); - setFilter(''); - }}> - - - - - - - setFilter(e.target.value)} - className="flex h-10 w-full rounded-md border-b border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50" - placeholder="Search..."/> - - {entitiesMapped - .filter((entity) => entity.label.toLowerCase().includes(filter.toLowerCase())) - .map((item) => ( -
{ - field.onChange(item.value); - setPayeeOpen(false); - }} - > - - {item.label} -
- ))} - -
-
-
+ + +
)} @@ -278,62 +179,13 @@ export default function PaymentForm({value, entities, categories, onSubmit, clas render={({field}) => ( Category - { - setCategoryOpen(open); - setFilter(''); - }}> - - - - - - - setFilter(e.target.value)} - className="flex h-10 w-full rounded-md border-b border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50" - placeholder="Search..."/> - - {categoriesMapped - .filter((entity) => entity.label.toLowerCase().includes(filter.toLowerCase())) - .map((item) => ( -
{ - field.onChange(item.value); - setCategoryOpen(false); - }} - > - - {item.label} -
- ))} - -
-
-
+ + +
)}