feat: add auth to context
This commit is contained in:
@@ -1,17 +1,18 @@
|
|||||||
import { useLocalSearchParams } from "expo-router";
|
import { useLocalSearchParams } from "expo-router";
|
||||||
import { Text } from "react-native";
|
import { Text } from "react-native";
|
||||||
import { App } from "@money/ui";
|
import { App, type Route } from "@money/ui";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const { route: initalRoute } = useLocalSearchParams<{ route: string[] }>();
|
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(() => {
|
useEffect(() => {
|
||||||
const handler = () => {
|
const handler = () => {
|
||||||
const newRoute = window.location.pathname.slice(1);
|
const newRoute = window.location.pathname.slice(1);
|
||||||
// call your app’s page change logic
|
|
||||||
setRoute(newRoute);
|
setRoute(newRoute);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -21,9 +22,11 @@ export default function Page() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<App
|
<App
|
||||||
page={route as any}
|
auth={data}
|
||||||
onPageChange={(page) => {
|
route={route as Route}
|
||||||
window.history.pushState({}, "", "/" + page);
|
setRoute={(page) => {
|
||||||
|
window.history.pushState({}, "", page);
|
||||||
|
setRoute(page);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { RGBA, TextAttributes, createCliRenderer } from "@opentui/core";
|
import { RGBA, TextAttributes, createCliRenderer } from "@opentui/core";
|
||||||
import { createRoot } from "@opentui/react";
|
import { createRoot } from "@opentui/react";
|
||||||
import { App } from "@money/ui";
|
import { App, type Route } from "@money/ui";
|
||||||
import { ZeroProvider } from "@rocicorp/zero/react";
|
import { ZeroProvider } from "@rocicorp/zero/react";
|
||||||
import { schema } from '@money/shared';
|
import { schema } from '@money/shared';
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
const userID = "anon";
|
const userID = "anon";
|
||||||
const server = "http://laptop:4848";
|
const server = "http://laptop:4848";
|
||||||
@@ -11,10 +12,22 @@ const auth = undefined;
|
|||||||
function Main() {
|
function Main() {
|
||||||
return (
|
return (
|
||||||
<ZeroProvider {...{ userID, auth, server, schema }}>
|
<ZeroProvider {...{ userID, auth, server, schema }}>
|
||||||
<App />
|
<Router />
|
||||||
</ZeroProvider>
|
</ZeroProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Router() {
|
||||||
|
const [route, setRoute] = useState<Route>("/");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<App
|
||||||
|
auth={null}
|
||||||
|
route={route}
|
||||||
|
setRoute={setRoute}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const renderer = await createCliRenderer();
|
const renderer = await createCliRenderer();
|
||||||
createRoot(renderer).render(<Main />);
|
createRoot(renderer).render(<Main />);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from "react";
|
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) {
|
export function View({ children, style }: ViewProps) {
|
||||||
const bg = style &&
|
const bg = style &&
|
||||||
@@ -22,9 +22,42 @@ export function View({ children, style }: ViewProps) {
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return <box backgroundColor={bg} flexDirection={flexDirection} flexGrow={flex}>{children}</box>
|
return <box backgroundColor={bg} flexDirection={flexDirection} flexGrow={flex}>{children}</box>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 <box
|
||||||
|
backgroundColor={bg}
|
||||||
|
flexDirection={flexDirection}
|
||||||
|
flexGrow={flex}
|
||||||
|
onMouseDown={onPress ? ((_event) => {
|
||||||
|
// @ts-ignore
|
||||||
|
onPress();
|
||||||
|
}) : undefined}
|
||||||
|
>{children}</box>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export function Text({ style, children }: TextProps) {
|
export function Text({ style, children }: TextProps) {
|
||||||
const fg = style &&
|
const fg = style &&
|
||||||
'color' in style
|
'color' in style
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ export const queries = {
|
|||||||
.one();
|
.one();
|
||||||
}),
|
}),
|
||||||
allTransactions: syncedQueryWithContext('allTransactions', z.tuple([]), (authData: AuthData | null) => {
|
allTransactions: syncedQueryWithContext('allTransactions', z.tuple([]), (authData: AuthData | null) => {
|
||||||
// isLoggedIn(authData);
|
isLoggedIn(authData);
|
||||||
return builder.transaction
|
return builder.transaction
|
||||||
// .where('user_id', '=', authData.user.id)
|
.where('user_id', '=', authData.user.id)
|
||||||
.orderBy('datetime', 'desc')
|
.orderBy('datetime', 'desc')
|
||||||
.limit(50)
|
.limit(50)
|
||||||
}),
|
}),
|
||||||
@@ -32,9 +32,9 @@ export const queries = {
|
|||||||
.orderBy('name', 'asc');
|
.orderBy('name', 'asc');
|
||||||
}),
|
}),
|
||||||
getItems: syncedQueryWithContext('getItems', z.tuple([]), (authData: AuthData | null) => {
|
getItems: syncedQueryWithContext('getItems', z.tuple([]), (authData: AuthData | null) => {
|
||||||
// isLoggedIn(authData);
|
isLoggedIn(authData);
|
||||||
return builder.plaidAccessTokens
|
return builder.plaidAccessTokens
|
||||||
// .where('userId', '=', authData.user.id)
|
.where('userId', '=', authData.user.id)
|
||||||
.orderBy('createdAt', 'desc');
|
.orderBy('createdAt', 'desc');
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,31 +1,95 @@
|
|||||||
import { useState } from "react";
|
import { createContext, use, useState } from "react";
|
||||||
import { Transactions } from "./transactions";
|
import { Transactions } from "./transactions";
|
||||||
import { Text } from "react-native";
|
import { View, Text } from "react-native";
|
||||||
import { Settings } from "./settings";
|
import { Settings } from "./settings";
|
||||||
import { useKeyboard } from "./useKeyboard";
|
import { useKeyboard } from "./useKeyboard";
|
||||||
|
|
||||||
type Page = "transactions" | "settings";
|
|
||||||
type AppProps = {
|
const PAGES = {
|
||||||
page?: Page;
|
'/': {
|
||||||
onPageChange?: (page: Page) => void;
|
screen: <Transactions />,
|
||||||
|
key: "1",
|
||||||
|
},
|
||||||
|
'/settings': {
|
||||||
|
screen: <Settings />,
|
||||||
|
key: "2",
|
||||||
|
children: {
|
||||||
|
"/accounts": {},
|
||||||
|
"/family": {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
type Join<A extends string, B extends string> =
|
||||||
|
`${A}${B}` extends `${infer X}` ? X : never;
|
||||||
|
|
||||||
|
type ChildRoutes<Parent extends string, Children> =
|
||||||
|
{
|
||||||
|
[K in keyof Children & string]:
|
||||||
|
K extends `/${string}`
|
||||||
|
? Join<Parent, K>
|
||||||
|
: never;
|
||||||
|
}[keyof Children & string];
|
||||||
|
|
||||||
|
type Routes<T> = {
|
||||||
|
[K in keyof T & string]:
|
||||||
|
| K
|
||||||
|
| (T[K] extends { children: infer C }
|
||||||
|
? ChildRoutes<K, C>
|
||||||
|
: never)
|
||||||
|
}[keyof T & string];
|
||||||
|
|
||||||
|
export type Route = Routes<typeof PAGES>;
|
||||||
|
|
||||||
|
type Auth = any;
|
||||||
|
|
||||||
|
interface RouterContextType {
|
||||||
|
auth: Auth;
|
||||||
|
route: Route;
|
||||||
|
setRoute: (route: Route) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function App({ page, onPageChange }: AppProps) {
|
|
||||||
const [curr, setPage] = useState<Page>(page || "transactions");
|
export const RouterContext = createContext<RouterContextType>({
|
||||||
|
auth: null,
|
||||||
|
route: '/',
|
||||||
|
setRoute: () => {}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
type AppProps = {
|
||||||
|
auth: Auth;
|
||||||
|
route: Route;
|
||||||
|
setRoute: (page: Route) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function App({ auth, route, setRoute }: AppProps) {
|
||||||
|
return <RouterContext.Provider value={{ auth, route, setRoute }}>
|
||||||
|
<Main />
|
||||||
|
</RouterContext.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
|
function Main() {
|
||||||
|
const { route, setRoute } = use(RouterContext);
|
||||||
|
|
||||||
useKeyboard((key) => {
|
useKeyboard((key) => {
|
||||||
if (key.name == "1") {
|
const screen = Object.entries(PAGES)
|
||||||
setPage("transactions");
|
.find(([, screen]) => screen.key == key.name);
|
||||||
if (onPageChange)
|
|
||||||
onPageChange("transactions");
|
if (!screen) return;
|
||||||
} else if (key.name == "2") {
|
|
||||||
setPage("settings");
|
const [route] = screen as [Route, never];
|
||||||
if (onPageChange)
|
|
||||||
onPageChange("settings");
|
setRoute(route);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return curr == "transactions" ? <Transactions /> : <Settings />;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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<Route, `/settings${string}`>;
|
||||||
|
|
||||||
|
const TABS = {
|
||||||
|
"/settings": {
|
||||||
|
label: "General",
|
||||||
|
screen: <General />
|
||||||
|
},
|
||||||
|
"/settings/accounts": {
|
||||||
|
label: "Bank Accounts",
|
||||||
|
screen: <Accounts />
|
||||||
|
},
|
||||||
|
"/settings/family": {
|
||||||
|
label: "Family",
|
||||||
|
screen: <Family />
|
||||||
|
},
|
||||||
|
} as const satisfies Record<SettingsRoute, { label: string, screen: ReactNode }>;
|
||||||
|
|
||||||
|
type Tab = keyof typeof TABS;
|
||||||
|
|
||||||
export function Settings() {
|
export function Settings() {
|
||||||
return <Text>Settings</Text>;
|
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 (
|
||||||
|
<View style={{ flexDirection: "row" }}>
|
||||||
|
|
||||||
|
<View>
|
||||||
|
{Object.entries(TABS).map(([tabRoute, tab]) => {
|
||||||
|
const isSelected = tabRoute == route;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pressable key={tab.label} style={{ backgroundColor: isSelected ? 'black' : undefined }} onPress={() => setRoute(tabRoute as SettingsRoute)}>
|
||||||
|
<Text style={{ fontFamily: 'mono', color: isSelected ? 'white' : 'black' }}>{tab.label}</Text>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View>
|
||||||
|
{TABS[route as Tab].screen}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
23
packages/ui/src/settings/accounts.tsx
Normal file
23
packages/ui/src/settings/accounts.tsx
Normal file
@@ -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 (
|
||||||
|
<Table.Provider columns={COLUMNS} data={items}>
|
||||||
|
<Table.Body />
|
||||||
|
</Table.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
6
packages/ui/src/settings/family.tsx
Normal file
6
packages/ui/src/settings/family.tsx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { Text } from "react-native";
|
||||||
|
|
||||||
|
export function Family() {
|
||||||
|
return <Text style={{ fontFamily: 'mono' }}>Welcome to family</Text>
|
||||||
|
}
|
||||||
|
|
||||||
7
packages/ui/src/settings/general.tsx
Normal file
7
packages/ui/src/settings/general.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { Text } from "react-native";
|
||||||
|
|
||||||
|
export function General() {
|
||||||
|
return <Text style={{ fontFamily: 'mono' }}>Welcome to settings</Text>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -3,6 +3,7 @@ import { useQuery } from "@rocicorp/zero/react";
|
|||||||
import { queries, type Transaction } from '@money/shared';
|
import { queries, type Transaction } from '@money/shared';
|
||||||
import { use } from "react";
|
import { use } from "react";
|
||||||
import { View, Text } from "react-native";
|
import { View, Text } from "react-native";
|
||||||
|
import { RouterContext } from ".";
|
||||||
|
|
||||||
|
|
||||||
const FORMAT = new Intl.NumberFormat("en-US", {
|
const FORMAT = new Intl.NumberFormat("en-US", {
|
||||||
@@ -23,7 +24,8 @@ const COLUMNS: Table.Column[] = [
|
|||||||
|
|
||||||
|
|
||||||
export function Transactions() {
|
export function Transactions() {
|
||||||
const [items] = useQuery(queries.allTransactions(null));
|
const { auth } = use(RouterContext);
|
||||||
|
const [items] = useQuery(queries.allTransactions(auth));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table.Provider
|
<Table.Provider
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ export function useKeyboard(handler: (key: KeyEvent) => void, deps: any[] = [])
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.addEventListener("keydown", handlerWeb);
|
window.addEventListener("keydown", handlerWeb);
|
||||||
return () => {
|
return () => {
|
||||||
console.log("REMOVING");
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.removeEventListener("keydown", handlerWeb);
|
window.removeEventListener("keydown", handlerWeb);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user