@@ -7,15 +7,23 @@ import {
77 setValue ,
88 SubmitEvent
99} from "@modular-forms/solid"
10- import { repeat } from "lodash"
10+ import { countBy , repeat , uniq } from "lodash"
1111import {
1212 IconArrowsSplit2 ,
1313 IconCalendarEvent ,
1414 IconPlus ,
1515 IconSelector ,
1616 IconSwitch3
1717} from "@tabler/icons-solidjs"
18- import { Component , createEffect , createSignal , createUniqueId , For , Show } from "solid-js"
18+ import {
19+ Component ,
20+ createEffect ,
21+ createMemo ,
22+ createSignal ,
23+ createUniqueId ,
24+ For ,
25+ Show
26+ } from "solid-js"
1927import toast from "solid-toast"
2028import { TransactionInput , FullTransactionFragment } from "../../graphql-types"
2129import { useCreateTransaction } from "../../graphql/mutations/createTransactionMutation"
@@ -36,6 +44,7 @@ import FormInput from "../forms/FormInput"
3644import FormInputGroup from "../forms/FormInputGroup"
3745import { SplitTransactionModal } from "./SplitTransactionModal"
3846import { toCents } from "./AmountEditor"
47+ import { Portal } from "solid-js/web"
3948
4049type NewTransactionModalValues = Omit < TransactionInput , "amount" | "shopAmount" > & {
4150 amountType : "expense" | "income"
@@ -73,6 +82,37 @@ export const NewTransactionModal: Component<{
7382
7483 let shopInput : HTMLInputElement | undefined
7584
85+ const [ shopFocused , setShopFocused ] = createSignal ( false )
86+
87+ const normalizeShop = ( string : string ) => string . toLowerCase ( ) . replace ( / [ ^ \w ] + / , "" )
88+
89+ const populateFromShop = ( shopString : string ) => {
90+ const recent =
91+ transactions ( ) ?. transactions . nodes . filter (
92+ ( transaction ) => normalizeShop ( transaction . shop ) === normalizeShop ( shopString )
93+ ) || [ ]
94+ const copyFrom = recent . at ( - 1 )
95+
96+ if ( copyFrom ) {
97+ setValue ( form , "categoryId" , copyFrom . category ?. id )
98+ setValue ( form , "accountId" , copyFrom . account . id )
99+ setValue ( form , "currencyId" , copyFrom . account . currency . id )
100+
101+ if ( ! getValue ( form , "memo" ) && recent . every ( ( { memo } ) => memo === copyFrom . memo ) ) {
102+ setValue ( form , "memo" , copyFrom . memo )
103+ }
104+ }
105+ }
106+
107+ const topShops = createMemo ( ( ) => {
108+ const nodes = transactions ( ) ?. transactions . nodes || [ ]
109+ const counts = countBy ( nodes , "shop" )
110+ return Array . from ( Object . entries ( counts ) )
111+ . sort ( ( a , b ) => b [ 1 ] - a [ 1 ] )
112+ . slice ( 0 , 20 )
113+ . map ( ( [ name ] ) => name )
114+ } )
115+
76116 createEffect ( ( ) => {
77117 if ( isDateSelected ( ) ) {
78118 shopInput ?. focus ( )
@@ -148,7 +188,7 @@ export const NewTransactionModal: Component<{
148188 < >
149189 < Show when = { ! splittingTransaction ( ) } >
150190 < Modal isOpen = { props . isOpen } >
151- < ModalContent class = "flex h-124 flex-col" >
191+ < ModalContent class = "h-124 relative flex flex-col" >
152192 < ModalTitle >
153193 New Transaction
154194 < ModalCloseButton onClick = { props . onClose } />
@@ -173,33 +213,21 @@ export const NewTransactionModal: Component<{
173213 label = "Where?"
174214 name = "shop"
175215 list = { recentShopsId }
216+ onFocus = { ( ) => setShopFocused ( true ) }
176217 onBlur = { ( e ) => {
177- const normalize = ( string : string ) =>
178- string . toLowerCase ( ) . replace ( / [ ^ \w ] + / , "" )
179-
180- const recent =
181- transactions ( ) ?. transactions . nodes . filter (
182- ( transaction ) => normalize ( transaction . shop ) === normalize ( e . target . value )
183- ) || [ ]
184- const copyFrom = recent [ 0 ]
185-
186- if ( copyFrom ) {
187- setValue ( form , "categoryId" , copyFrom . category ?. id )
188- setValue ( form , "accountId" , copyFrom . account . id )
189- setValue ( form , "currencyId" , copyFrom . account . currency . id )
190-
191- if (
192- ! getValue ( form , "memo" ) &&
193- recent . every ( ( { memo } ) => memo === copyFrom . memo )
194- ) {
195- setValue ( form , "memo" , copyFrom . memo )
196- }
197- }
218+ populateFromShop ( e . target . value )
219+ setTimeout ( ( ) => setShopFocused ( false ) , 120 )
198220 } }
199221 />
200222 < datalist id = { recentShopsId } >
201- < For each = { transactions ( ) ?. transactions . nodes } >
202- { ( transaction ) => < option value = { transaction . shop } /> }
223+ < For
224+ each = { uniq (
225+ transactions ( ) ?. transactions . nodes ?. map (
226+ ( transaction ) => transaction . shop
227+ ) ?? [ ]
228+ ) }
229+ >
230+ { ( shop ) => < option value = { shop } /> }
203231 </ For >
204232 </ datalist >
205233
@@ -397,6 +425,33 @@ export const NewTransactionModal: Component<{
397425 </ Form >
398426 </ ModalContent >
399427 </ Modal >
428+ < Show when = { shopFocused ( ) } >
429+ < Portal >
430+ < div class = "z-modal fixed inset-x-0 bottom-0 hidden bg-white/60 shadow-sm sm:block lg:bottom-2 lg:left-1/2 lg:max-w-lg lg:-translate-x-1/2 lg:rounded-sm" >
431+ < div class = "mx-auto max-w-lg p-2" >
432+ < div class = "flex flex-wrap gap-2" >
433+ < For each = { topShops ( ) } >
434+ { ( shop ) => (
435+ < Button
436+ size = "custom"
437+ variant = "ghost"
438+ class = "bg-white px-3 py-2 text-xs"
439+ onClick = { ( ) => {
440+ setValue ( form , "shop" , shop )
441+ populateFromShop ( shop )
442+ setShopFocused ( false )
443+ shopInput ?. blur ( )
444+ } }
445+ >
446+ { shop }
447+ </ Button >
448+ ) }
449+ </ For >
450+ </ div >
451+ </ div >
452+ </ div >
453+ </ Portal >
454+ </ Show >
400455 </ Show >
401456 < Show when = { splittingTransaction ( ) } >
402457 { ( splittingTransaction ) => (
0 commit comments