Skip to content

Commit 4a46545

Browse files
committed
Add shortcuts for top shops
1 parent 8208fb3 commit 4a46545

File tree

2 files changed

+83
-28
lines changed

2 files changed

+83
-28
lines changed

web/src/components/base/Modal.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ export const Modal = <T extends AsProp = "div">(
2222
<Dynamic
2323
component={props.as || "div"}
2424
ref={props.ref}
25-
class={`z-modal relative transition-opacity ${
25+
class={`relative transition-opacity ${
2626
props.isOpen ? "visible opacity-100" : "invisible opacity-0"
2727
} ${props.class}`}
2828
classList={props.classList}
2929
{...elementProps}
3030
>
31-
<div class="fixed inset-0 bg-gray-500/75" />
31+
<div class="z-modal fixed inset-0 bg-gray-500/75" />
3232

3333
<div class="z-modal fixed inset-0 overflow-y-auto" role="dialog">
3434
<div

web/src/components/transactions/NewTransactionModal.tsx

Lines changed: 81 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -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"
1111
import {
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"
1927
import toast from "solid-toast"
2028
import { TransactionInput, FullTransactionFragment } from "../../graphql-types"
2129
import { useCreateTransaction } from "../../graphql/mutations/createTransactionMutation"
@@ -36,6 +44,7 @@ import FormInput from "../forms/FormInput"
3644
import FormInputGroup from "../forms/FormInputGroup"
3745
import { SplitTransactionModal } from "./SplitTransactionModal"
3846
import { toCents } from "./AmountEditor"
47+
import { Portal } from "solid-js/web"
3948

4049
type 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

Comments
 (0)