diff options
| author | soryu <soryu@soryu.co> | 2026-01-18 17:44:50 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-18 17:44:50 +0000 |
| commit | 869f21ee2efaefed6a5aa4fbd417c25df8dec02a (patch) | |
| tree | 2a90820ac817173e5b7154e0ba5e4f5d095f9613 /apps/mobile/components/TaskListItem.tsx | |
| parent | 219bca168508e1ea5e91e8a9ce98338afeddfbd2 (diff) | |
| download | soryu-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/components/TaskListItem.tsx')
| -rw-r--r-- | apps/mobile/components/TaskListItem.tsx | 199 |
1 files changed, 199 insertions, 0 deletions
diff --git a/apps/mobile/components/TaskListItem.tsx b/apps/mobile/components/TaskListItem.tsx new file mode 100644 index 0000000..c21f41a --- /dev/null +++ b/apps/mobile/components/TaskListItem.tsx @@ -0,0 +1,199 @@ +import React from 'react'; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + useColorScheme, +} from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { Colors } from '../constants/Colors'; +import { TaskStatusBadge } from './TaskStatusBadge'; +import type { TaskSummary } from '../lib/api'; + +interface TaskListItemProps { + task: TaskSummary; + onPress: (task: TaskSummary) => void; +} + +/** + * Format relative time from a date string + */ +function formatRelativeTime(dateString: string): string { + const date = new Date(dateString); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffSec = Math.floor(diffMs / 1000); + const diffMin = Math.floor(diffSec / 60); + const diffHour = Math.floor(diffMin / 60); + const diffDay = Math.floor(diffHour / 24); + + if (diffSec < 60) return 'just now'; + if (diffMin < 60) return `${diffMin}m ago`; + if (diffHour < 24) return `${diffHour}h ago`; + if (diffDay < 7) return `${diffDay}d ago`; + + return date.toLocaleDateString(); +} + +/** + * Truncate text with ellipsis + */ +function truncate(text: string, maxLength: number): string { + if (text.length <= maxLength) return text; + return text.slice(0, maxLength - 3) + '...'; +} + +export function TaskListItem({ task, onPress }: TaskListItemProps) { + const colorScheme = useColorScheme() ?? 'light'; + const colors = Colors[colorScheme]; + + const isRunning = ['running', 'initializing', 'starting'].includes(task.status); + const isCompleted = ['done', 'merged'].includes(task.status); + const isFailed = task.status === 'failed'; + + return ( + <TouchableOpacity + style={[ + styles.container, + { + backgroundColor: colors.card, + borderColor: colors.border, + }, + ]} + onPress={() => onPress(task)} + activeOpacity={0.7} + > + <View style={styles.leftSection}> + <TaskStatusBadge status={task.status} size="medium" /> + </View> + + <View style={styles.content}> + <View style={styles.header}> + <Text + style={[styles.name, { color: colors.text }]} + numberOfLines={1} + > + {task.name} + </Text> + {task.isSupervisor && ( + <View style={styles.supervisorBadge}> + <Ionicons name="star" size={12} color={colors.tint} /> + </View> + )} + </View> + + {task.contractName && ( + <Text + style={[styles.contractName, { color: colors.secondaryText }]} + numberOfLines={1} + > + {task.contractName} + </Text> + )} + + {task.progressSummary && ( + <Text + style={[styles.progressSummary, { color: colors.secondaryText }]} + numberOfLines={2} + > + {truncate(task.progressSummary, 100)} + </Text> + )} + + <View style={styles.footer}> + <Text style={[styles.time, { color: colors.secondaryText }]}> + {isRunning + ? `Started ${formatRelativeTime(task.updatedAt)}` + : isCompleted + ? `Completed ${formatRelativeTime(task.updatedAt)}` + : isFailed + ? `Failed ${formatRelativeTime(task.updatedAt)}` + : `Created ${formatRelativeTime(task.createdAt)}`} + </Text> + + {task.subtaskCount > 0 && ( + <View style={styles.subtaskBadge}> + <Ionicons + name="git-branch-outline" + size={12} + color={colors.secondaryText} + /> + <Text style={[styles.subtaskCount, { color: colors.secondaryText }]}> + {task.subtaskCount} + </Text> + </View> + )} + </View> + </View> + + <View style={styles.rightSection}> + <Ionicons + name="chevron-forward" + size={20} + color={colors.secondaryText} + /> + </View> + </TouchableOpacity> + ); +} + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 12, + paddingHorizontal: 16, + borderBottomWidth: StyleSheet.hairlineWidth, + minHeight: 64, + }, + leftSection: { + marginRight: 12, + alignItems: 'center', + justifyContent: 'center', + }, + content: { + flex: 1, + gap: 2, + }, + header: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + }, + name: { + fontSize: 16, + fontWeight: '600', + flex: 1, + }, + supervisorBadge: { + padding: 2, + }, + contractName: { + fontSize: 13, + }, + progressSummary: { + fontSize: 13, + marginTop: 2, + }, + footer: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginTop: 4, + }, + time: { + fontSize: 12, + }, + subtaskBadge: { + flexDirection: 'row', + alignItems: 'center', + gap: 4, + }, + subtaskCount: { + fontSize: 12, + }, + rightSection: { + marginLeft: 8, + }, +}); |
