14c880123c
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>
151 lines
3.8 KiB
TypeScript
151 lines
3.8 KiB
TypeScript
import { Ionicons } from "@expo/vector-icons";
|
|
import { useEffect, useState } from "react";
|
|
import { Pressable, StyleSheet, Text, View } from "react-native";
|
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
|
|
import { Input } from "@/components/ui/Input";
|
|
import { fonts, spacing } from "@/constants/theme";
|
|
import { useAccounts } from "@/contexts/AccountsContext";
|
|
import { useAppTheme } from "@/contexts/ThemeContext";
|
|
import { hasConfiguredInstanceUrl } from "@/lib/accounts";
|
|
|
|
type CollapsibleServerFieldProps = {
|
|
defaultExpanded?: boolean;
|
|
};
|
|
|
|
function formatServerLabel(url: string) {
|
|
try {
|
|
return new URL(url).host;
|
|
} catch {
|
|
return url.replace(/^https?:\/\//, "");
|
|
}
|
|
}
|
|
|
|
export function CollapsibleServerField({ defaultExpanded = false }: CollapsibleServerFieldProps) {
|
|
const { colors } = useAppTheme();
|
|
const insets = useSafeAreaInsets();
|
|
const { apiUrl, setInstanceUrl } = useAccounts();
|
|
const [expanded, setExpanded] = useState(defaultExpanded);
|
|
const [value, setValue] = useState(apiUrl);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
hasConfiguredInstanceUrl().then((configured) => {
|
|
if (!configured) setExpanded(true);
|
|
});
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
setValue(apiUrl);
|
|
}, [apiUrl]);
|
|
|
|
async function commit() {
|
|
const trimmed = value.trim();
|
|
if (!trimmed || trimmed === apiUrl) {
|
|
setError(null);
|
|
setExpanded(false);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const saved = await setInstanceUrl(trimmed);
|
|
setValue(saved);
|
|
setError(null);
|
|
setExpanded(false);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "Could not save server URL");
|
|
}
|
|
}
|
|
|
|
return (
|
|
<View
|
|
style={[
|
|
styles.wrapper,
|
|
{ paddingBottom: Math.max(insets.bottom, spacing.sm) },
|
|
]}
|
|
>
|
|
<Pressable
|
|
accessibilityRole="button"
|
|
accessibilityState={{ expanded }}
|
|
onPress={() => setExpanded((open) => !open)}
|
|
hitSlop={8}
|
|
style={({ pressed }) => [styles.trigger, pressed && styles.pressed]}
|
|
>
|
|
<Text style={[styles.triggerText, { color: colors.mutedForeground }]}>
|
|
Server ·{" "}
|
|
<Text style={[styles.host, { color: colors.foreground }]}>
|
|
{formatServerLabel(apiUrl)}
|
|
</Text>
|
|
</Text>
|
|
<Ionicons
|
|
name={expanded ? "chevron-down" : "chevron-up"}
|
|
size={16}
|
|
color={colors.mutedForeground}
|
|
/>
|
|
</Pressable>
|
|
|
|
{expanded ? (
|
|
<View
|
|
style={[
|
|
styles.panel,
|
|
{ backgroundColor: colors.cardGlass, borderColor: colors.borderGlass },
|
|
]}
|
|
>
|
|
<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 }]}>
|
|
Use your Mac's LAN IP on a physical device.
|
|
</Text>
|
|
</View>
|
|
) : null}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
wrapper: {
|
|
flexDirection: "column-reverse",
|
|
gap: spacing.sm,
|
|
paddingHorizontal: spacing.md,
|
|
},
|
|
trigger: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
gap: spacing.xs,
|
|
minHeight: 36,
|
|
},
|
|
pressed: {
|
|
opacity: 0.7,
|
|
},
|
|
triggerText: {
|
|
fontSize: 13,
|
|
fontFamily: fonts.body,
|
|
},
|
|
host: {
|
|
fontFamily: fonts.mono,
|
|
fontSize: 13,
|
|
},
|
|
panel: {
|
|
borderWidth: 1,
|
|
borderRadius: 14,
|
|
padding: spacing.md,
|
|
gap: spacing.sm,
|
|
},
|
|
hint: {
|
|
fontSize: 12,
|
|
fontFamily: fonts.body,
|
|
lineHeight: 16,
|
|
},
|
|
});
|