gitshark

Clone repository

git clone https://git.vilip.de/git/vilip/taskflow.git
git clone git@git.vilip.de:vilip/taskflow.git
import React, { useEffect, useRef } from 'react';
import { StyleSheet, TouchableOpacity, View, Animated } from 'react-native';
import { Task } from '@/types/gtd';
import { ThemedText } from './themed-text';
import { Colors } from '@/constants/theme';
import { useColorScheme } from '@/hooks/use-color-scheme';

interface TaskItemProps {
  task: Task;
  onPress: () => void;
  onToggleComplete: () => void;
}

export function TaskItem({ task, onPress, onToggleComplete }: TaskItemProps) {
  const colorScheme = useColorScheme() ?? 'light';
  const colors = Colors[colorScheme];
  const fadeAnim = useRef(new Animated.Value(1)).current;
  const scaleAnim = useRef(new Animated.Value(1)).current;
  const strikeAnim = useRef(new Animated.Value(0)).current;
  const [textWidth, setTextWidth] = React.useState(0);

  const priorityColor = task.priority === 'high' ? '#ef4444' : 
                       task.priority === 'medium' ? '#f59e0b' : 
                       colors.subtitle;

  useEffect(() => {
    if (task.completed) {
      let fadeTimeout: ReturnType<typeof setTimeout> | undefined;
      // Animate strikethrough
      const animation = Animated.parallel([
        Animated.timing(strikeAnim, {
          toValue: 1,
          duration: 400,
          useNativeDriver: false,
        }),
        Animated.sequence([
          Animated.timing(scaleAnim, {
            toValue: 0.98,
            duration: 100,
            useNativeDriver: true,
          }),
          Animated.timing(scaleAnim, {
            toValue: 1,
            duration: 100,
            useNativeDriver: true,
          }),
        ])
      ]);
      animation.start(() => {
        // Wait 1 second, then fade out
        fadeTimeout = setTimeout(() => {
          Animated.timing(fadeAnim, {
            toValue: 0,
            duration: 300,
            useNativeDriver: true,
          }).start();
        }, 1000);
      });

      // Cancel the pending animation/timeout if the task is toggled back
      // or the component unmounts, so we never call start() after unmount.
      return () => {
        animation.stop();
        if (fadeTimeout) {
          clearTimeout(fadeTimeout);
        }
      };
    } else {
      // Reset animations when uncompleted
      fadeAnim.setValue(1);
      scaleAnim.setValue(1);
      strikeAnim.setValue(0);
    }
  }, [task.completed, fadeAnim, scaleAnim, strikeAnim]);

  const formatDate = (timestamp?: number) => {
    if (!timestamp) return null;
    const date = new Date(timestamp);
    const today = new Date();
    today.setHours(0, 0, 0, 0);
    const tomorrow = new Date(today);
    tomorrow.setDate(tomorrow.getDate() + 1);
    
    if (date >= today && date < tomorrow) return 'Today';
    if (date >= tomorrow && date < new Date(tomorrow.getTime() + 86400000)) return 'Tomorrow';
    if (date < today) return '⚠️ Overdue';
    
    return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' });
  };

  const strikeWidth = strikeAnim.interpolate({
    inputRange: [0, 1],
    outputRange: [0, textWidth],
  });

  return (
    <Animated.View style={[
      styles.container,
      {
        opacity: fadeAnim,
        transform: [{ scale: scaleAnim }],
      }
    ]}>
      <TouchableOpacity 
        style={[
          styles.checkbox, 
          { borderColor: colors.placeholder },
          task.completed && [styles.checkboxCompleted, { backgroundColor: colors.tint }]
        ]}
        onPress={onToggleComplete}
      >
        {task.completed && <ThemedText style={styles.checkmark}>✓</ThemedText>}
      </TouchableOpacity>
      
      <TouchableOpacity style={styles.content} onPress={onPress}>
        <View style={styles.header}>
          {task.priority && (
            <View style={[styles.priorityBadge, { backgroundColor: priorityColor }]} />
          )}
          <View style={styles.titleWrapper}>
            <View style={styles.titleContainer}>
              <ThemedText 
                style={[
                  styles.title,
                  task.completed && styles.completedText
                ]}
                numberOfLines={1}
                onLayout={(e) => setTextWidth(e.nativeEvent.layout.width)}
              >
                {task.title}
              </ThemedText>
              {task.completed && (
                <Animated.View 
                  style={[
                    styles.strikeLine, 
                    { 
                      width: strikeWidth,
                      backgroundColor: colors.text,
                    }
                  ]} 
                />
              )}
            </View>
          </View>
        </View>
        
        {(task.description || task.dueDate) && (
          <View style={styles.meta}>
            {task.description && (
              <ThemedText style={styles.description} numberOfLines={1}>
                {task.description}
              </ThemedText>
            )}
            {task.dueDate && (
              <ThemedText style={[styles.dueDate, task.dueDate < Date.now() && styles.overdue]}>
                {formatDate(task.dueDate)}
              </ThemedText>
            )}
          </View>
        )}
      </TouchableOpacity>
    </Animated.View>
  );
}

const styles = StyleSheet.create({
  container: {
    flexDirection: 'row',
    alignItems: 'flex-start',
    paddingVertical: 12,
    paddingHorizontal: 16,
    gap: 12,
  },
  checkbox: {
    width: 24,
    height: 24,
    borderRadius: 12,
    borderWidth: 2,
    justifyContent: 'center',
    alignItems: 'center',
    marginTop: 2,
  },
  checkboxCompleted: {
    borderWidth: 0,
  },
  checkmark: {
    color: '#fff',
    fontSize: 16,
    fontWeight: 'bold',
  },
  content: {
    flex: 1,
  },
  header: {
    flexDirection: 'row',
    alignItems: 'center',
    gap: 8,
  },
  priorityBadge: {
    width: 8,
    height: 8,
    borderRadius: 4,
  },
  titleWrapper: {
    flex: 1,
  },
  titleContainer: {
    alignSelf: 'flex-start',
    position: 'relative',
  },
  title: {
    fontSize: 16,
  },
  completedText: {
    opacity: 0.5,
  },
  strikeLine: {
    position: 'absolute',
    top: '50%',
    left: 0,
    height: 2,
    opacity: 0.6,
  },
  meta: {
    marginTop: 4,
    gap: 4,
  },
  description: {
    fontSize: 14,
    opacity: 0.7,
  },
  dueDate: {
    fontSize: 12,
    opacity: 0.6,
  },
  overdue: {
    color: '#ef4444',
    fontWeight: '600',
  },
});

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog