[skip ci] Initial commit — CodexBar Mobile Expo app
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
import { View, Text } from "react-native";
|
||||
|
||||
const ERROR_MESSAGES: Record<string, string> = {
|
||||
TOKEN_EXPIRED: "Your session token has expired. Please reconfigure.",
|
||||
MISSING_ACCESS_TOKEN: "auth.json is missing the accessToken field.",
|
||||
INVALID_JSON: "The selected file is not valid JSON.",
|
||||
NO_ORGS_FOUND: "No Claude organizations found for this session key.",
|
||||
};
|
||||
|
||||
function humanize(code: string): string {
|
||||
if (code.startsWith("HTTP_ERROR_")) {
|
||||
return `Unexpected server error (HTTP ${code.replace("HTTP_ERROR_", "")}).`;
|
||||
}
|
||||
return ERROR_MESSAGES[code] ?? `Unexpected error: ${code}`;
|
||||
}
|
||||
|
||||
interface ErrorMessageProps {
|
||||
message: string | null;
|
||||
}
|
||||
|
||||
export function ErrorMessage({ message }: ErrorMessageProps) {
|
||||
if (!message) return null;
|
||||
|
||||
return (
|
||||
<View className="bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-xl px-3 py-2.5 mb-3">
|
||||
<Text className="text-sm text-red-700 dark:text-red-300">{humanize(message)}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { View } from "react-native";
|
||||
|
||||
interface ProgressBarProps {
|
||||
percent: number;
|
||||
}
|
||||
|
||||
function getBarColor(percent: number): string {
|
||||
if (percent >= 85) return "bg-red-500";
|
||||
if (percent >= 60) return "bg-yellow-400";
|
||||
return "bg-green-500";
|
||||
}
|
||||
|
||||
export function ProgressBar({ percent }: ProgressBarProps) {
|
||||
const clamped = Math.max(0, Math.min(100, percent));
|
||||
return (
|
||||
<View className="h-2.5 w-full rounded-full bg-neutral-200 dark:bg-neutral-700">
|
||||
<View
|
||||
className={`h-2.5 rounded-full ${getBarColor(clamped)}`}
|
||||
style={{ width: `${clamped}%` }}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Text } from "react-native";
|
||||
import { formatResetCountdown, formatResetCountdownISO } from "@/lib/timeUtils";
|
||||
|
||||
interface ResetCountdownProps {
|
||||
resetAtSeconds?: number;
|
||||
resetAtISO?: string;
|
||||
}
|
||||
|
||||
export function ResetCountdown({ resetAtSeconds, resetAtISO }: ResetCountdownProps) {
|
||||
const getLabel = () => {
|
||||
if (resetAtSeconds !== undefined) return formatResetCountdown(resetAtSeconds);
|
||||
if (resetAtISO) return formatResetCountdownISO(resetAtISO);
|
||||
return "";
|
||||
};
|
||||
|
||||
const [label, setLabel] = useState(getLabel);
|
||||
|
||||
useEffect(() => {
|
||||
setLabel(getLabel());
|
||||
const interval = setInterval(() => setLabel(getLabel()), 60_000);
|
||||
return () => clearInterval(interval);
|
||||
}, [resetAtSeconds, resetAtISO]);
|
||||
|
||||
if (!label) return null;
|
||||
|
||||
return (
|
||||
<Text className="text-xs text-neutral-500 dark:text-neutral-400">{label}</Text>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { View, Text } from "react-native";
|
||||
|
||||
interface SectionCardProps {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
accentClass?: string;
|
||||
}
|
||||
|
||||
export function SectionCard({ title, children, accentClass }: SectionCardProps) {
|
||||
return (
|
||||
<View
|
||||
className={`rounded-2xl bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-700 p-4 mb-4 ${accentClass ?? ""}`}
|
||||
>
|
||||
<Text className="text-xs font-semibold uppercase tracking-widest text-neutral-500 dark:text-neutral-400 mb-3">
|
||||
{title}
|
||||
</Text>
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { View, Text } from "react-native";
|
||||
import { ProgressBar } from "./ProgressBar";
|
||||
import { ResetCountdown } from "./ResetCountdown";
|
||||
|
||||
interface UsageStatProps {
|
||||
label: string;
|
||||
percent: number;
|
||||
resetAtSeconds?: number;
|
||||
resetAtISO?: string;
|
||||
}
|
||||
|
||||
export function UsageStat({ label, percent, resetAtSeconds, resetAtISO }: UsageStatProps) {
|
||||
return (
|
||||
<View className="gap-y-1.5 mb-4">
|
||||
<View className="flex-row justify-between items-center">
|
||||
<Text className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||
{label}
|
||||
</Text>
|
||||
<Text className="text-sm font-semibold text-neutral-900 dark:text-white">
|
||||
{Math.round(percent)}%
|
||||
</Text>
|
||||
</View>
|
||||
<ProgressBar percent={percent} />
|
||||
<ResetCountdown resetAtSeconds={resetAtSeconds} resetAtISO={resetAtISO} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user