feat: production-ready UI with onboarding wizard and dashboard
- Onboarding flow (welcome -> Codex setup -> Claude setup -> done) with dark-themed screens and step indicators; completion stored in SecureStore - Smart root redirect checks onboarding state on launch - Dashboard tab shows both services at a glance via ServiceStatusCard; useFocusEffect + reload() keeps it fresh when credentials change in tabs - Settings tab manages credentials and exposes re-run setup wizard - Polished Codex and Claude detail screens with service headers and empty states - 4-tab layout (Home, Codex, Claude, Settings) with dark-mode tab bar - Both hooks gain reload() for cross-tab credential refresh Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
import {
|
||||
ScrollView,
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
RefreshControl,
|
||||
} from "react-native";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import { router, useFocusEffect } from "expo-router";
|
||||
import { useCallback } from "react";
|
||||
import { MaterialIcons } from "@expo/vector-icons";
|
||||
import { useCodexUsage } from "@/hooks/useCodexUsage";
|
||||
import { useClaudeUsage } from "@/hooks/useClaudeUsage";
|
||||
import { ServiceStatusCard } from "@/components/ServiceStatusCard";
|
||||
import { windowLabel } from "@/lib/timeUtils";
|
||||
|
||||
export default function DashboardTab() {
|
||||
const codex = useCodexUsage();
|
||||
const claude = useClaudeUsage();
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
codex.reload();
|
||||
claude.reload();
|
||||
}, [])
|
||||
);
|
||||
|
||||
const isRefreshing =
|
||||
codex.status === "loading" || claude.status === "loading";
|
||||
|
||||
const handleRefresh = () => {
|
||||
codex.refresh();
|
||||
claude.refresh();
|
||||
};
|
||||
|
||||
const codexRows =
|
||||
codex.usage
|
||||
? [
|
||||
{
|
||||
label: `Primary (${windowLabel(codex.usage.rate_limit.primary_window.limit_window_seconds)})`,
|
||||
percent: codex.usage.rate_limit.primary_window.used_percent,
|
||||
resetAtSeconds: codex.usage.rate_limit.primary_window.reset_at,
|
||||
},
|
||||
{
|
||||
label: `Secondary (${windowLabel(codex.usage.rate_limit.secondary_window.limit_window_seconds)})`,
|
||||
percent: codex.usage.rate_limit.secondary_window.used_percent,
|
||||
resetAtSeconds: codex.usage.rate_limit.secondary_window.reset_at,
|
||||
},
|
||||
]
|
||||
: undefined;
|
||||
|
||||
const claudeRows =
|
||||
claude.usage
|
||||
? [
|
||||
{
|
||||
label: "5-hour window",
|
||||
percent: claude.usage.five_hour.utilization,
|
||||
resetAtISO: claude.usage.five_hour.resets_at,
|
||||
},
|
||||
{
|
||||
label: "7-day window",
|
||||
percent: claude.usage.seven_day.utilization,
|
||||
resetAtISO: claude.usage.seven_day.resets_at,
|
||||
},
|
||||
]
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
className="flex-1 bg-neutral-50 dark:bg-neutral-950"
|
||||
edges={["top", "bottom"]}
|
||||
>
|
||||
<ScrollView
|
||||
className="flex-1"
|
||||
contentContainerStyle={{ paddingHorizontal: 16, paddingBottom: 32 }}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isRefreshing}
|
||||
onRefresh={handleRefresh}
|
||||
tintColor="#10a37f"
|
||||
/>
|
||||
}
|
||||
>
|
||||
{/* Header */}
|
||||
<View className="flex-row items-center justify-between py-5">
|
||||
<View>
|
||||
<Text className="text-2xl font-bold text-neutral-900 dark:text-white tracking-tight">
|
||||
Codexbar
|
||||
</Text>
|
||||
<Text className="text-xs text-neutral-400 mt-0.5">
|
||||
AI usage monitor
|
||||
</Text>
|
||||
</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="#10a37f" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Codex card */}
|
||||
<ServiceStatusCard
|
||||
title="Codex CLI"
|
||||
icon="auto-awesome"
|
||||
accentColor="#10a37f"
|
||||
status={!codex.auth ? "idle" : codex.status}
|
||||
badge={codex.usage?.plan_type}
|
||||
rows={codexRows}
|
||||
unconfiguredLabel="Import auth.json to see your Codex rate limits."
|
||||
onConfigurePress={() => router.navigate("/(tabs)/codex")}
|
||||
footer={
|
||||
codex.usage && !codex.usage.credits.unlimited && codex.usage.credits.has_credits
|
||||
? (
|
||||
<View className="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="flex-row items-center gap-x-2">
|
||||
<MaterialIcons name="all-inclusive" size={14} color="#10a37f" />
|
||||
<Text className="text-xs text-green-600 dark:text-green-400">
|
||||
Unlimited credits
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
: null
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Claude card */}
|
||||
<ServiceStatusCard
|
||||
title="Claude.ai"
|
||||
icon="psychology"
|
||||
accentColor="#d97706"
|
||||
status={!claude.sessionKey ? "idle" : claude.status}
|
||||
rows={claudeRows}
|
||||
unconfiguredLabel="Add your session key to see Claude usage windows."
|
||||
onConfigurePress={() => router.navigate("/(tabs)/claude")}
|
||||
/>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user