Files
carry-your-live/carry-your-live/src/components/SubtasksSection.tsx
T
2026-08-09 21:42:54 +02:00

237 lines
6.5 KiB
TypeScript

import React, { useState } from 'react';
import { View, Text, StyleSheet, TextInput, TouchableOpacity, Animated } from 'react-native';
import { Controller, Control } from 'react-hook-form';
import { TaskFormData, SubtaskFormValue } from '@/types';
import { useSettings } from '@/theme';
import Svg, { Path } from 'react-native-svg';
interface SubtasksSectionProps {
control: Control<TaskFormData>;
}
function makeKey(): string {
return Math.random().toString(36).slice(2, 10);
}
export function SubtasksSection({ control }: SubtasksSectionProps) {
const { theme } = useSettings();
const [expanded, setExpanded] = useState(false);
return (
<Controller
control={control}
name="subtasks"
render={({ field }) => {
const items: SubtaskFormValue[] = field.value ?? [];
const addSubtask = () => {
field.onChange([...items, { title: '', _key: makeKey() }]);
};
const updateSubtask = (index: number, title: string) => {
const next = items.map((s, i) => (i === index ? { ...s, title } : s));
field.onChange(next);
};
const removeSubtask = (index: number) => {
field.onChange(items.filter((_, i) => i !== index));
};
return (
<View style={styles.container}>
<TouchableOpacity
style={styles.header}
onPress={() => setExpanded(!expanded)}
activeOpacity={0.7}
>
<View style={styles.headerLeft}>
<Text style={[styles.label, { color: theme.text }]}>Subtasks</Text>
{items.length > 0 && (
<View style={[styles.badge, { backgroundColor: theme.accentSoft }]}>
<Text style={[styles.badgeText, { color: theme.accentStrong }]}>{items.length}</Text>
</View>
)}
</View>
<Animated.View
style={[
styles.chevron,
{
transform: [
{
rotate: expanded ? '180deg' : '0deg',
},
],
},
]}
>
<Svg width={16} height={16} viewBox="0 0 24 24">
<Path
d="M6 9l6 6 6-6"
stroke={theme.textSecondary}
strokeWidth={2.5}
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
/>
</Svg>
</Animated.View>
</TouchableOpacity>
<Animated.View
style={[
styles.content,
{
maxHeight: expanded ? items.length * 60 + 80 : 0,
opacity: expanded ? 1 : 0,
},
]}
>
<View style={styles.list}>
{items.map((item, index) => (
<SubtaskItem
key={item._key ?? `subtask-${index}`}
index={index}
value={item.title}
onChange={(value) => updateSubtask(index, value)}
onRemove={() => removeSubtask(index)}
/>
))}
</View>
<TouchableOpacity
style={[styles.addButton, { backgroundColor: theme.accentSoft, borderColor: theme.accentBorder }]}
onPress={addSubtask}
activeOpacity={0.8}
>
<Svg width={16} height={16} viewBox="0 0 24 24">
<Path d="M12 5v14M5 12h14" stroke={theme.accent} strokeWidth={2} strokeLinecap="round" />
</Svg>
<Text style={[styles.addButtonText, { color: theme.accentStrong }]}>Add Subtask</Text>
</TouchableOpacity>
</Animated.View>
</View>
);
}}
/>
);
}
interface SubtaskItemProps {
index: number;
value: string;
onChange: (value: string) => void;
onRemove: () => void;
}
function SubtaskItem({ index, value, onChange, onRemove }: SubtaskItemProps) {
const { theme } = useSettings();
return (
<View style={[styles.item, { backgroundColor: theme.cardAlt, borderColor: theme.border }]}>
<View style={styles.checkbox}>
<Svg width={22} height={22} viewBox="0 0 24 24">
<Path
d="M4 12.5l5 5 10-10"
stroke={theme.textFaint}
strokeWidth={2.2}
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
/>
</Svg>
</View>
<TextInput
style={[styles.input, { color: theme.text }]}
placeholder={`Subtask ${index + 1}`}
placeholderTextColor={theme.textMuted}
value={value}
onChangeText={onChange}
autoCapitalize="sentences"
maxLength={100}
/>
<TouchableOpacity style={styles.removeButton} onPress={onRemove} activeOpacity={0.7}>
<Svg width={20} height={20} viewBox="0 0 24 24">
<Path d="M18 6L6 18M6 6l12 12" stroke={theme.textSecondary} strokeWidth={2.2} strokeLinecap="round" />
</Svg>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: {
gap: 6,
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 2,
},
headerLeft: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
},
label: {
fontSize: 13,
fontWeight: '600',
},
badge: {
paddingHorizontal: 6,
paddingVertical: 1,
borderRadius: 8,
},
badgeText: {
fontSize: 11,
fontWeight: '700',
},
chevron: {
width: 14,
height: 14,
alignItems: 'center',
justifyContent: 'center',
},
content: {
overflow: 'hidden',
},
list: {
gap: 6,
paddingBottom: 6,
},
item: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
paddingHorizontal: 10,
paddingVertical: 8,
borderRadius: 10,
borderWidth: 1,
},
checkbox: {
width: 20,
height: 20,
alignItems: 'center',
justifyContent: 'center',
},
input: {
flex: 1,
fontSize: 14,
paddingVertical: 2,
},
removeButton: {
padding: 3,
},
addButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 5,
paddingVertical: 8,
borderRadius: 10,
borderWidth: 1,
},
addButtonText: {
fontSize: 12,
fontWeight: '500',
},
});