feat: add tui app
This commit is contained in:
@@ -1,130 +1,14 @@
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { authClient } from '@/lib/auth-client';
|
||||
import { Button, Linking, Platform, Pressable, Text, View } from 'react-native';
|
||||
import { useQuery, useZero } from "@rocicorp/zero/react";
|
||||
import { queries, type Mutators, type Schema } from '@money/shared';
|
||||
import Header from '@/components/Header';
|
||||
import { useEffect, useState, type ReactNode } from 'react';
|
||||
|
||||
import { Settings } from "@money/ui";
|
||||
|
||||
export default function HomeScreen() {
|
||||
const { data: session } = authClient.useSession();
|
||||
|
||||
const onLogout = () => {
|
||||
authClient.signOut();
|
||||
}
|
||||
const z = useZero<Schema, Mutators>();
|
||||
const [plaidLink] = useQuery(queries.getPlaidLink(session));
|
||||
const [items] = useQuery(queries.getItems(session));
|
||||
|
||||
return (
|
||||
<SafeAreaView>
|
||||
<Header />
|
||||
|
||||
<UI
|
||||
columns={[
|
||||
{
|
||||
name: "Banks",
|
||||
items: items,
|
||||
renderItem: (item, props) => <Row {...props}>{item.name}</Row>
|
||||
},
|
||||
{
|
||||
name: "Family",
|
||||
items: [],
|
||||
renderItem() {
|
||||
return <View></View>;
|
||||
},
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<Settings />
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"@better-auth/expo": "^1.3.27",
|
||||
"@expo/vector-icons": "^15.0.2",
|
||||
"@money/shared": "workspace:*",
|
||||
"@money/ui": "workspace:*",
|
||||
"@react-navigation/bottom-tabs": "^7.4.0",
|
||||
"@react-navigation/elements": "^2.6.3",
|
||||
"@react-navigation/native": "^7.1.8",
|
||||
|
||||
34
apps/tui/.gitignore
vendored
Normal file
34
apps/tui/.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
*.log
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
15
apps/tui/README.md
Normal file
15
apps/tui/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# react
|
||||
|
||||
To install dependencies:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
To run:
|
||||
|
||||
```bash
|
||||
bun run src/index.tsx
|
||||
```
|
||||
|
||||
This project was created using `bun create tui`. [create-tui](https://git.new/create-tui) is the easiest way to get started with OpenTUI.
|
||||
41
apps/tui/build.ts
Normal file
41
apps/tui/build.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import esbuild from "esbuild";
|
||||
import path from "path";
|
||||
|
||||
// Custom plugin to alias "react-native" to react-native-opentui
|
||||
const aliasPlugin = {
|
||||
name: "alias-react-native",
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /^react-native$/ }, args => {
|
||||
return {
|
||||
path: path.resolve(__dirname, "../../packages/react-native-opentui/index.tsx"),
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
// Build configuration
|
||||
await esbuild.build({
|
||||
entryPoints: ["src/index.tsx"], // your app entry
|
||||
bundle: true, // inline all dependencies (ui included)
|
||||
platform: "node", // Node/Bun target
|
||||
format: "esm", // keep ESM for top-level await
|
||||
outfile: "dist/index.js",
|
||||
sourcemap: true,
|
||||
plugins: [aliasPlugin],
|
||||
loader: {
|
||||
".ts": "ts",
|
||||
".tsx": "tsx",
|
||||
},
|
||||
external: [
|
||||
// leave OpenTUI and Bun built-ins for Bun runtime
|
||||
"react",
|
||||
"@opentui/core",
|
||||
"@opentui/react",
|
||||
"@opentui/react/jsx-runtime",
|
||||
"bun:ffi",
|
||||
// "./assets/**/*.scm",
|
||||
// "./assets/**/*.wasm",
|
||||
],
|
||||
});
|
||||
|
||||
console.log("✅ App bundled successfully");
|
||||
22
apps/tui/package.json
Normal file
22
apps/tui/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@money/tui",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"build": "bun run build.js",
|
||||
"start": "bun run dist/index.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@money/ui": "workspace:*",
|
||||
"@money/shared": "workspace:*",
|
||||
"@opentui/core": "^0.1.39",
|
||||
"@opentui/react": "^0.1.39",
|
||||
"react-native": "^0.82.1",
|
||||
"react-native-opentui": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.27.0"
|
||||
}
|
||||
}
|
||||
20
apps/tui/src/index.tsx
Normal file
20
apps/tui/src/index.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { RGBA, TextAttributes, createCliRenderer } from "@opentui/core";
|
||||
import { createRoot } from "@opentui/react";
|
||||
import { Settings } from "@money/ui";
|
||||
import { ZeroProvider } from "@rocicorp/zero/react";
|
||||
import { schema } from '@money/shared';
|
||||
|
||||
const userID = "anon";
|
||||
const server = "http://laptop:4848";
|
||||
const auth = undefined;
|
||||
|
||||
function Main() {
|
||||
return (
|
||||
<ZeroProvider {...{ userID, auth, server, schema }}>
|
||||
<Settings />
|
||||
</ZeroProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const renderer = await createCliRenderer();
|
||||
createRoot(renderer).render(<Main />);
|
||||
33
apps/tui/tsconfig.json
Normal file
33
apps/tui/tsconfig.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"react-native": ["../react-native-opentui"]
|
||||
},
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "@opentui/react",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user