diff --git a/apps/expo/app/[...route].tsx b/apps/expo/app/[...route].tsx index c27e6be..7a3d2dc 100644 --- a/apps/expo/app/[...route].tsx +++ b/apps/expo/app/[...route].tsx @@ -1,17 +1,18 @@ import { useLocalSearchParams } from "expo-router"; import { Text } from "react-native"; -import { App } from "@money/ui"; +import { App, type Route } from "@money/ui"; import { useEffect, useState } from "react"; +import { authClient } from "@/lib/auth-client"; export default function Page() { const { route: initalRoute } = useLocalSearchParams<{ route: string[] }>(); - const [route, setRoute] = useState(initalRoute[0]!); + const [route, setRoute] = useState(initalRoute ? "/" + initalRoute.join("/") : "/"); + + const { data } = authClient.useSession(); - // detect back/forward useEffect(() => { const handler = () => { const newRoute = window.location.pathname.slice(1); - // call your app’s page change logic setRoute(newRoute); }; @@ -21,9 +22,11 @@ export default function Page() { return ( { - window.history.pushState({}, "", "/" + page); + auth={data} + route={route as Route} + setRoute={(page) => { + window.history.pushState({}, "", page); + setRoute(page); }} /> ); diff --git a/apps/tui/src/index.tsx b/apps/tui/src/index.tsx index d5f9027..e5b5e05 100644 --- a/apps/tui/src/index.tsx +++ b/apps/tui/src/index.tsx @@ -1,8 +1,9 @@ import { RGBA, TextAttributes, createCliRenderer } from "@opentui/core"; import { createRoot } from "@opentui/react"; -import { App } from "@money/ui"; +import { App, type Route } from "@money/ui"; import { ZeroProvider } from "@rocicorp/zero/react"; import { schema } from '@money/shared'; +import { useState } from "react"; const userID = "anon"; const server = "http://laptop:4848"; @@ -11,10 +12,22 @@ const auth = undefined; function Main() { return ( - + ); } +function Router() { + const [route, setRoute] = useState("/"); + + return ( + + ); +} + const renderer = await createCliRenderer(); createRoot(renderer).render(
); diff --git a/packages/react-native-opentui/index.tsx b/packages/react-native-opentui/index.tsx index ba790e8..6fa1bb1 100644 --- a/packages/react-native-opentui/index.tsx +++ b/packages/react-native-opentui/index.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import type { ViewProps, TextProps } from "react-native"; +import type { ViewProps, TextProps, PressableProps } from "react-native"; export function View({ children, style }: ViewProps) { const bg = style && @@ -22,9 +22,42 @@ export function View({ children, style }: ViewProps) { : undefined; return {children} - } +export function Pressable({ children: childrenRaw, style, onPress }: PressableProps) { + const bg = style && + 'backgroundColor' in style + ? typeof style.backgroundColor == 'string' + ? style.backgroundColor + : undefined + : undefined; + const flexDirection = style && + 'flexDirection' in style + ? typeof style.flexDirection == 'string' + ? style.flexDirection + : undefined + : undefined; + const flex = style && + 'flex' in style + ? typeof style.flex == 'number' + ? style.flex + : undefined + : undefined; + + const children = childrenRaw instanceof Function ? childrenRaw({ pressed: true }) : childrenRaw; + + return { + // @ts-ignore + onPress(); + }) : undefined} + >{children} +} + + export function Text({ style, children }: TextProps) { const fg = style && 'color' in style diff --git a/packages/shared/src/queries.ts b/packages/shared/src/queries.ts index 53a4dd5..95cc75f 100644 --- a/packages/shared/src/queries.ts +++ b/packages/shared/src/queries.ts @@ -12,9 +12,9 @@ export const queries = { .one(); }), allTransactions: syncedQueryWithContext('allTransactions', z.tuple([]), (authData: AuthData | null) => { - // isLoggedIn(authData); + isLoggedIn(authData); return builder.transaction - // .where('user_id', '=', authData.user.id) + .where('user_id', '=', authData.user.id) .orderBy('datetime', 'desc') .limit(50) }), @@ -32,9 +32,9 @@ export const queries = { .orderBy('name', 'asc'); }), getItems: syncedQueryWithContext('getItems', z.tuple([]), (authData: AuthData | null) => { - // isLoggedIn(authData); + isLoggedIn(authData); return builder.plaidAccessTokens - // .where('userId', '=', authData.user.id) + .where('userId', '=', authData.user.id) .orderBy('createdAt', 'desc'); }) }; diff --git a/packages/ui/src/index.tsx b/packages/ui/src/index.tsx index 4b023f9..b8e8a84 100644 --- a/packages/ui/src/index.tsx +++ b/packages/ui/src/index.tsx @@ -1,31 +1,95 @@ -import { useState } from "react"; +import { createContext, use, useState } from "react"; import { Transactions } from "./transactions"; -import { Text } from "react-native"; +import { View, Text } from "react-native"; import { Settings } from "./settings"; import { useKeyboard } from "./useKeyboard"; -type Page = "transactions" | "settings"; -type AppProps = { - page?: Page; - onPageChange?: (page: Page) => void; + +const PAGES = { + '/': { + screen: , + key: "1", + }, + '/settings': { + screen: , + key: "2", + children: { + "/accounts": {}, + "/family": {}, + } + }, +}; + +type Join = + `${A}${B}` extends `${infer X}` ? X : never; + +type ChildRoutes = + { + [K in keyof Children & string]: + K extends `/${string}` + ? Join + : never; + }[keyof Children & string]; + +type Routes = { + [K in keyof T & string]: + | K + | (T[K] extends { children: infer C } + ? ChildRoutes + : never) +}[keyof T & string]; + +export type Route = Routes; + +type Auth = any; + +interface RouterContextType { + auth: Auth; + route: Route; + setRoute: (route: Route) => void; } -export function App({ page, onPageChange }: AppProps) { - const [curr, setPage] = useState(page || "transactions"); + +export const RouterContext = createContext({ + auth: null, + route: '/', + setRoute: () => {} +}); + + +type AppProps = { + auth: Auth; + route: Route; + setRoute: (page: Route) => void; +} + +export function App({ auth, route, setRoute }: AppProps) { + return +
+ +} + +function Main() { + const { route, setRoute } = use(RouterContext); useKeyboard((key) => { - if (key.name == "1") { - setPage("transactions"); - if (onPageChange) - onPageChange("transactions"); - } else if (key.name == "2") { - setPage("settings"); - if (onPageChange) - onPageChange("settings"); - } + const screen = Object.entries(PAGES) + .find(([, screen]) => screen.key == key.name); + + if (!screen) return; + + const [route] = screen as [Route, never]; + + setRoute(route); }); - return curr == "transactions" ? : ; + const match = + route in PAGES + ? (route as keyof typeof PAGES) + : (Object.keys(PAGES).sort((a, b) => b.length - a.length).find(p => route.startsWith(p)) as + keyof typeof PAGES); + + return PAGES[match].screen; } diff --git a/packages/ui/src/settings.tsx b/packages/ui/src/settings.tsx index 87c8927..d72613e 100644 --- a/packages/ui/src/settings.tsx +++ b/packages/ui/src/settings.tsx @@ -1,5 +1,68 @@ -import { Text } from "react-native"; +import { Text, View, Pressable } from "react-native"; +import { use, useState, type ReactNode } from "react"; +import { RouterContext, type Route } from "."; +import { General } from "./settings/general"; +import { Accounts } from "./settings/accounts"; +import { Family } from "./settings/family"; +import { useKeyboard } from "./useKeyboard"; + +type SettingsRoute = Extract; + +const TABS = { + "/settings": { + label: "General", + screen: + }, + "/settings/accounts": { + label: "Bank Accounts", + screen: + }, + "/settings/family": { + label: "Family", + screen: + }, +} as const satisfies Record; + +type Tab = keyof typeof TABS; export function Settings() { - return 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); + } + }, [route]); + + return ( + + + + {Object.entries(TABS).map(([tabRoute, tab]) => { + const isSelected = tabRoute == route; + + return ( + setRoute(tabRoute as SettingsRoute)}> + {tab.label} + + ); + })} + + + + {TABS[route as Tab].screen} + + + ); } + diff --git a/packages/ui/src/settings/accounts.tsx b/packages/ui/src/settings/accounts.tsx new file mode 100644 index 0000000..6d37bad --- /dev/null +++ b/packages/ui/src/settings/accounts.tsx @@ -0,0 +1,23 @@ +import { useQuery } from "@rocicorp/zero/react"; +import { queries } from '@money/shared'; +import * as Table from "../table"; +import { use } from "react"; +import { RouterContext } from ".."; + +const COLUMNS: Table.Column[] = [ + { name: 'name', label: 'Name' }, + { name: 'createdAt', label: 'Added At', render: (n) => new Date(n).toLocaleString() }, +]; + +export function Accounts() { + const { auth } = use(RouterContext); + const [items] = useQuery(queries.getItems(auth)); + + return ( + + + + ); +} + + diff --git a/packages/ui/src/settings/family.tsx b/packages/ui/src/settings/family.tsx new file mode 100644 index 0000000..16357e8 --- /dev/null +++ b/packages/ui/src/settings/family.tsx @@ -0,0 +1,6 @@ +import { Text } from "react-native"; + +export function Family() { + return Welcome to family +} + diff --git a/packages/ui/src/settings/general.tsx b/packages/ui/src/settings/general.tsx new file mode 100644 index 0000000..cf2e72b --- /dev/null +++ b/packages/ui/src/settings/general.tsx @@ -0,0 +1,7 @@ +import { Text } from "react-native"; + +export function General() { + return Welcome to settings +} + + diff --git a/packages/ui/src/transactions.tsx b/packages/ui/src/transactions.tsx index b741b8c..c619e1e 100644 --- a/packages/ui/src/transactions.tsx +++ b/packages/ui/src/transactions.tsx @@ -3,6 +3,7 @@ import { useQuery } from "@rocicorp/zero/react"; import { queries, type Transaction } from '@money/shared'; import { use } from "react"; import { View, Text } from "react-native"; +import { RouterContext } from "."; const FORMAT = new Intl.NumberFormat("en-US", { @@ -23,7 +24,8 @@ const COLUMNS: Table.Column[] = [ export function Transactions() { - const [items] = useQuery(queries.allTransactions(null)); + const { auth } = use(RouterContext); + const [items] = useQuery(queries.allTransactions(auth)); return ( void, deps: any[] = []) // @ts-ignore window.addEventListener("keydown", handlerWeb); return () => { - console.log("REMOVING"); // @ts-ignore window.removeEventListener("keydown", handlerWeb); };