Files
codexbar-mobile/app/(tabs)/index.tsx
T
space 2c31be11c4
CodexBar Mobile Build / build-android (push) Successful in 21m9s
CodexBar Mobile Build / release (push) Successful in 11s
by codex: feat. General Improvements
2026-06-25 14:27:58 +02:00

332 lines
13 KiB
TypeScript

import {
ScrollView,
View,
Text,
TouchableOpacity,
RefreshControl,
ActivityIndicator,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { router, useFocusEffect } from "expo-router";
import { useCallback, useEffect } from "react";
import { MaterialIcons } from "@expo/vector-icons";
import { useCodexUsage } from "@/hooks/useCodexUsage";
import { useClaudeUsage } from "@/hooks/useClaudeUsage";
import { onUsageDataLoaded } from "@/lib/notifications";
import { ProgressBar } from "@/components/ProgressBar";
import { ResetCountdown } from "@/components/ResetCountdown";
import { LastUpdated } from "@/components/LastUpdated";
import { ScreenErrorBoundary } from "@/components/ScreenErrorBoundary";
import { COLORS, getUsageColor } from "@/lib/constants";
import { windowLabel } from "@/lib/timeUtils";
function UsageRow({
label,
percent,
resetAtSeconds,
resetAtISO,
}: {
label: string;
percent: number;
resetAtSeconds?: number;
resetAtISO?: string;
}) {
const color = getUsageColor(percent);
return (
<View className="mb-4">
<View className="flex-row justify-between items-baseline mb-1.5">
<Text className="text-xs font-medium text-neutral-500 dark:text-neutral-400">
{label}
</Text>
<Text className="text-sm font-bold" style={{ color }}>
{Math.round(percent)}% used
</Text>
</View>
<ProgressBar percent={percent} />
<ResetCountdown resetAtSeconds={resetAtSeconds} resetAtISO={resetAtISO} />
</View>
);
}
function SetupPrompt({ label }: { label: string }) {
return (
<View>
<Text className="text-sm text-neutral-400 dark:text-neutral-500 mb-3">
{label}
</Text>
<TouchableOpacity
onPress={() => router.navigate("/(tabs)/settings")}
className="flex-row items-center self-start gap-x-1.5 px-3 py-2 rounded-xl border border-neutral-200 dark:border-neutral-700"
>
<Text className="text-xs font-semibold text-neutral-600 dark:text-neutral-300">
Set up in Settings
</Text>
<MaterialIcons name="arrow-forward" size={12} color="#737373" />
</TouchableOpacity>
</View>
);
}
function DashboardTabContent() {
const codex = useCodexUsage();
const claude = useClaudeUsage();
// Fire daily digest reschedule + threshold check whenever fresh data arrives
useEffect(() => {
if (codex.usage || claude.usage) {
void onUsageDataLoaded(
claude.usage,
codex.usage
);
}
}, [codex.lastFetchedAt, claude.lastFetchedAt]);
useFocusEffect(
useCallback(() => {
void codex.reloadCredentials();
void claude.reloadCredentials();
}, [codex.reloadCredentials, claude.reloadCredentials])
);
const isRefreshing =
codex.status === "loading" || claude.status === "loading";
const handleRefresh = () => {
void codex.refresh();
void claude.refresh();
};
const connectedCount = [codex.auth, claude.auth].filter(Boolean).length;
return (
<SafeAreaView
className="flex-1 bg-neutral-50 dark:bg-neutral-950"
edges={["top"]}
>
<ScrollView
className="flex-1"
contentContainerStyle={{ paddingHorizontal: 16, paddingBottom: 40 }}
refreshControl={
<RefreshControl
refreshing={isRefreshing}
onRefresh={handleRefresh}
tintColor={COLORS.codex}
/>
}
>
{/* Header */}
<View className="flex-row items-center justify-between pt-6 pb-5">
<View className="flex-row items-center gap-x-3">
<View className="w-10 h-10 rounded-2xl bg-emerald-500 items-center justify-center">
<MaterialIcons name="bar-chart" size={20} color="white" />
</View>
<View>
<Text className="text-xl font-bold text-neutral-900 dark:text-white tracking-tight">
Codexbar
</Text>
<Text className="text-xs text-neutral-400 mt-0.5">
{connectedCount === 0
? "No services connected"
: connectedCount === 2
? "2 services connected"
: "1 of 2 services connected"}
</Text>
</View>
</View>
<TouchableOpacity
onPress={handleRefresh}
className="w-9 h-9 rounded-xl bg-neutral-100 dark:bg-neutral-800 items-center justify-center"
>
<MaterialIcons name="refresh" size={18} color={COLORS.codex} />
</TouchableOpacity>
</View>
{/* Codex card */}
<View className="rounded-2xl bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 mb-4 overflow-hidden">
<View style={{ height: 3, backgroundColor: COLORS.codex }} />
<View className="p-4">
<View className="flex-row items-center justify-between mb-4">
<View className="flex-row items-center gap-x-2.5">
<View
className="w-8 h-8 rounded-xl items-center justify-center"
style={{ backgroundColor: `${COLORS.codex}18` }}
>
<MaterialIcons name="auto-awesome" size={16} color={COLORS.codex} />
</View>
<Text className="text-sm font-semibold text-neutral-900 dark:text-white">
Codex CLI
</Text>
</View>
<View className="flex-row items-center gap-x-2">
{codex.usage?.plan_type && (
<View className="px-2.5 py-1 rounded-full bg-green-100 dark:bg-green-900/30">
<Text className="text-xs font-semibold text-green-700 dark:text-green-300 capitalize">
{codex.usage.plan_type}
</Text>
</View>
)}
{codex.auth && codex.status === "error" && (
<View className="w-2 h-2 rounded-full bg-red-500" />
)}
{codex.status === "success" && !codex.usage?.plan_type && (
<View className="w-2 h-2 rounded-full bg-green-500" />
)}
</View>
</View>
{!codex.auth && (
<SetupPrompt label="Import auth.json to see your Codex rate limits." />
)}
{codex.auth && codex.status === "loading" && !codex.usage && (
<ActivityIndicator
color={COLORS.codex}
style={{ marginVertical: 16 }}
/>
)}
{codex.auth && codex.status === "error" && (
<View className="flex-row items-center gap-x-2 py-1">
<MaterialIcons name="error-outline" size={16} color="#ef4444" />
<Text className="text-sm text-red-500 dark:text-red-400">
Failed to fetch usage
</Text>
</View>
)}
{codex.usage && (
<View>
<UsageRow
label={`${windowLabel(codex.usage.rate_limit.primary_window.limit_window_seconds)} window`}
percent={codex.usage.rate_limit.primary_window.used_percent}
resetAtSeconds={codex.usage.rate_limit.primary_window.reset_at}
/>
<UsageRow
label={`${windowLabel(codex.usage.rate_limit.secondary_window.limit_window_seconds)} window`}
percent={
codex.usage.rate_limit.secondary_window.used_percent
}
resetAtSeconds={
codex.usage.rate_limit.secondary_window.reset_at
}
/>
{!codex.usage.credits.unlimited &&
codex.usage.credits.has_credits && (
<View className="pt-3 border-t border-neutral-100 dark:border-neutral-800 flex-row items-center gap-x-2">
<MaterialIcons name="toll" size={14} color="#a3a3a3" />
<Text className="text-xs text-neutral-500 dark:text-neutral-400">
${Number(codex.usage.credits.balance).toFixed(2)}{" "}
credits remaining
</Text>
</View>
)}
{codex.usage.credits.unlimited && (
<View className="pt-3 border-t border-neutral-100 dark:border-neutral-800 flex-row items-center gap-x-2">
<MaterialIcons
name="all-inclusive"
size={14}
color={COLORS.codex}
/>
<Text className="text-xs text-green-600 dark:text-green-400">
Unlimited credits
</Text>
</View>
)}
<View className="mt-3 border-t border-neutral-100 pt-3 dark:border-neutral-800">
<LastUpdated timestamp={codex.lastFetchedAt} />
</View>
</View>
)}
</View>
</View>
{/* Claude card */}
<View className="rounded-2xl bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 mb-4 overflow-hidden">
<View style={{ height: 3, backgroundColor: COLORS.claude }} />
<View className="p-4">
<View className="flex-row items-center justify-between mb-4">
<View className="flex-row items-center gap-x-2.5">
<View
className="w-8 h-8 rounded-xl items-center justify-center"
style={{ backgroundColor: `${COLORS.claude}18` }}
>
<MaterialIcons name="psychology" size={16} color={COLORS.claude} />
</View>
<Text className="text-sm font-semibold text-neutral-900 dark:text-white">
Claude.ai
</Text>
</View>
{claude.auth && claude.status === "error" && (
<View className="w-2 h-2 rounded-full bg-red-500" />
)}
{claude.status === "success" && (
<View className="w-2 h-2 rounded-full bg-green-500" />
)}
</View>
{!claude.auth && (
<SetupPrompt label="Add your session key to see Claude usage windows." />
)}
{claude.auth && claude.status === "loading" && !claude.usage && (
<ActivityIndicator
color={COLORS.claude}
style={{ marginVertical: 16 }}
/>
)}
{claude.auth && claude.status === "error" && (
<View className="flex-row items-center gap-x-2 py-1">
<MaterialIcons name="error-outline" size={16} color="#ef4444" />
<Text className="text-sm text-red-500 dark:text-red-400">
Failed to fetch usage
</Text>
</View>
)}
{claude.usage && (
<View>
<UsageRow
label="5-hour window"
percent={claude.usage.five_hour.utilization}
resetAtISO={claude.usage.five_hour.resets_at}
/>
<UsageRow
label="7-day window"
percent={claude.usage.seven_day.utilization}
resetAtISO={claude.usage.seven_day.resets_at}
/>
{(claude.usage.seven_day_sonnet ||
claude.usage.seven_day_opus) && (
<View className="pt-3 border-t border-neutral-100 dark:border-neutral-800">
<Text className="text-xs font-semibold uppercase tracking-widest text-neutral-400 mb-3">
By Model (7-day)
</Text>
{claude.usage.seven_day_sonnet && (
<UsageRow
label="Sonnet"
percent={claude.usage.seven_day_sonnet.utilization}
/>
)}
{claude.usage.seven_day_opus && (
<UsageRow
label="Opus"
percent={claude.usage.seven_day_opus.utilization}
/>
)}
</View>
)}
<View className="mt-3 border-t border-neutral-100 pt-3 dark:border-neutral-800">
<LastUpdated timestamp={claude.lastFetchedAt} />
</View>
</View>
)}
</View>
</View>
</ScrollView>
</SafeAreaView>
);
}
export default function DashboardTab() {
return (
<ScreenErrorBoundary screenName="Dashboard">
<DashboardTabContent />
</ScreenErrorBoundary>
);
}