@montytools/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,263 @@
1
+ import { useState } from "react";
2
+ import { createFileRoute } from "@tanstack/react-router";
3
+ import { CheckIcon, PlusIcon, TrashIcon } from "lucide-react";
4
+
5
+ import {
6
+ useInsert,
7
+ useList,
8
+ useMembers,
9
+ useRemove,
10
+ useUpdate,
11
+ } from "@montytools/sdk/react";
12
+ import { Badge } from "@/components/ui/badge";
13
+ import { Button } from "@/components/ui/button";
14
+ import {
15
+ Card,
16
+ CardContent,
17
+ CardHeader,
18
+ CardTitle,
19
+ } from "@/components/ui/card";
20
+ import {
21
+ Empty,
22
+ EmptyDescription,
23
+ EmptyHeader,
24
+ EmptyTitle,
25
+ } from "@/components/ui/empty";
26
+ import {
27
+ Field,
28
+ FieldError,
29
+ FieldGroup,
30
+ FieldLabel,
31
+ } from "@/components/ui/field";
32
+ import { Input } from "@/components/ui/input";
33
+ import {
34
+ Select,
35
+ SelectContent,
36
+ SelectGroup,
37
+ SelectItem,
38
+ SelectTrigger,
39
+ SelectValue,
40
+ } from "@/components/ui/select";
41
+ import { Spinner } from "@/components/ui/spinner";
42
+ import {
43
+ Table,
44
+ TableBody,
45
+ TableCell,
46
+ TableHead,
47
+ TableHeader,
48
+ TableRow,
49
+ } from "@/components/ui/table";
50
+
51
+ import { app } from "../../monty.config";
52
+
53
+ export const Route = createFileRoute("/")({
54
+ component: ExpenseBoard,
55
+ });
56
+
57
+ type Status = "draft" | "submitted" | "approved";
58
+ const NEXT_STATUS: Record<Status, Status | null> = {
59
+ draft: "submitted",
60
+ submitted: "approved",
61
+ approved: null,
62
+ };
63
+ const STATUS_VARIANT = {
64
+ draft: "secondary",
65
+ submitted: "default",
66
+ approved: "outline",
67
+ } as const;
68
+
69
+ function ExpenseBoard() {
70
+ const [statusFilter, setStatusFilter] = useState<Status | "all">("all");
71
+
72
+ // Live, typed, reactive — a teammate's write re-renders this list.
73
+ const { data: expenses, status, loadMore } = useList(app, "expenses", {
74
+ filter: statusFilter === "all" ? undefined : { status: statusFilter },
75
+ order: "desc",
76
+ limit: 50,
77
+ });
78
+ const members = useMembers();
79
+ const insert = useInsert(app, "expenses");
80
+ const update = useUpdate(app, "expenses");
81
+ const remove = useRemove(app, "expenses");
82
+
83
+ const [title, setTitle] = useState("");
84
+ const [amount, setAmount] = useState("");
85
+ // "__none" sentinel: a controlled Radix Select must never flip to undefined
86
+ // (it goes uncontrolled and keeps displaying the stale selection).
87
+ const [assigneeId, setAssigneeId] = useState("__none");
88
+ const [formError, setFormError] = useState<string | null>(null);
89
+
90
+ async function addExpense() {
91
+ setFormError(null);
92
+ try {
93
+ await insert({
94
+ title,
95
+ amount: Number(amount),
96
+ ...(assigneeId !== "__none" ? { assigneeId } : {}),
97
+ });
98
+ setTitle("");
99
+ setAmount("");
100
+ setAssigneeId("__none");
101
+ } catch (error) {
102
+ setFormError(error instanceof Error ? error.message : String(error));
103
+ }
104
+ }
105
+
106
+ const memberName = (userId?: string) =>
107
+ members?.find((m) => m.userId === userId)?.name ?? "—";
108
+
109
+ return (
110
+ <main className="mx-auto flex max-w-4xl flex-col gap-6 p-6">
111
+ <header className="flex items-center justify-between">
112
+ <h1 className="text-2xl font-semibold">Expenses</h1>
113
+ <Select
114
+ value={statusFilter}
115
+ onValueChange={(v) => setStatusFilter(v as Status | "all")}
116
+ >
117
+ <SelectTrigger className="w-40">
118
+ <SelectValue placeholder="Filter status" />
119
+ </SelectTrigger>
120
+ <SelectContent>
121
+ <SelectGroup>
122
+ <SelectItem value="all">All statuses</SelectItem>
123
+ <SelectItem value="draft">Draft</SelectItem>
124
+ <SelectItem value="submitted">Submitted</SelectItem>
125
+ <SelectItem value="approved">Approved</SelectItem>
126
+ </SelectGroup>
127
+ </SelectContent>
128
+ </Select>
129
+ </header>
130
+
131
+ <Card>
132
+ <CardHeader>
133
+ <CardTitle>Add expense</CardTitle>
134
+ </CardHeader>
135
+ <CardContent>
136
+ <FieldGroup>
137
+ <div className="flex flex-wrap items-end gap-4">
138
+ <Field className="min-w-48 flex-1">
139
+ <FieldLabel htmlFor="title">Title</FieldLabel>
140
+ <Input
141
+ id="title"
142
+ value={title}
143
+ onChange={(e) => setTitle(e.target.value)}
144
+ placeholder="Team lunch"
145
+ />
146
+ </Field>
147
+ <Field className="w-32">
148
+ <FieldLabel htmlFor="amount">Amount</FieldLabel>
149
+ <Input
150
+ id="amount"
151
+ type="number"
152
+ value={amount}
153
+ onChange={(e) => setAmount(e.target.value)}
154
+ placeholder="42.50"
155
+ />
156
+ </Field>
157
+ <Field className="w-48">
158
+ <FieldLabel>Assignee</FieldLabel>
159
+ <Select value={assigneeId} onValueChange={setAssigneeId}>
160
+ <SelectTrigger>
161
+ <SelectValue placeholder="Unassigned" />
162
+ </SelectTrigger>
163
+ <SelectContent>
164
+ <SelectGroup>
165
+ <SelectItem value="__none">Unassigned</SelectItem>
166
+ {(members ?? []).map((m) => (
167
+ <SelectItem key={m.userId} value={m.userId}>
168
+ {m.name}
169
+ </SelectItem>
170
+ ))}
171
+ </SelectGroup>
172
+ </SelectContent>
173
+ </Select>
174
+ </Field>
175
+ <Button onClick={addExpense}>
176
+ <PlusIcon data-icon="inline-start" />
177
+ Add
178
+ </Button>
179
+ </div>
180
+ {formError && <FieldError>{formError}</FieldError>}
181
+ </FieldGroup>
182
+ </CardContent>
183
+ </Card>
184
+
185
+ {status === "LoadingFirstPage" && expenses.length === 0 ? (
186
+ <div className="flex justify-center p-12">
187
+ <Spinner />
188
+ </div>
189
+ ) : expenses.length === 0 ? (
190
+ <Empty>
191
+ <EmptyHeader>
192
+ <EmptyTitle>No expenses yet</EmptyTitle>
193
+ <EmptyDescription>
194
+ Add the first one above — it appears for every teammate in real
195
+ time.
196
+ </EmptyDescription>
197
+ </EmptyHeader>
198
+ </Empty>
199
+ ) : (
200
+ <Card>
201
+ <CardContent>
202
+ <Table>
203
+ <TableHeader>
204
+ <TableRow>
205
+ <TableHead>Title</TableHead>
206
+ <TableHead>Amount</TableHead>
207
+ <TableHead>Status</TableHead>
208
+ <TableHead>Assignee</TableHead>
209
+ <TableHead className="w-40" />
210
+ </TableRow>
211
+ </TableHeader>
212
+ <TableBody>
213
+ {expenses.map((e) => (
214
+ <TableRow key={e._id}>
215
+ <TableCell className="font-medium">{e.title}</TableCell>
216
+ <TableCell>${e.amount.toFixed(2)}</TableCell>
217
+ <TableCell>
218
+ <Badge variant={STATUS_VARIANT[e.status]}>
219
+ {e.status}
220
+ </Badge>
221
+ </TableCell>
222
+ <TableCell>{memberName(e.assigneeId)}</TableCell>
223
+ <TableCell>
224
+ <div className="flex justify-end gap-2">
225
+ {NEXT_STATUS[e.status] && (
226
+ <Button
227
+ variant="outline"
228
+ size="sm"
229
+ onClick={() =>
230
+ update(e._id, { status: NEXT_STATUS[e.status]! })
231
+ }
232
+ >
233
+ <CheckIcon data-icon="inline-start" />
234
+ {NEXT_STATUS[e.status]}
235
+ </Button>
236
+ )}
237
+ <Button
238
+ variant="ghost"
239
+ size="sm"
240
+ onClick={() => remove(e._id)}
241
+ >
242
+ <TrashIcon data-icon="inline-start" />
243
+ Delete
244
+ </Button>
245
+ </div>
246
+ </TableCell>
247
+ </TableRow>
248
+ ))}
249
+ </TableBody>
250
+ </Table>
251
+ {status === "CanLoadMore" && (
252
+ <div className="flex justify-center pt-4">
253
+ <Button variant="ghost" onClick={() => loadMore()}>
254
+ Load more
255
+ </Button>
256
+ </div>
257
+ )}
258
+ </CardContent>
259
+ </Card>
260
+ )}
261
+ </main>
262
+ );
263
+ }
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,22 @@
1
+ {
2
+ "include": ["src/**/*", "monty.config.ts", "vite.config.ts"],
3
+ "compilerOptions": {
4
+ "target": "ES2022",
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
8
+ "strict": true,
9
+ "noFallthroughCasesInSwitch": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "isolatedModules": true,
12
+ "esModuleInterop": true,
13
+ "skipLibCheck": true,
14
+ "resolveJsonModule": true,
15
+ "noEmit": true,
16
+ "jsx": "react-jsx",
17
+ "verbatimModuleSyntax": false,
18
+ "paths": {
19
+ "@/*": ["./src/*"]
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,16 @@
1
+ import { monty } from "@montytools/sdk/vite";
2
+ import tailwindcss from "@tailwindcss/vite";
3
+ import { tanstackRouter } from "@tanstack/router-plugin/vite";
4
+ import react from "@vitejs/plugin-react";
5
+ import { defineConfig } from "vite";
6
+
7
+ export default defineConfig({
8
+ resolve: { tsconfigPaths: true },
9
+ plugins: [
10
+ // ORDER MATTERS: tanstackRouter before react
11
+ tanstackRouter({ target: "react", autoCodeSplitting: true }),
12
+ react(),
13
+ tailwindcss(),
14
+ monty(), // mirrors browser errors into the `monty dev` terminal
15
+ ],
16
+ });