summaryrefslogblamecommitdiff
path: root/apps/mobile/components/TaskListItem.tsx
blob: c21f41a72e699f092d35d89e6628bb992e5329f2 (plain) (tree)






































































































































































































                                                                                   
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,
  },
});