Skip to content

Commit d2d7cf1

Browse files
committed
feat: modal for rule adds
1 parent 3b52db5 commit d2d7cf1

File tree

2 files changed

+147
-100
lines changed

2 files changed

+147
-100
lines changed
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { useRules } from "@/hooks/use-rules"
2+
import { zodResolver } from "@hookform/resolvers/zod"
3+
import { useForm } from "react-hook-form"
4+
import type { components } from "schema"
5+
import { z } from "zod"
6+
import { categoryToDisplayName, expenseCategories } from "./expense-modals"
7+
import { Button } from "./ui/button"
8+
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog"
9+
import {
10+
Form,
11+
FormControl,
12+
FormField,
13+
FormItem,
14+
FormLabel,
15+
FormMessage,
16+
} from "./ui/form"
17+
import { Input } from "./ui/input"
18+
import {
19+
Select,
20+
SelectContent,
21+
SelectItem,
22+
SelectTrigger,
23+
SelectValue,
24+
} from "./ui/select"
25+
26+
type ExpenseCategory = components["schemas"]["ExpenseCategory"]
27+
28+
const ruleSchema = z.object({
29+
rule: z.string().min(1, "Rule is required"),
30+
category: z.string().min(1, "Category is required"),
31+
})
32+
33+
type RuleFormValues = z.infer<typeof ruleSchema>
34+
35+
export function AddRuleModal({
36+
isOpen,
37+
setIsOpen,
38+
}: {
39+
isOpen: boolean
40+
setIsOpen: (isOpen: boolean) => void
41+
}) {
42+
const { createRule, isCreatingRule } = useRules({
43+
setIsCreating: setIsOpen,
44+
})
45+
46+
const form = useForm<RuleFormValues>({
47+
resolver: zodResolver(ruleSchema),
48+
defaultValues: {
49+
rule: "",
50+
category: "",
51+
},
52+
})
53+
54+
return (
55+
<Dialog
56+
open={isOpen}
57+
onOpenChange={value => {
58+
setIsOpen(value)
59+
if (!value) {
60+
form.reset()
61+
}
62+
}}
63+
>
64+
<DialogContent className="max-w-md">
65+
<DialogHeader>
66+
<DialogTitle>Create Category Rule</DialogTitle>
67+
</DialogHeader>
68+
69+
<Form {...form}>
70+
<form
71+
onSubmit={form.handleSubmit(async data => {
72+
try {
73+
await createRule(
74+
data.rule.trim(),
75+
data.category as ExpenseCategory
76+
)
77+
setIsOpen(false)
78+
form.reset()
79+
} catch (error) {
80+
console.error("Failed to create rule:", error)
81+
alert("Failed to create rule. Please try again.")
82+
}
83+
})}
84+
className="space-y-6"
85+
>
86+
<FormField
87+
control={form.control}
88+
name="rule"
89+
render={({ field }) => (
90+
<FormItem>
91+
<FormLabel>Rule (Supports regex)</FormLabel>
92+
<FormControl>
93+
<Input
94+
placeholder="e.g., Uber, Lyft, taxi, bus"
95+
{...field}
96+
/>
97+
</FormControl>
98+
<FormMessage />
99+
<p className="text-xs text-muted-foreground">
100+
Enter the expression that should trigger this category
101+
</p>
102+
</FormItem>
103+
)}
104+
/>
105+
106+
<FormField
107+
control={form.control}
108+
name="category"
109+
render={({ field }) => (
110+
<FormItem>
111+
<FormLabel>Category</FormLabel>
112+
<FormControl>
113+
<Select value={field.value} onValueChange={field.onChange}>
114+
<SelectTrigger className="w-full">
115+
<SelectValue placeholder="Select a category" />
116+
</SelectTrigger>
117+
<SelectContent>
118+
{expenseCategories.map((category: ExpenseCategory) => (
119+
<SelectItem key={category} value={category}>
120+
{categoryToDisplayName({ category })}
121+
</SelectItem>
122+
))}
123+
</SelectContent>
124+
</Select>
125+
</FormControl>
126+
<FormMessage />
127+
</FormItem>
128+
)}
129+
/>
130+
131+
<Button type="submit" className="w-full" disabled={isCreatingRule}>
132+
{isCreatingRule ? "Creating..." : "Create Rule"}
133+
</Button>
134+
</form>
135+
</Form>
136+
</DialogContent>
137+
</Dialog>
138+
)
139+
}

frontend/src/components/rules-view.tsx

Lines changed: 8 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -7,43 +7,19 @@ import {
77
CardHeader,
88
CardTitle,
99
} from "@/components/ui/card"
10-
import { Input } from "@/components/ui/input"
11-
import { Label } from "@/components/ui/label"
12-
import {
13-
Select,
14-
SelectContent,
15-
SelectItem,
16-
SelectTrigger,
17-
SelectValue,
18-
} from "@/components/ui/select"
1910
import { useRules } from "@/hooks/use-rules"
2011
import { FileText, Plus, Trash2 } from "lucide-react"
2112
import { motion } from "motion/react"
2213
import { useState } from "react"
2314
import type { components } from "schema"
24-
import {
25-
categoryToDisplayName,
26-
categoryToIcon,
27-
expenseCategories,
28-
} from "./expense-modals"
15+
import { AddRuleModal } from "./add-rule-modal"
16+
import { categoryToDisplayName, categoryToIcon } from "./expense-modals"
2917

30-
type ExpenseCategory = components["schemas"]["ExpenseCategory"]
3118
type ExpenseCategoryRule = components["schemas"]["ExpenseCategoryRule"]
3219

3320
export const RulesView = () => {
34-
const [newRule, setNewRule] = useState("")
35-
const [newCategory, setNewCategory] = useState<ExpenseCategory>()
36-
const [isCreating, setIsCreating] = useState(false)
37-
const {
38-
isLoading,
39-
isCreatingRule,
40-
isDeletingRule,
41-
rules,
42-
createRule,
43-
deleteRule,
44-
} = useRules({
45-
setIsCreating,
46-
})
21+
const [isModalOpen, setIsModalOpen] = useState(false)
22+
const { isLoading, isDeletingRule, rules, deleteRule } = useRules()
4723

4824
if (isLoading) {
4925
return null
@@ -66,7 +42,7 @@ export const RulesView = () => {
6642
<Button
6743
variant="outline"
6844
size="sm"
69-
onClick={() => setIsCreating(!isCreating)}
45+
onClick={() => setIsModalOpen(true)}
7046
className="flex items-center gap-2"
7147
>
7248
<Plus className="h-4 w-4" />
@@ -76,76 +52,6 @@ export const RulesView = () => {
7652
</CardHeader>
7753

7854
<CardContent className="flex-1 overflow-auto space-y-4">
79-
{isCreating && (
80-
<motion.div
81-
initial={{ opacity: 0, y: -20 }}
82-
animate={{ opacity: 1, y: 0 }}
83-
transition={{ duration: 0.2 }}
84-
className="bg-muted/30 border rounded-lg p-4 space-y-4"
85-
>
86-
<div className="space-y-2">
87-
<Label className="text-sm font-medium">
88-
Rule (Supports regex)
89-
</Label>
90-
<Input
91-
placeholder="e.g., Uber, Lyft, taxi, bus"
92-
value={newRule}
93-
onChange={e => setNewRule(e.target.value)}
94-
className="w-full"
95-
/>
96-
<p className="text-xs text-muted-foreground">
97-
Enter the expression that should trigger this category
98-
</p>
99-
</div>
100-
101-
<div className="space-y-2">
102-
<Label className="text-sm font-medium">Category</Label>
103-
<Select
104-
value={newCategory}
105-
onValueChange={(value: ExpenseCategory) =>
106-
setNewCategory(value)
107-
}
108-
>
109-
<SelectTrigger>
110-
<SelectValue placeholder="Select a category" />
111-
</SelectTrigger>
112-
<SelectContent>
113-
{expenseCategories.map((category: ExpenseCategory) => (
114-
<SelectItem key={category} value={category}>
115-
{categoryToDisplayName({ category })}
116-
</SelectItem>
117-
))}
118-
</SelectContent>
119-
</Select>
120-
</div>
121-
122-
<div className="flex gap-2 justify-end">
123-
<Button
124-
variant="outline"
125-
size="sm"
126-
onClick={() => {
127-
setIsCreating(false)
128-
setNewRule("")
129-
setNewCategory(undefined)
130-
}}
131-
>
132-
Cancel
133-
</Button>
134-
<Button
135-
size="sm"
136-
onClick={async () => {
137-
if (newCategory && newRule.trim()) {
138-
createRule(newRule.trim(), newCategory)
139-
}
140-
}}
141-
disabled={!newRule.trim() || !newCategory || isCreatingRule}
142-
>
143-
Create Rule
144-
</Button>
145-
</div>
146-
</motion.div>
147-
)}
148-
14955
{rules.length === 0 ? (
15056
<div className="flex flex-col items-center justify-center py-12 text-center">
15157
<div className="bg-muted mb-4 rounded-full p-4">
@@ -158,7 +64,7 @@ export const RulesView = () => {
15864
Rules help automatically categorize your expenses based on
15965
keywords
16066
</p>
161-
<Button variant="outline" onClick={() => setIsCreating(true)}>
67+
<Button variant="outline" onClick={() => setIsModalOpen(true)}>
16268
Create your first rule
16369
</Button>
16470
</div>
@@ -210,6 +116,8 @@ export const RulesView = () => {
210116
</div>
211117
</CardFooter>
212118
</Card>
119+
120+
<AddRuleModal isOpen={isModalOpen} setIsOpen={setIsModalOpen} />
213121
</>
214122
)
215123
}

0 commit comments

Comments
 (0)