Add beenvoice mobile companion app with full dark mode support.

Expo app with dashboard, time clock, invoices, and settings — native tabs, glass UI, theme-aware components, and iOS Live Activities.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-17 22:36:37 -04:00
parent 8a7a8df477
commit 14c880123c
93 changed files with 8849 additions and 7849 deletions
+77
View File
@@ -0,0 +1,77 @@
import { useEffect, useState } from "react";
import { StyleSheet, Text, View } from "react-native";
import { Input } from "@/components/ui/Input";
import { fonts, spacing } from "@/constants/theme";
import { useAccounts } from "@/contexts/AccountsContext";
import { useAppTheme } from "@/contexts/ThemeContext";
import { normalizeInstanceUrl } from "@/lib/instance-url";
type InstanceUrlFieldProps = {
onSaved?: (url: string) => void;
};
export function InstanceUrlField({ onSaved }: InstanceUrlFieldProps) {
const { colors } = useAppTheme();
const { apiUrl, setInstanceUrl } = useAccounts();
const [value, setValue] = useState(apiUrl);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
setValue(apiUrl);
}, [apiUrl]);
async function commit() {
const trimmed = value.trim();
if (!trimmed || trimmed === apiUrl) {
setError(null);
return;
}
const normalized = normalizeInstanceUrl(trimmed);
if (!normalized) {
setError("Enter a valid URL like beenvoice.app or localhost:3000");
return;
}
try {
const saved = await setInstanceUrl(trimmed);
setValue(saved);
setError(null);
onSaved?.(saved);
} catch (err) {
setError(err instanceof Error ? err.message : "Could not save server URL");
}
}
return (
<View style={styles.wrapper}>
<Input
label="Server instance"
value={value}
onChangeText={setValue}
onBlur={commit}
onSubmitEditing={commit}
autoCapitalize="none"
autoCorrect={false}
keyboardType="url"
placeholder="beenvoice.app or localhost:3000"
error={error ?? undefined}
/>
<Text style={[styles.hint, { color: colors.mutedForeground }]}>
Point the app at your beenvoice server. Use your Mac&apos;s LAN IP on a physical device.
</Text>
</View>
);
}
const styles = StyleSheet.create({
wrapper: {
gap: spacing.xs,
},
hint: {
fontSize: 12,
fontFamily: fonts.body,
lineHeight: 16,
},
});