summaryrefslogtreecommitdiff
path: root/apps/mobile/app/(tabs)/index.tsx
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-18 17:44:50 +0000
committerGitHub <noreply@github.com>2026-01-18 17:44:50 +0000
commit869f21ee2efaefed6a5aa4fbd417c25df8dec02a (patch)
tree2a90820ac817173e5b7154e0ba5e4f5d095f9613 /apps/mobile/app/(tabs)/index.tsx
parent219bca168508e1ea5e91e8a9ce98338afeddfbd2 (diff)
downloadsoryu-869f21ee2efaefed6a5aa4fbd417c25df8dec02a.tar.gz
soryu-869f21ee2efaefed6a5aa4fbd417c25df8dec02a.zip
Add React Native mobile app for Makima (#3)
* [WIP] Heartbeat checkpoint - 2026-01-18 02:58:27 UTC * feat(mobile): complete mobile app integration and verification - Add ThemeColors type export to Colors.ts for type safety - Export SUPABASE_URL from supabase.ts and use environment variables - Update .env.example with correct default URLs - Add comprehensive README.md with setup instructions Verified: - TypeScript compiles without errors - App exports successfully for iOS and Android - All screens accessible (login, dashboard, tasks, settings, task detail) - Auth flow working with Zustand store and Supabase Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Task completion checkpoint --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'apps/mobile/app/(tabs)/index.tsx')
-rw-r--r--apps/mobile/app/(tabs)/index.tsx365
1 files changed, 365 insertions, 0 deletions
diff --git a/apps/mobile/app/(tabs)/index.tsx b/apps/mobile/app/(tabs)/index.tsx
new file mode 100644
index 0000000..96b1a8f
--- /dev/null
+++ b/apps/mobile/app/(tabs)/index.tsx
@@ -0,0 +1,365 @@
+import React from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ ScrollView,
+ TouchableOpacity,
+ useColorScheme,
+ RefreshControl,
+} from 'react-native';
+import { useRouter } from 'expo-router';
+import { Ionicons } from '@expo/vector-icons';
+import { Colors, TaskStatusColors } from '../../constants/Colors';
+import { useTasks, getTaskCounts } from '../../hooks/useTasks';
+import { usePendingQuestions } from '../../hooks/useQuestions';
+import { TaskStatusBadge } from '../../components/TaskStatusBadge';
+import type { TaskSummary } from '../../lib/api';
+
+interface StatCardProps {
+ title: string;
+ value: number;
+ icon: keyof typeof Ionicons.glyphMap;
+ color: string;
+ onPress?: () => void;
+}
+
+function StatCard({ title, value, icon, color, onPress }: StatCardProps) {
+ const colorScheme = useColorScheme() ?? 'light';
+ const colors = Colors[colorScheme];
+
+ return (
+ <TouchableOpacity
+ style={[styles.statCard, { backgroundColor: colors.card }]}
+ onPress={onPress}
+ activeOpacity={onPress ? 0.7 : 1}
+ disabled={!onPress}
+ >
+ <View style={[styles.iconContainer, { backgroundColor: color + '20' }]}>
+ <Ionicons name={icon} size={24} color={color} />
+ </View>
+ <Text style={[styles.statValue, { color: colors.text }]}>{value}</Text>
+ <Text style={[styles.statTitle, { color: colors.secondaryText }]}>
+ {title}
+ </Text>
+ </TouchableOpacity>
+ );
+}
+
+interface QuickTaskItemProps {
+ task: TaskSummary;
+ onPress: () => void;
+}
+
+function QuickTaskItem({ task, onPress }: QuickTaskItemProps) {
+ const colorScheme = useColorScheme() ?? 'light';
+ const colors = Colors[colorScheme];
+
+ return (
+ <TouchableOpacity
+ style={[styles.quickTaskItem, { backgroundColor: colors.card }]}
+ onPress={onPress}
+ activeOpacity={0.7}
+ >
+ <TaskStatusBadge status={task.status} size="small" />
+ <View style={styles.quickTaskContent}>
+ <Text
+ style={[styles.quickTaskName, { color: colors.text }]}
+ numberOfLines={1}
+ >
+ {task.name}
+ </Text>
+ {task.progressSummary && (
+ <Text
+ style={[styles.quickTaskSummary, { color: colors.secondaryText }]}
+ numberOfLines={1}
+ >
+ {task.progressSummary}
+ </Text>
+ )}
+ </View>
+ <Ionicons name="chevron-forward" size={16} color={colors.secondaryText} />
+ </TouchableOpacity>
+ );
+}
+
+export default function DashboardScreen() {
+ const colorScheme = useColorScheme() ?? 'light';
+ const colors = Colors[colorScheme];
+ const router = useRouter();
+
+ const {
+ data: tasks,
+ isLoading: isLoadingTasks,
+ refetch: refetchTasks,
+ isRefetching: isRefetchingTasks,
+ } = useTasks();
+
+ const {
+ data: questions,
+ isLoading: isLoadingQuestions,
+ refetch: refetchQuestions,
+ isRefetching: isRefetchingQuestions,
+ } = usePendingQuestions();
+
+ const isRefreshing = isRefetchingTasks || isRefetchingQuestions;
+
+ const handleRefresh = () => {
+ refetchTasks();
+ refetchQuestions();
+ };
+
+ // Calculate counts
+ const counts = tasks ? getTaskCounts(tasks) : null;
+ const questionCount = questions?.length ?? 0;
+
+ // Get running tasks for quick access
+ const runningTasks = tasks?.filter((t) =>
+ ['running', 'initializing', 'starting'].includes(t.status)
+ ) ?? [];
+
+ // Get tasks needing attention (blocked, paused, or with questions)
+ const attentionTasks = tasks?.filter((t) =>
+ ['blocked', 'paused'].includes(t.status)
+ ) ?? [];
+
+ return (
+ <ScrollView
+ style={[styles.container, { backgroundColor: colors.background }]}
+ contentContainerStyle={styles.content}
+ refreshControl={
+ <RefreshControl
+ refreshing={isRefreshing}
+ onRefresh={handleRefresh}
+ tintColor={colors.tint}
+ />
+ }
+ >
+ {/* Stats Grid */}
+ <View style={styles.statsGrid}>
+ <StatCard
+ title="Running"
+ value={counts?.running ?? 0}
+ icon="play-circle"
+ color={TaskStatusColors.running.dot}
+ onPress={() => router.push('/(tabs)/tasks')}
+ />
+ <StatCard
+ title="Questions"
+ value={questionCount}
+ icon="help-circle"
+ color="#f59e0b"
+ onPress={questionCount > 0 ? () => router.push('/(tabs)/tasks') : undefined}
+ />
+ <StatCard
+ title="Completed"
+ value={counts?.completed ?? 0}
+ icon="checkmark-circle"
+ color={TaskStatusColors.done.dot}
+ onPress={() => router.push('/(tabs)/tasks')}
+ />
+ <StatCard
+ title="Failed"
+ value={counts?.failed ?? 0}
+ icon="alert-circle"
+ color={TaskStatusColors.failed.dot}
+ onPress={counts?.failed ? () => router.push('/(tabs)/tasks') : undefined}
+ />
+ </View>
+
+ {/* Pending Questions Alert */}
+ {questionCount > 0 && (
+ <TouchableOpacity
+ style={[styles.alertBanner, { backgroundColor: '#fef3c7' }]}
+ onPress={() => router.push('/(tabs)/tasks')}
+ activeOpacity={0.7}
+ >
+ <Ionicons name="help-circle" size={24} color="#92400e" />
+ <View style={styles.alertContent}>
+ <Text style={[styles.alertTitle, { color: '#92400e' }]}>
+ {questionCount} Question{questionCount !== 1 ? 's' : ''} Waiting
+ </Text>
+ <Text style={[styles.alertSubtitle, { color: '#b45309' }]}>
+ Tap to review and respond
+ </Text>
+ </View>
+ <Ionicons name="chevron-forward" size={20} color="#92400e" />
+ </TouchableOpacity>
+ )}
+
+ {/* Running Tasks */}
+ {runningTasks.length > 0 && (
+ <View style={styles.section}>
+ <View style={styles.sectionHeader}>
+ <Text style={[styles.sectionTitle, { color: colors.text }]}>
+ Running Tasks
+ </Text>
+ <TouchableOpacity onPress={() => router.push('/(tabs)/tasks')}>
+ <Text style={[styles.seeAllButton, { color: colors.tint }]}>
+ See All
+ </Text>
+ </TouchableOpacity>
+ </View>
+ <View style={styles.taskList}>
+ {runningTasks.slice(0, 3).map((task) => (
+ <QuickTaskItem
+ key={task.id}
+ task={task}
+ onPress={() => router.push(`/task/${task.id}`)}
+ />
+ ))}
+ </View>
+ </View>
+ )}
+
+ {/* Needs Attention */}
+ {attentionTasks.length > 0 && (
+ <View style={styles.section}>
+ <View style={styles.sectionHeader}>
+ <Text style={[styles.sectionTitle, { color: colors.text }]}>
+ Needs Attention
+ </Text>
+ <TouchableOpacity onPress={() => router.push('/(tabs)/tasks')}>
+ <Text style={[styles.seeAllButton, { color: colors.tint }]}>
+ See All
+ </Text>
+ </TouchableOpacity>
+ </View>
+ <View style={styles.taskList}>
+ {attentionTasks.slice(0, 3).map((task) => (
+ <QuickTaskItem
+ key={task.id}
+ task={task}
+ onPress={() => router.push(`/task/${task.id}`)}
+ />
+ ))}
+ </View>
+ </View>
+ )}
+
+ {/* Empty State */}
+ {!isLoadingTasks && (!tasks || tasks.length === 0) && (
+ <View style={styles.emptyState}>
+ <Ionicons
+ name="cube-outline"
+ size={64}
+ color={colors.secondaryText}
+ />
+ <Text style={[styles.emptyTitle, { color: colors.text }]}>
+ No tasks yet
+ </Text>
+ <Text style={[styles.emptyMessage, { color: colors.secondaryText }]}>
+ Tasks created from contracts will appear here
+ </Text>
+ </View>
+ )}
+ </ScrollView>
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ content: {
+ padding: 16,
+ gap: 20,
+ },
+ statsGrid: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ gap: 12,
+ },
+ statCard: {
+ flex: 1,
+ minWidth: '45%',
+ padding: 16,
+ borderRadius: 12,
+ alignItems: 'center',
+ gap: 8,
+ },
+ iconContainer: {
+ width: 48,
+ height: 48,
+ borderRadius: 24,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ statValue: {
+ fontSize: 28,
+ fontWeight: '700',
+ },
+ statTitle: {
+ fontSize: 14,
+ fontWeight: '500',
+ },
+ alertBanner: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ padding: 16,
+ borderRadius: 12,
+ gap: 12,
+ },
+ alertContent: {
+ flex: 1,
+ },
+ alertTitle: {
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ alertSubtitle: {
+ fontSize: 13,
+ marginTop: 2,
+ },
+ section: {
+ gap: 12,
+ },
+ sectionHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ },
+ sectionTitle: {
+ fontSize: 18,
+ fontWeight: '600',
+ },
+ seeAllButton: {
+ fontSize: 14,
+ fontWeight: '500',
+ },
+ taskList: {
+ gap: 8,
+ },
+ quickTaskItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ padding: 12,
+ borderRadius: 10,
+ gap: 12,
+ },
+ quickTaskContent: {
+ flex: 1,
+ gap: 2,
+ },
+ quickTaskName: {
+ fontSize: 15,
+ fontWeight: '500',
+ },
+ quickTaskSummary: {
+ fontSize: 13,
+ },
+ emptyState: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingVertical: 48,
+ gap: 12,
+ },
+ emptyTitle: {
+ fontSize: 18,
+ fontWeight: '600',
+ },
+ emptyMessage: {
+ fontSize: 14,
+ textAlign: 'center',
+ },
+});