feat: budget crud actions
This commit is contained in:
@@ -13,7 +13,7 @@ export default function Page() {
|
||||
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
const newRoute = window.location.pathname.slice(1);
|
||||
const newRoute = window.location.pathname.slice(1) + "/";
|
||||
setRoute(newRoute);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { createCliRenderer } from "@opentui/core";
|
||||
import { createRoot, useKeyboard } from "@opentui/react";
|
||||
import { createRoot, useKeyboard, useRenderer } from "@opentui/react";
|
||||
import { App, type Route } from "@money/ui";
|
||||
import { ZeroProvider } from "@rocicorp/zero/react";
|
||||
import { schema } from "@money/shared";
|
||||
import { schema, createMutators } from "@money/shared";
|
||||
import { useState } from "react";
|
||||
import { AuthClientLayer, getAuth } from "./auth";
|
||||
import { Effect } from "effect";
|
||||
@@ -13,24 +13,14 @@ import { config } from "./config";
|
||||
|
||||
function Main({ auth }: { auth: AuthData }) {
|
||||
const [route, setRoute] = useState<Route>("/");
|
||||
const renderer = useRenderer();
|
||||
|
||||
useKeyboard((key) => {
|
||||
if (key.name == "c" && key.ctrl) process.exit(0);
|
||||
if (key.name == "i" && key.meta) renderer.console.toggle();
|
||||
});
|
||||
|
||||
return (
|
||||
<ZeroProvider
|
||||
{...{
|
||||
userID: auth.user.id,
|
||||
auth: auth.session.token,
|
||||
server: config.zeroUrl,
|
||||
schema,
|
||||
kvStore,
|
||||
}}
|
||||
>
|
||||
<App auth={auth} route={route} setRoute={setRoute} />
|
||||
</ZeroProvider>
|
||||
);
|
||||
return <App auth={auth} route={route} setRoute={setRoute} />;
|
||||
}
|
||||
|
||||
const auth = await Effect.runPromise(
|
||||
@@ -40,4 +30,17 @@ const auth = await Effect.runPromise(
|
||||
),
|
||||
);
|
||||
const renderer = await createCliRenderer({ exitOnCtrlC: false });
|
||||
createRoot(renderer).render(<Main auth={auth} />);
|
||||
createRoot(renderer).render(
|
||||
<ZeroProvider
|
||||
{...{
|
||||
userID: auth.user.id,
|
||||
auth: auth.session.token,
|
||||
server: config.zeroUrl,
|
||||
schema,
|
||||
mutators: createMutators(auth),
|
||||
kvStore,
|
||||
}}
|
||||
>
|
||||
<Main auth={auth} />
|
||||
</ZeroProvider>,
|
||||
);
|
||||
|
||||
12
package.json
12
package.json
@@ -3,16 +3,10 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "process-compose up -p 0",
|
||||
"tui": "bun run --hot apps/tui/src/index.tsx"
|
||||
"tui": "pnpm --filter=@money/tui run build && pnpm --filter=@money/tui run start"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@rocicorp/zero-sqlite3"
|
||||
],
|
||||
"ignoredBuiltDependencies": [
|
||||
"esbuild",
|
||||
"protobufjs",
|
||||
"unrs-resolver"
|
||||
]
|
||||
"onlyBuiltDependencies": ["@rocicorp/zero-sqlite3"],
|
||||
"ignoredBuiltDependencies": ["esbuild", "protobufjs", "unrs-resolver"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
StyleProp,
|
||||
ViewStyle,
|
||||
LinkingImpl,
|
||||
TextInputProps,
|
||||
} from "react-native";
|
||||
import { useTerminalDimensions } from "@opentui/react";
|
||||
import { RGBA } from "@opentui/core";
|
||||
@@ -226,6 +227,32 @@ export function Modal({ children, visible }: ModalProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export function TextInput({
|
||||
defaultValue,
|
||||
onChangeText,
|
||||
onKeyPress,
|
||||
}: TextInputProps) {
|
||||
return (
|
||||
<input
|
||||
width={20}
|
||||
backgroundColor="white"
|
||||
textColor="black"
|
||||
focused={true}
|
||||
cursorColor={"black"}
|
||||
onInput={onChangeText}
|
||||
onKeyDown={(key) =>
|
||||
// @ts-ignore
|
||||
onKeyPress({
|
||||
nativeEvent: {
|
||||
key: key.name == "return" ? "Enter" : key.name,
|
||||
},
|
||||
})
|
||||
}
|
||||
placeholder={defaultValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const Platform = {
|
||||
OS: "tui",
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import {
|
||||
boolean,
|
||||
decimal,
|
||||
@@ -87,4 +88,17 @@ export const category = pgTable("category", {
|
||||
createdBy: text("created_by").notNull(),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||
removedBy: text("removed_by"),
|
||||
removedAt: timestamp("removed_at"),
|
||||
});
|
||||
|
||||
export const budgetRelations = relations(budget, ({ many }) => ({
|
||||
categories: many(category),
|
||||
}));
|
||||
|
||||
export const categoryRelations = relations(category, ({ one }) => ({
|
||||
budget: one(budget, {
|
||||
fields: [category.budgetId],
|
||||
references: [budget.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -8,10 +8,10 @@ type Tx = Transaction<Schema>;
|
||||
export function createMutators(authData: AuthData | null) {
|
||||
return {
|
||||
link: {
|
||||
async create() {},
|
||||
async get(tx: Tx, { link_token }: { link_token: string }) {},
|
||||
async updateTransactions() {},
|
||||
async updateBalences() {},
|
||||
async create() { },
|
||||
async get(tx: Tx, { link_token }: { link_token: string }) { },
|
||||
async updateTransactions() { },
|
||||
async updateBalences() { },
|
||||
async deleteAccounts(tx: Tx, { accountIds }: { accountIds: string[] }) {
|
||||
isLoggedIn(authData);
|
||||
for (const id of accountIds) {
|
||||
@@ -40,7 +40,10 @@ export function createMutators(authData: AuthData | null) {
|
||||
},
|
||||
},
|
||||
budget: {
|
||||
async create(tx: Tx, { id }: { id: string }) {
|
||||
async create(
|
||||
tx: Tx,
|
||||
{ id, categoryId }: { id: string; categoryId: string },
|
||||
) {
|
||||
isLoggedIn(authData);
|
||||
await tx.mutate.budget.insert({
|
||||
id,
|
||||
@@ -48,6 +51,60 @@ export function createMutators(authData: AuthData | null) {
|
||||
label: "New Budget",
|
||||
createdBy: authData.user.id,
|
||||
});
|
||||
await tx.mutate.category.insert({
|
||||
id: categoryId,
|
||||
budgetId: id,
|
||||
amount: 0,
|
||||
every: "week",
|
||||
order: 1000,
|
||||
label: "My category",
|
||||
color: "#f06",
|
||||
createdBy: authData.user.id,
|
||||
});
|
||||
},
|
||||
async delete(tx: Tx, { id }: { id: string }) {
|
||||
isLoggedIn(authData);
|
||||
await tx.mutate.budget.delete({
|
||||
id,
|
||||
});
|
||||
},
|
||||
async createCategory(
|
||||
tx: Tx,
|
||||
{
|
||||
id,
|
||||
budgetId,
|
||||
order,
|
||||
}: { id: string; budgetId: string; order?: number },
|
||||
) {
|
||||
isLoggedIn(authData);
|
||||
tx.mutate.category.insert({
|
||||
id,
|
||||
budgetId,
|
||||
amount: 0,
|
||||
every: "week",
|
||||
order: order || 0,
|
||||
label: "My category",
|
||||
color: "#f06",
|
||||
createdBy: authData.user.id,
|
||||
});
|
||||
},
|
||||
async deleteCategory(tx: Tx, { id }: { id: string }) {
|
||||
isLoggedIn(authData);
|
||||
tx.mutate.category.update({
|
||||
id,
|
||||
removedAt: new Date().getTime(),
|
||||
removedBy: authData.user.id,
|
||||
});
|
||||
},
|
||||
async updateCategory(
|
||||
tx: Tx,
|
||||
{ id, label }: { id: string; label: string },
|
||||
) {
|
||||
isLoggedIn(authData);
|
||||
tx.mutate.category.update({
|
||||
id,
|
||||
label,
|
||||
});
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -65,7 +65,11 @@ export const queries = {
|
||||
z.tuple([]),
|
||||
(authData: AuthData | null) => {
|
||||
isLoggedIn(authData);
|
||||
return builder.budget.limit(10);
|
||||
return builder.budget
|
||||
.related("categories", (q) =>
|
||||
q.where("removedAt", "IS", null).orderBy("order", "desc"),
|
||||
)
|
||||
.limit(10);
|
||||
},
|
||||
),
|
||||
getBudgetCategories: syncedQueryWithContext(
|
||||
|
||||
@@ -273,6 +273,26 @@ export const schema = {
|
||||
>,
|
||||
serverName: "updated_at",
|
||||
},
|
||||
removedBy: {
|
||||
type: "string",
|
||||
optional: true,
|
||||
customType: null as unknown as ZeroCustomType<
|
||||
ZeroSchema,
|
||||
"category",
|
||||
"removedBy"
|
||||
>,
|
||||
serverName: "removed_by",
|
||||
},
|
||||
removedAt: {
|
||||
type: "number",
|
||||
optional: true,
|
||||
customType: null as unknown as ZeroCustomType<
|
||||
ZeroSchema,
|
||||
"category",
|
||||
"removedAt"
|
||||
>,
|
||||
serverName: "removed_at",
|
||||
},
|
||||
},
|
||||
primaryKey: ["id"],
|
||||
},
|
||||
@@ -582,7 +602,28 @@ export const schema = {
|
||||
serverName: "user",
|
||||
},
|
||||
},
|
||||
relationships: {},
|
||||
relationships: {
|
||||
budget: {
|
||||
categories: [
|
||||
{
|
||||
sourceField: ["id"],
|
||||
destField: ["budgetId"],
|
||||
destSchema: "category",
|
||||
cardinality: "many",
|
||||
},
|
||||
],
|
||||
},
|
||||
category: {
|
||||
budget: [
|
||||
{
|
||||
sourceField: ["budgetId"],
|
||||
destField: ["id"],
|
||||
destSchema: "budget",
|
||||
cardinality: "one",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
enableLegacyQueries: false,
|
||||
enableLegacyMutators: false,
|
||||
} as const;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, type ReactNode } from "react";
|
||||
import { Text, Pressable } from "react-native";
|
||||
import { useShortcut, type Key } from "../lib/shortcuts";
|
||||
|
||||
type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
|
||||
|
||||
@@ -7,7 +8,7 @@ export interface ButtonProps {
|
||||
children: ReactNode;
|
||||
onPress?: () => void;
|
||||
variant?: "default" | "secondary" | "destructive";
|
||||
shortcut?: string;
|
||||
shortcut?: Key;
|
||||
}
|
||||
|
||||
const STYLES: Record<
|
||||
@@ -22,16 +23,9 @@ const STYLES: Record<
|
||||
export function Button({ children, variant, onPress, shortcut }: ButtonProps) {
|
||||
const { backgroundColor, color } = STYLES[variant || "default"];
|
||||
|
||||
// if (shortcut) {
|
||||
// useKeys((key) => {
|
||||
// if (
|
||||
// typeof shortcut == "object"
|
||||
// ? key.name == shortcut.name
|
||||
// : key.name == shortcut
|
||||
// )
|
||||
// return onPress;
|
||||
// });
|
||||
// }
|
||||
if (shortcut && onPress) {
|
||||
useShortcut(shortcut, onPress);
|
||||
}
|
||||
|
||||
return (
|
||||
<Pressable onPress={onPress} style={{ backgroundColor }}>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { createContext, type ReactNode } from "react";
|
||||
import { createContext, use, type ReactNode } from "react";
|
||||
import { Modal, View, Text } from "react-native";
|
||||
import { useShortcut } from "../lib/shortcuts";
|
||||
|
||||
export interface DialogState {
|
||||
close?: () => void;
|
||||
}
|
||||
export const Context = createContext<DialogState>({
|
||||
close: () => {},
|
||||
close: () => { },
|
||||
});
|
||||
|
||||
interface ProviderProps {
|
||||
@@ -37,6 +38,9 @@ interface ContentProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
export function Content({ children }: ContentProps) {
|
||||
const { close } = use(Context);
|
||||
useShortcut("escape", () => close?.());
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{ backgroundColor: "white", padding: 12, alignItems: "center" }}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { View, Text } from "react-native";
|
||||
import type { KeyEvent } from "@opentui/core";
|
||||
import { useShortcut } from "../lib/shortcuts/hooks";
|
||||
import type { Key } from "../lib/shortcuts";
|
||||
|
||||
const HEADER_COLOR = "#7158e2";
|
||||
const TABLE_COLORS = ["#ddd", "#eee"];
|
||||
@@ -50,17 +51,22 @@ function renderCell(row: ValidRecord, column: Column): string {
|
||||
return cell.toString();
|
||||
}
|
||||
|
||||
interface TableShortcut<T> {
|
||||
key: Key;
|
||||
handler: (params: { selected: T[]; index: number }) => void;
|
||||
}
|
||||
|
||||
export interface ProviderProps<T> {
|
||||
data: T[];
|
||||
columns: Column[];
|
||||
children: ReactNode;
|
||||
onKey?: (event: KeyEvent, selected: T[]) => void;
|
||||
shortcuts?: TableShortcut<T>[];
|
||||
}
|
||||
export function Provider<T extends ValidRecord>({
|
||||
data,
|
||||
columns,
|
||||
children,
|
||||
onKey,
|
||||
shortcuts,
|
||||
}: ProviderProps<T>) {
|
||||
const [idx, setIdx] = useState(0);
|
||||
const [selectedFrom, setSelectedFrom] = useState<number>();
|
||||
@@ -68,9 +74,30 @@ export function Provider<T extends ValidRecord>({
|
||||
useShortcut("j", () => {
|
||||
setIdx((prev) => Math.min(prev + 1, data.length - 1));
|
||||
});
|
||||
useShortcut("down", () => {
|
||||
setIdx((prev) => Math.min(prev + 1, data.length - 1));
|
||||
});
|
||||
useShortcut("k", () => {
|
||||
setIdx((prev) => Math.max(prev - 1, 0));
|
||||
});
|
||||
useShortcut("up", () => {
|
||||
setIdx((prev) => Math.max(prev - 1, 0));
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setIdx((prev) => Math.max(Math.min(prev, data.length - 1), 0));
|
||||
}, [data]);
|
||||
|
||||
if (shortcuts) {
|
||||
for (const shortcut of shortcuts) {
|
||||
useShortcut(shortcut.key, () => {
|
||||
const from = selectedFrom ? Math.min(idx, selectedFrom) : idx;
|
||||
const to = selectedFrom ? Math.max(idx, selectedFrom) : idx;
|
||||
const selected = data.slice(from, to + 1);
|
||||
shortcut.handler({ selected, index: idx });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const columnMap = new Map(
|
||||
columns.map((col) => {
|
||||
|
||||
@@ -16,9 +16,11 @@ export function ShortcutDebug() {
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
backgroundColor: "black",
|
||||
padding: 10,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: "red", fontFamily: "mono" }}>
|
||||
<Text style={{ color: "red", fontFamily: "mono" }}>Registered:</Text>
|
||||
<Text style={{ color: "red", fontFamily: "mono", textAlign: "right" }}>
|
||||
{entries
|
||||
.values()
|
||||
.map(([key, _]) => key)
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { keysStore } from "./store";
|
||||
import type { KeyName } from "./types";
|
||||
|
||||
const KEY_MAP: { [k: string]: KeyName } = {
|
||||
Escape: "escape",
|
||||
ArrowUp: "up",
|
||||
ArrowDown: "down",
|
||||
ArrowLeft: "left",
|
||||
ArrowRight: "right",
|
||||
};
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.addEventListener("keydown", (e) => {
|
||||
const fn = keysStore.getHandler(e.key);
|
||||
fn?.();
|
||||
const key = Object.hasOwn(KEY_MAP, e.key) ? KEY_MAP[e.key]! : e.key;
|
||||
const fn = keysStore.getHandler(key);
|
||||
// console.log(e.key);
|
||||
if (!fn) return;
|
||||
e.preventDefault();
|
||||
fn();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { keysStore } from "./store";
|
||||
import type { Key } from "./types";
|
||||
import { enforceKeyOptions } from "./util";
|
||||
|
||||
export const useShortcut = (key: string, handler: () => void) => {
|
||||
export const useShortcut = (key: Key, handler: () => void) => {
|
||||
const keyOptions = enforceKeyOptions(key);
|
||||
const keyName = keyOptions.name;
|
||||
const ref = useRef(handler);
|
||||
ref.current = handler;
|
||||
|
||||
useEffect(() => {
|
||||
keysStore.register(key, ref);
|
||||
keysStore.register(keyName, ref);
|
||||
return () => {
|
||||
keysStore.deregister(key);
|
||||
keysStore.deregister(keyName);
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./Debug";
|
||||
export * from "./Provider";
|
||||
export * from "./store";
|
||||
export * from "./hooks";
|
||||
export * from "./types";
|
||||
|
||||
52
packages/ui/lib/shortcuts/types.ts
Normal file
52
packages/ui/lib/shortcuts/types.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
export type KeyName =
|
||||
| "0"
|
||||
| "1"
|
||||
| "2"
|
||||
| "3"
|
||||
| "4"
|
||||
| "5"
|
||||
| "6"
|
||||
| "7"
|
||||
| "8"
|
||||
| "9"
|
||||
| "a"
|
||||
| "b"
|
||||
| "c"
|
||||
| "d"
|
||||
| "e"
|
||||
| "f"
|
||||
| "g"
|
||||
| "h"
|
||||
| "i"
|
||||
| "j"
|
||||
| "k"
|
||||
| "l"
|
||||
| "m"
|
||||
| "n"
|
||||
| "o"
|
||||
| "p"
|
||||
| "q"
|
||||
| "r"
|
||||
| "s"
|
||||
| "t"
|
||||
| "u"
|
||||
| "v"
|
||||
| "w"
|
||||
| "x"
|
||||
| "y"
|
||||
| "z"
|
||||
| ":"
|
||||
| "up"
|
||||
| "down"
|
||||
| "left"
|
||||
| "right"
|
||||
| "return"
|
||||
| "escape";
|
||||
|
||||
export type Key = KeyName | KeyOptions;
|
||||
|
||||
export interface KeyOptions {
|
||||
name: KeyName;
|
||||
ctrl?: boolean;
|
||||
shift?: boolean;
|
||||
}
|
||||
9
packages/ui/lib/shortcuts/util.ts
Normal file
9
packages/ui/lib/shortcuts/util.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { Key, KeyOptions } from "./types";
|
||||
|
||||
export function enforceKeyOptions(key: Key): KeyOptions {
|
||||
return typeof key == "string"
|
||||
? {
|
||||
name: key,
|
||||
}
|
||||
: key;
|
||||
}
|
||||
@@ -1,26 +1,39 @@
|
||||
import { use } from "react";
|
||||
import { View, Text } from "react-native";
|
||||
import { use, useRef, useState } from "react";
|
||||
import { View, Text, TextInput } from "react-native";
|
||||
import { RouterContext } from ".";
|
||||
import { queries, type Mutators, type Schema } from "@money/shared";
|
||||
import {
|
||||
queries,
|
||||
type Category,
|
||||
type Mutators,
|
||||
type Schema,
|
||||
} from "@money/shared";
|
||||
import { useQuery, useZero } from "@rocicorp/zero/react";
|
||||
import * as Table from "../components/Table";
|
||||
import { Button } from "../components/Button";
|
||||
import * as Dialog from "../components/Dialog";
|
||||
|
||||
const COLUMNS: Table.Column[] = [{ name: "label", label: "Name" }];
|
||||
const COLUMNS: Table.Column[] = [
|
||||
{ name: "label", label: "Name" },
|
||||
{ name: "week", label: "Week" },
|
||||
{ name: "month", label: "Month" },
|
||||
{ name: "year", label: "Year" },
|
||||
{ name: "order", label: "Order" },
|
||||
];
|
||||
|
||||
export function Budget() {
|
||||
const { auth } = use(RouterContext);
|
||||
const [budgets] = useQuery(queries.getBudgets(auth));
|
||||
// const [items] = useQuery(queries.getBudgetCategories(auth));
|
||||
|
||||
const items: any[] = [];
|
||||
const [renaming, setRenaming] = useState<Category>();
|
||||
|
||||
const z = useZero<Schema, Mutators>();
|
||||
const refText = useRef("");
|
||||
|
||||
const newBudget = () => {
|
||||
const id = new Date().getTime().toString();
|
||||
const categoryId = new Date().getTime().toString();
|
||||
z.mutate.budget.create({
|
||||
id,
|
||||
categoryId,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -45,21 +58,89 @@ export function Budget() {
|
||||
|
||||
const budget = budgets[0]!;
|
||||
|
||||
const data = budget.categories.slice().map((category) => {
|
||||
const { amount } = category;
|
||||
const week = amount;
|
||||
const month = amount * 4;
|
||||
const year = amount * 12;
|
||||
|
||||
return {
|
||||
...category,
|
||||
...{
|
||||
week,
|
||||
month,
|
||||
year,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const newCategory = ({ index }: { index: number }) => {
|
||||
const id = new Date().getTime().toString();
|
||||
z.mutate.budget.createCategory({
|
||||
id,
|
||||
budgetId: budget.id,
|
||||
order: index - 1,
|
||||
});
|
||||
};
|
||||
|
||||
const deleteCategory = ({ selected }: { selected: { id: string }[] }) => {
|
||||
for (const { id } of selected) {
|
||||
z.mutate.budget.deleteCategory({ id });
|
||||
}
|
||||
};
|
||||
|
||||
const renameCategory = ({ selected }: { selected: Category[] }) => {
|
||||
for (const category of selected) {
|
||||
setRenaming(category);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<View>
|
||||
<Text style={{ fontFamily: "mono" }}>
|
||||
<Dialog.Provider
|
||||
visible={renaming != undefined}
|
||||
close={() => setRenaming(undefined)}
|
||||
>
|
||||
<Dialog.Content>
|
||||
<Text style={{ fontFamily: "mono" }}>Edit Category</Text>
|
||||
|
||||
<TextInput
|
||||
style={{ fontFamily: "mono" }}
|
||||
autoFocus
|
||||
selectTextOnFocus
|
||||
defaultValue={renaming?.label}
|
||||
onChangeText={(t) => {
|
||||
refText.current = t;
|
||||
}}
|
||||
onKeyPress={(e) => {
|
||||
if (!renaming) return;
|
||||
if (e.nativeEvent.key == "Enter") {
|
||||
z.mutate.budget.updateCategory({
|
||||
id: renaming.id,
|
||||
label: refText.current,
|
||||
});
|
||||
setRenaming(undefined);
|
||||
} else if (e.nativeEvent.key == "Escape") {
|
||||
setRenaming(undefined);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Dialog.Content>
|
||||
</Dialog.Provider>
|
||||
|
||||
<View style={{ alignItems: "flex-start" }}>
|
||||
<Text style={{ fontFamily: "mono", textAlign: "left" }}>
|
||||
Selected Budget: {budget.label}
|
||||
</Text>
|
||||
</View>
|
||||
<Table.Provider
|
||||
data={items}
|
||||
data={data}
|
||||
columns={COLUMNS}
|
||||
onKey={(event) => {
|
||||
if (event.name == "n" && event.shift) {
|
||||
newBudget();
|
||||
}
|
||||
}}
|
||||
shortcuts={[
|
||||
{ key: "i", handler: newCategory },
|
||||
{ key: "d", handler: deleteCategory },
|
||||
{ key: "r", handler: renameCategory },
|
||||
]}
|
||||
>
|
||||
<View style={{ flex: 1 }}>
|
||||
<View style={{ flexShrink: 0 }}>
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { createContext, use, useEffect, useRef } from "react";
|
||||
import { createContext, use, type ReactNode } from "react";
|
||||
import { Transactions } from "./transactions";
|
||||
import { View } from "react-native";
|
||||
import { Settings } from "./settings";
|
||||
import type { AuthData } from "@money/shared/auth";
|
||||
import { Budget } from "./budget";
|
||||
import { ShortcutProvider, ShortcutDebug, keysStore } from "../lib/shortcuts";
|
||||
import { useShortcut } from "../lib/shortcuts/hooks";
|
||||
import {
|
||||
ShortcutProvider,
|
||||
ShortcutDebug,
|
||||
useShortcut,
|
||||
type KeyName,
|
||||
} from "../lib/shortcuts";
|
||||
|
||||
const PAGES = {
|
||||
"/": {
|
||||
@@ -24,7 +28,10 @@ const PAGES = {
|
||||
"/family": {},
|
||||
},
|
||||
},
|
||||
};
|
||||
} satisfies Record<
|
||||
string,
|
||||
{ screen: ReactNode; key: KeyName; children?: Record<string, unknown> }
|
||||
>;
|
||||
|
||||
type Join<A extends string, B extends string> = `${A}${B}` extends `${infer X}`
|
||||
? X
|
||||
@@ -53,7 +60,7 @@ interface RouterContextType {
|
||||
export const RouterContext = createContext<RouterContextType>({
|
||||
auth: null,
|
||||
route: "/",
|
||||
setRoute: () => {},
|
||||
setRoute: () => { },
|
||||
});
|
||||
|
||||
type AppProps = {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { RouterContext, type Route } from ".";
|
||||
import { General } from "./settings/general";
|
||||
import { Accounts } from "./settings/accounts";
|
||||
import { Family } from "./settings/family";
|
||||
import { useShortcut } from "../lib/shortcuts";
|
||||
|
||||
type SettingsRoute = Extract<Route, `/settings${string}`>;
|
||||
|
||||
@@ -30,27 +31,24 @@ type Tab = keyof typeof TABS;
|
||||
export function Settings() {
|
||||
const { route, setRoute } = use(RouterContext);
|
||||
|
||||
// useKeyboard(
|
||||
// (key) => {
|
||||
// if (key.name == "h") {
|
||||
// const currentIdx = Object.entries(TABS).findIndex(
|
||||
// ([tabRoute, _]) => tabRoute == route,
|
||||
// );
|
||||
// const routes = Object.keys(TABS) as SettingsRoute[];
|
||||
// const last = routes[currentIdx - 1];
|
||||
// if (!last) return;
|
||||
// setRoute(last);
|
||||
// } else if (key.name == "l") {
|
||||
// const currentIdx = Object.entries(TABS).findIndex(
|
||||
// ([tabRoute, _]) => tabRoute == route,
|
||||
// );
|
||||
// const routes = Object.keys(TABS) as SettingsRoute[];
|
||||
// const next = routes[currentIdx + 1];
|
||||
// if (!next) return;
|
||||
// setRoute(next);
|
||||
// }
|
||||
// },
|
||||
// );
|
||||
useShortcut("h", () => {
|
||||
const currentIdx = Object.entries(TABS).findIndex(
|
||||
([tabRoute, _]) => tabRoute == route,
|
||||
);
|
||||
const routes = Object.keys(TABS) as SettingsRoute[];
|
||||
const last = routes[currentIdx - 1];
|
||||
if (!last) return;
|
||||
setRoute(last);
|
||||
});
|
||||
useShortcut("l", () => {
|
||||
const currentIdx = Object.entries(TABS).findIndex(
|
||||
([tabRoute, _]) => tabRoute == route,
|
||||
);
|
||||
const routes = Object.keys(TABS) as SettingsRoute[];
|
||||
const next = routes[currentIdx + 1];
|
||||
if (!next) return;
|
||||
setRoute(next);
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={{ flexDirection: "row" }}>
|
||||
|
||||
Reference in New Issue
Block a user