237 lines
6.5 KiB
TypeScript
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',
|
|
},
|
|
}); |