feat: add ui
This commit is contained in:
@@ -4,6 +4,7 @@ import { useQuery, useZero } from "@rocicorp/zero/react";
|
|||||||
import { queries, type Mutators, type Schema } from '@money/shared';
|
import { queries, type Mutators, type Schema } from '@money/shared';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Link } from 'expo-router';
|
import { Link } from 'expo-router';
|
||||||
|
import Header from '@/components/Header';
|
||||||
|
|
||||||
export default function HomeScreen() {
|
export default function HomeScreen() {
|
||||||
const { data: session } = authClient.useSession();
|
const { data: session } = authClient.useSession();
|
||||||
@@ -68,9 +69,7 @@ export default function HomeScreen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<Link prefetch href="/settings">
|
<Header />
|
||||||
<Button title="Settings" />
|
|
||||||
</Link>
|
|
||||||
<View style={{ flexDirection: "row" }}>
|
<View style={{ flexDirection: "row" }}>
|
||||||
<View style={{ backgroundColor: '' }}>
|
<View style={{ backgroundColor: '' }}>
|
||||||
{balances.map((bal, i) => <View key={bal.id} style={{ backgroundColor: i == accountIdx ? 'black' : undefined}}>
|
{balances.map((bal, i) => <View key={bal.id} style={{ backgroundColor: i == accountIdx ? 'black' : undefined}}>
|
||||||
|
|||||||
129
app/settings.tsx
129
app/settings.tsx
@@ -1,8 +1,10 @@
|
|||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { authClient } from '@/lib/auth-client';
|
import { authClient } from '@/lib/auth-client';
|
||||||
import { Button, Linking, Text } from 'react-native';
|
import { Button, Linking, Platform, Pressable, Text, View } from 'react-native';
|
||||||
import { useQuery, useZero } from "@rocicorp/zero/react";
|
import { useQuery, useZero } from "@rocicorp/zero/react";
|
||||||
import { queries, type Mutators, type Schema } from '@money/shared';
|
import { queries, type Mutators, type Schema } from '@money/shared';
|
||||||
|
import Header from '@/components/Header';
|
||||||
|
import { useEffect, useState, type ReactNode } from 'react';
|
||||||
|
|
||||||
export default function HomeScreen() {
|
export default function HomeScreen() {
|
||||||
const { data: session } = authClient.useSession();
|
const { data: session } = authClient.useSession();
|
||||||
@@ -11,31 +13,118 @@ export default function HomeScreen() {
|
|||||||
authClient.signOut();
|
authClient.signOut();
|
||||||
}
|
}
|
||||||
const z = useZero<Schema, Mutators>();
|
const z = useZero<Schema, Mutators>();
|
||||||
const [user] = useQuery(queries.me(session));
|
|
||||||
const [plaidLink] = useQuery(queries.getPlaidLink(session));
|
const [plaidLink] = useQuery(queries.getPlaidLink(session));
|
||||||
|
const [items] = useQuery(queries.getItems(session));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView>
|
||||||
<Text>Hello {user?.name}</Text>
|
<Header />
|
||||||
<Button onPress={onLogout} title="Logout" />
|
|
||||||
|
|
||||||
<Text>{JSON.stringify(plaidLink)}</Text>
|
<UI
|
||||||
{plaidLink ? <Button onPress={() => {
|
columns={[
|
||||||
Linking.openURL(plaidLink.link);
|
{
|
||||||
}} title="Open Plaid" /> : <Text>No plaid link</Text>}
|
name: "Banks",
|
||||||
|
items: items,
|
||||||
<Button onPress={() => {
|
renderItem: (item, props) => <Row {...props}>{item.name}</Row>
|
||||||
z.mutate.link.create();
|
},
|
||||||
}} title="Generate Link" />
|
{
|
||||||
|
name: "Family",
|
||||||
{plaidLink && <Button onPress={() => {
|
items: [],
|
||||||
z.mutate.link.get({ link_token: plaidLink.token });
|
renderItem() {
|
||||||
}} title="Check Link" />}
|
return <View></View>;
|
||||||
|
},
|
||||||
{plaidLink && <Button onPress={() => {
|
}
|
||||||
z.mutate.link.updateTransactions();
|
]}
|
||||||
}} title="Update transactions" />}
|
/>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Col<T> = {
|
||||||
|
name: string;
|
||||||
|
items: T[];
|
||||||
|
renderItem: (item: T, props: { isSelected: boolean, isActive: boolean }) => ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
idx: number;
|
||||||
|
columns: Map<number, State>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function UI({ columns }: { columns: Col<any>[] }) {
|
||||||
|
const [col, setCol] = useState(0);
|
||||||
|
const [state, setState] = useState<State>({
|
||||||
|
idx: 0,
|
||||||
|
columns: new Map(
|
||||||
|
Array.from({ length: columns.length })
|
||||||
|
.map((_, i) => ([i, { idx: 0, columns: new Map() }]))
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
const getColState = (res: State): State => {
|
||||||
|
let i = col;
|
||||||
|
while (i > 0) {
|
||||||
|
res = res.columns.get(res.idx)!;
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const colState = getColState(state);
|
||||||
|
|
||||||
|
const curr = columns.at(col)!;
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (Platform.OS != 'web') return;
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === "j") {
|
||||||
|
setState((prev) => {
|
||||||
|
if (prev.idx + 1 == colState.columns.size) return prev;
|
||||||
|
return {...prev, ...{ idx: prev.idx + 1 }};
|
||||||
|
});
|
||||||
|
} else if (event.key === "k") {
|
||||||
|
setState((prev) => {
|
||||||
|
if (prev.idx == 0) return prev;
|
||||||
|
return {...prev, ...{ idx: prev.idx - 1 }};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{ flexDirection: "row" }}>
|
||||||
|
<Column>
|
||||||
|
{columns.map((c, i) => <Pressable onPress={() => { setCol(i) }}><Row isSelected={col == i} isActive={col == 0}>{c.name}</Row></Pressable>)}
|
||||||
|
</Column>
|
||||||
|
<Column>
|
||||||
|
{curr.items.map((item, i) => curr.renderItem(item, { isSelected: colState.idx == i, isActive: col == 1 }))}
|
||||||
|
</Column>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Column({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Row({ children, isSelected, isActive }: { children: ReactNode, isSelected: boolean, isActive: boolean }) {
|
||||||
|
const color = isSelected ? 'white': undefined;
|
||||||
|
const backgroundColor = isSelected ? (isActive ? 'black' : 'gray'): undefined;
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<Text style={{ fontFamily: 'mono', color, backgroundColor }}>{children}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
70
components/Header.tsx
Normal file
70
components/Header.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
import { queries } from "@/shared/src";
|
||||||
|
import { useQuery } from "@rocicorp/zero/react";
|
||||||
|
import { Link, usePathname, useRouter, type LinkProps } from "expo-router";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { View, Text, Platform } from "react-native";
|
||||||
|
|
||||||
|
type Page = { name: string, href: LinkProps['href'] };
|
||||||
|
const PAGES: Page[] = [
|
||||||
|
{
|
||||||
|
name: "Home",
|
||||||
|
href: "/",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Settings",
|
||||||
|
href: "/settings",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
export default function Header() {
|
||||||
|
const router = useRouter();
|
||||||
|
const { data: session } = authClient.useSession();
|
||||||
|
const [user] = useQuery(queries.me(session));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (Platform.OS != 'web') return;
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === "1" && event.ctrlKey) {
|
||||||
|
router.push(PAGES.at(0)!.href);
|
||||||
|
} else if (event.key === "2" && event.ctrlKey) {
|
||||||
|
router.push(PAGES.at(1)!.href);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{ flexDirection: "row", justifyContent: "space-between", backgroundColor: "#efe" }}>
|
||||||
|
<View style={{ flexDirection: "row" }}>
|
||||||
|
{PAGES.map(page => <Page
|
||||||
|
key={page.name}
|
||||||
|
name={page.name}
|
||||||
|
href={page.href}
|
||||||
|
/>)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Link href={"#" as any}>
|
||||||
|
<Text style={{ fontFamily: 'mono' }}>{user?.name} </Text>
|
||||||
|
</Link>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Page({ name, href }: Page) {
|
||||||
|
const path = usePathname();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link href={href }>
|
||||||
|
<Text style={{ fontFamily: 'mono' }}>{path == href ? `[ ${name} ]` : ` ${name} `}</Text>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -93,9 +93,3 @@ export const auditLogs = pgTable("audit_log", {
|
|||||||
action: text("action").notNull(),
|
action: text("action").notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const plaidAccessTokens = pgTable("plaidAccessToken", {
|
|
||||||
id: text("id").primaryKey(),
|
|
||||||
userId: text("user_id").notNull(),
|
|
||||||
token: text("token").notNull(),
|
|
||||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { definePermissions } from "@rocicorp/zero";
|
|
||||||
import { pgTable, text, boolean, timestamp, uniqueIndex, decimal } from "drizzle-orm/pg-core";
|
import { pgTable, text, boolean, timestamp, uniqueIndex, decimal } from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
export const users = pgTable(
|
export const users = pgTable(
|
||||||
@@ -46,4 +45,13 @@ export const balance = pgTable("balance", {
|
|||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||||
})
|
});
|
||||||
|
|
||||||
|
export const plaidAccessTokens = pgTable("plaidAccessToken", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
logoUrl: text("logoUrl").notNull(),
|
||||||
|
userId: text("user_id").notNull(),
|
||||||
|
token: text("token").notNull(),
|
||||||
|
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||||
|
});
|
||||||
|
|||||||
@@ -30,5 +30,11 @@ export const queries = {
|
|||||||
return builder.balance
|
return builder.balance
|
||||||
.where('user_id', '=', authData.user.id)
|
.where('user_id', '=', authData.user.id)
|
||||||
.orderBy('name', 'asc');
|
.orderBy('name', 'asc');
|
||||||
|
}),
|
||||||
|
getItems: syncedQueryWithContext('getItems', z.tuple([]), (authData: AuthData | null) => {
|
||||||
|
isLoggedIn(authData);
|
||||||
|
return builder.plaidAccessTokens
|
||||||
|
.where('userId', '=', authData.user.id)
|
||||||
|
.orderBy('createdAt', 'desc');
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -103,6 +103,69 @@ export const schema = {
|
|||||||
},
|
},
|
||||||
primaryKey: ["id"],
|
primaryKey: ["id"],
|
||||||
},
|
},
|
||||||
|
plaidAccessTokens: {
|
||||||
|
name: "plaidAccessTokens",
|
||||||
|
columns: {
|
||||||
|
id: {
|
||||||
|
type: "string",
|
||||||
|
optional: false,
|
||||||
|
customType: null as unknown as ZeroCustomType<
|
||||||
|
ZeroSchema,
|
||||||
|
"plaidAccessTokens",
|
||||||
|
"id"
|
||||||
|
>,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: "string",
|
||||||
|
optional: false,
|
||||||
|
customType: null as unknown as ZeroCustomType<
|
||||||
|
ZeroSchema,
|
||||||
|
"plaidAccessTokens",
|
||||||
|
"name"
|
||||||
|
>,
|
||||||
|
},
|
||||||
|
logoUrl: {
|
||||||
|
type: "string",
|
||||||
|
optional: false,
|
||||||
|
customType: null as unknown as ZeroCustomType<
|
||||||
|
ZeroSchema,
|
||||||
|
"plaidAccessTokens",
|
||||||
|
"logoUrl"
|
||||||
|
>,
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: "string",
|
||||||
|
optional: false,
|
||||||
|
customType: null as unknown as ZeroCustomType<
|
||||||
|
ZeroSchema,
|
||||||
|
"plaidAccessTokens",
|
||||||
|
"userId"
|
||||||
|
>,
|
||||||
|
serverName: "user_id",
|
||||||
|
},
|
||||||
|
token: {
|
||||||
|
type: "string",
|
||||||
|
optional: false,
|
||||||
|
customType: null as unknown as ZeroCustomType<
|
||||||
|
ZeroSchema,
|
||||||
|
"plaidAccessTokens",
|
||||||
|
"token"
|
||||||
|
>,
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: "number",
|
||||||
|
optional: true,
|
||||||
|
customType: null as unknown as ZeroCustomType<
|
||||||
|
ZeroSchema,
|
||||||
|
"plaidAccessTokens",
|
||||||
|
"createdAt"
|
||||||
|
>,
|
||||||
|
serverName: "created_at",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
primaryKey: ["id"],
|
||||||
|
serverName: "plaidAccessToken",
|
||||||
|
},
|
||||||
plaidLink: {
|
plaidLink: {
|
||||||
name: "plaidLink",
|
name: "plaidLink",
|
||||||
columns: {
|
columns: {
|
||||||
@@ -351,6 +414,11 @@ export type Schema = typeof schema;
|
|||||||
* This type is auto-generated from your Drizzle schema definition.
|
* This type is auto-generated from your Drizzle schema definition.
|
||||||
*/
|
*/
|
||||||
export type Balance = Row<Schema["tables"]["balance"]>;
|
export type Balance = Row<Schema["tables"]["balance"]>;
|
||||||
|
/**
|
||||||
|
* Represents a row from the "plaidAccessTokens" table.
|
||||||
|
* This type is auto-generated from your Drizzle schema definition.
|
||||||
|
*/
|
||||||
|
export type PlaidAccessToken = Row<Schema["tables"]["plaidAccessTokens"]>;
|
||||||
/**
|
/**
|
||||||
* Represents a row from the "plaidLink" table.
|
* Represents a row from the "plaidLink" table.
|
||||||
* This type is auto-generated from your Drizzle schema definition.
|
* This type is auto-generated from your Drizzle schema definition.
|
||||||
|
|||||||
Reference in New Issue
Block a user