d5b0f9c833
- 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>
125 lines
3.9 KiB
TypeScript
125 lines
3.9 KiB
TypeScript
import { View, Text, TouchableOpacity, ActivityIndicator } from "react-native";
|
|
import { MaterialIcons } from "@expo/vector-icons";
|
|
import { ProgressBar } from "./ProgressBar";
|
|
import { ResetCountdown } from "./ResetCountdown";
|
|
|
|
interface UsageRow {
|
|
label: string;
|
|
percent: number;
|
|
resetAtSeconds?: number;
|
|
resetAtISO?: string;
|
|
}
|
|
|
|
interface ServiceStatusCardProps {
|
|
title: string;
|
|
icon: React.ComponentProps<typeof MaterialIcons>["name"];
|
|
accentColor: string;
|
|
status: "idle" | "loading" | "success" | "error";
|
|
badge?: string;
|
|
rows?: UsageRow[];
|
|
footer?: React.ReactNode;
|
|
unconfiguredLabel?: string;
|
|
onConfigurePress?: () => void;
|
|
}
|
|
|
|
export function ServiceStatusCard({
|
|
title,
|
|
icon,
|
|
accentColor,
|
|
status,
|
|
badge,
|
|
rows,
|
|
footer,
|
|
unconfiguredLabel,
|
|
onConfigurePress,
|
|
}: ServiceStatusCardProps) {
|
|
return (
|
|
<View className="rounded-2xl bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 p-4 mb-4">
|
|
{/* Header */}
|
|
<View className="flex-row items-center justify-between mb-3">
|
|
<View className="flex-row items-center gap-x-2.5">
|
|
<View
|
|
className="w-8 h-8 rounded-xl items-center justify-center"
|
|
style={{ backgroundColor: `${accentColor}20` }}
|
|
>
|
|
<MaterialIcons name={icon} size={16} color={accentColor} />
|
|
</View>
|
|
<Text className="text-sm font-semibold text-neutral-800 dark:text-white">
|
|
{title}
|
|
</Text>
|
|
</View>
|
|
{badge && (
|
|
<View className="px-2.5 py-1 rounded-full bg-green-100 dark:bg-green-900/40">
|
|
<Text className="text-xs font-semibold text-green-700 dark:text-green-300 capitalize">
|
|
{badge}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
{status === "success" && !badge && (
|
|
<View className="w-2 h-2 rounded-full bg-green-500" />
|
|
)}
|
|
{status === "error" && (
|
|
<View className="w-2 h-2 rounded-full bg-red-500" />
|
|
)}
|
|
</View>
|
|
|
|
{/* Body */}
|
|
{status === "loading" && (
|
|
<ActivityIndicator color={accentColor} style={{ marginVertical: 12 }} />
|
|
)}
|
|
|
|
{status === "idle" && (
|
|
<View className="py-2 items-start">
|
|
<Text className="text-sm text-neutral-400 dark:text-neutral-500 mb-3">
|
|
{unconfiguredLabel ?? "Not configured"}
|
|
</Text>
|
|
{onConfigurePress && (
|
|
<TouchableOpacity
|
|
onPress={onConfigurePress}
|
|
className="px-3 py-1.5 rounded-lg border border-neutral-200 dark:border-neutral-700"
|
|
>
|
|
<Text className="text-xs font-semibold text-neutral-600 dark:text-neutral-300">
|
|
Configure →
|
|
</Text>
|
|
</TouchableOpacity>
|
|
)}
|
|
</View>
|
|
)}
|
|
|
|
{status === "error" && (
|
|
<Text className="text-sm text-red-500 dark:text-red-400 py-2">
|
|
Failed to load usage data
|
|
</Text>
|
|
)}
|
|
|
|
{status === "success" && rows && (
|
|
<View>
|
|
{rows.map((row, i) => (
|
|
<View key={i} className={`gap-y-1 ${i < rows.length - 1 ? "mb-3" : ""}`}>
|
|
<View className="flex-row justify-between items-center">
|
|
<Text className="text-xs font-medium text-neutral-500 dark:text-neutral-400">
|
|
{row.label}
|
|
</Text>
|
|
<Text className="text-xs font-semibold text-neutral-700 dark:text-neutral-300">
|
|
{Math.round(row.percent)}%
|
|
</Text>
|
|
</View>
|
|
<ProgressBar percent={row.percent} />
|
|
<ResetCountdown
|
|
resetAtSeconds={row.resetAtSeconds}
|
|
resetAtISO={row.resetAtISO}
|
|
/>
|
|
</View>
|
|
))}
|
|
</View>
|
|
)}
|
|
|
|
{footer && (
|
|
<View className="mt-3 pt-3 border-t border-neutral-100 dark:border-neutral-800">
|
|
{footer}
|
|
</View>
|
|
)}
|
|
</View>
|
|
);
|
|
}
|