Skip to content

Commit 1daea5b

Browse files
committed
✨ [feat][frontend] Create account fab
1 parent 2ff4ada commit 1daea5b

3 files changed

Lines changed: 210 additions & 1 deletion

File tree

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
2+
import { ChevronDown, Globe } from 'lucide-react'
3+
import { useState } from 'react'
4+
import { toast } from 'sonner'
5+
6+
import { FabSheet } from '@/components/fab-sheet'
7+
import { Button } from '@/components/ui/button'
8+
import {
9+
Combobox,
10+
ComboboxContent,
11+
ComboboxEmpty,
12+
ComboboxInput,
13+
ComboboxItem,
14+
ComboboxList,
15+
useComboboxAnchor
16+
} from '@/components/ui/combobox'
17+
import {
18+
DropdownMenu,
19+
DropdownMenuContent,
20+
DropdownMenuItem,
21+
DropdownMenuTrigger
22+
} from '@/components/ui/dropdown-menu'
23+
import { Field, FieldGroup, FieldLabel } from '@/components/ui/field'
24+
import { Input } from '@/components/ui/input'
25+
import { InputGroupAddon } from '@/components/ui/input-group'
26+
import { SheetFooter } from '@/components/ui/sheet'
27+
import {
28+
type AccountCreate,
29+
type AccountRead,
30+
createAccount,
31+
readCurrencies
32+
} from '@/lib/client'
33+
import type { Client } from '@/lib/client/client'
34+
35+
const TIMEZONES = Intl.supportedValuesOf('timeZone')
36+
37+
interface CreateAccountFabProps {
38+
client: Client
39+
}
40+
41+
export function CreateAccountFab({ client }: CreateAccountFabProps) {
42+
const queryClient = useQueryClient()
43+
const [open, setOpen] = useState(false)
44+
const [name, setName] = useState('')
45+
const [currencyCode, setCurrencyCode] = useState<string | null>(null)
46+
const [timezone, setTimezone] = useState(
47+
() => Intl.DateTimeFormat().resolvedOptions().timeZone
48+
)
49+
const timezoneAnchor = useComboboxAnchor()
50+
51+
const { data: currencies } = useQuery({
52+
queryKey: ['currencies'],
53+
queryFn: async () => {
54+
const response = await readCurrencies({ client })
55+
if (response.error) throw new Error('Failed to fetch currencies')
56+
if (!response.data) throw new Error('No data returned')
57+
return response.data
58+
},
59+
enabled: open
60+
})
61+
62+
const selectedCurrency = currencies?.find((c) => c.code === currencyCode)
63+
64+
const reset = () => {
65+
setName('')
66+
setCurrencyCode(null)
67+
setTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone)
68+
}
69+
70+
const { mutate, isPending } = useMutation({
71+
mutationFn: async (body: AccountCreate) => {
72+
const response = await createAccount({ client, body })
73+
if (response.error) throw new Error('Failed to create account')
74+
if (!response.data) throw new Error('No data returned')
75+
return response.data as AccountRead
76+
},
77+
onSuccess: () => {
78+
toast.success('Account created')
79+
queryClient.invalidateQueries({ queryKey: ['accounts'] })
80+
reset()
81+
setOpen(false)
82+
},
83+
onError: (error) => {
84+
console.error(error)
85+
toast.error('Failed to create account', {
86+
description:
87+
error instanceof Error ? error.message : 'An unknown error occurred'
88+
})
89+
}
90+
})
91+
92+
const handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
93+
event.preventDefault()
94+
if (currencyCode == null) return
95+
mutate({
96+
name,
97+
currency_code: currencyCode,
98+
timezone: timezone as AccountCreate['timezone']
99+
})
100+
}
101+
102+
return (
103+
<FabSheet open={open} onOpenChange={setOpen} hotkey="n" label="New account">
104+
<form
105+
onSubmit={handleSubmit}
106+
className="flex min-h-0 flex-1 flex-col gap-4"
107+
>
108+
<FieldGroup className="flex-1 overflow-y-auto px-4">
109+
<Field>
110+
<FieldLabel htmlFor="account-name">Name</FieldLabel>
111+
<Input
112+
id="account-name"
113+
value={name}
114+
onChange={(e) => setName(e.target.value)}
115+
placeholder="Account name"
116+
required
117+
/>
118+
</Field>
119+
120+
<Field>
121+
<FieldLabel htmlFor="account-currency">Currency</FieldLabel>
122+
<DropdownMenu>
123+
<DropdownMenuTrigger asChild>
124+
<Button
125+
id="account-currency"
126+
type="button"
127+
variant="outline"
128+
className="w-full justify-between"
129+
>
130+
{selectedCurrency ? (
131+
<span className="flex items-center gap-2">
132+
<span className="font-medium">
133+
{selectedCurrency.code}
134+
</span>
135+
<span className="text-muted-foreground text-xs">
136+
{selectedCurrency.name}
137+
</span>
138+
</span>
139+
) : (
140+
<span className="text-muted-foreground">
141+
Select currency
142+
</span>
143+
)}
144+
<ChevronDown className="size-4" />
145+
</Button>
146+
</DropdownMenuTrigger>
147+
<DropdownMenuContent
148+
align="start"
149+
className="max-h-72 w-(--radix-dropdown-menu-trigger-width) overflow-y-auto"
150+
>
151+
{currencies?.map((currency) => (
152+
<DropdownMenuItem
153+
key={currency.code}
154+
onSelect={() => setCurrencyCode(currency.code)}
155+
>
156+
<span className="leading-none">{currency.code}</span>
157+
<span className="text-muted-foreground ml-auto text-xs leading-none">
158+
{currency.name}
159+
</span>
160+
</DropdownMenuItem>
161+
))}
162+
</DropdownMenuContent>
163+
</DropdownMenu>
164+
</Field>
165+
166+
<Field>
167+
<FieldLabel htmlFor="account-timezone">Timezone</FieldLabel>
168+
<Combobox
169+
id="account-timezone"
170+
items={TIMEZONES}
171+
value={timezone}
172+
onValueChange={(value) => setTimezone(value ?? '')}
173+
>
174+
<div ref={timezoneAnchor}>
175+
<ComboboxInput placeholder="Select timezone">
176+
<InputGroupAddon>
177+
<Globe className="size-4" />
178+
</InputGroupAddon>
179+
</ComboboxInput>
180+
</div>
181+
<ComboboxContent anchor={timezoneAnchor} className="min-w-0">
182+
<ComboboxEmpty>No timezone found.</ComboboxEmpty>
183+
<ComboboxList>
184+
{(tz: string) => (
185+
<ComboboxItem key={tz} value={tz}>
186+
{tz}
187+
</ComboboxItem>
188+
)}
189+
</ComboboxList>
190+
</ComboboxContent>
191+
</Combobox>
192+
</Field>
193+
</FieldGroup>
194+
195+
<SheetFooter>
196+
<Button
197+
type="submit"
198+
disabled={isPending || !name || currencyCode == null}
199+
>
200+
{isPending ? 'Creating...' : 'Create'}
201+
</Button>
202+
</SheetFooter>
203+
</form>
204+
</FabSheet>
205+
)
206+
}

frontend/src/components/fab-sheet.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export function FabSheet({
3333
useHotkeys(hotkey, () => onOpenChange(true), { preventDefault: true })
3434

3535
return (
36-
<Sheet open={open} onOpenChange={onOpenChange}>
36+
<Sheet open={open} onOpenChange={onOpenChange} modal={false}>
3737
<Tooltip>
3838
<TooltipTrigger asChild>
3939
<SheetTrigger asChild>

frontend/src/routes/_auth/account/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Wallet } from 'lucide-react'
44
import { useEffect } from 'react'
55
import { toast } from 'sonner'
66

7+
import { CreateAccountFab } from '@/components/create-account-fab'
78
import { Separator } from '@/components/ui/separator'
89
import { readAccounts } from '@/lib/client'
910
import { cn, formatCurrency } from '@/lib/utils'
@@ -74,6 +75,8 @@ function AccountListPage() {
7475
{accounts?.length === 0 && (
7576
<p className="text-muted-foreground p-4 text-sm">No accounts found.</p>
7677
)}
78+
79+
<CreateAccountFab client={client} />
7780
</div>
7881
)
7982
}

0 commit comments

Comments
 (0)