@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, ScrollView } from 'react-native';
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Modal, Pressable, KeyboardAvoidingView } from 'react-native';
|
||||
import { useCategories } from '@/hooks/useDatabase';
|
||||
import { useSettings } from '@/theme';
|
||||
|
||||
@@ -12,82 +12,109 @@ interface CategorySelectorProps {
|
||||
export function CategorySelector({ value, onChange, error }: CategorySelectorProps) {
|
||||
const categories = useCategories();
|
||||
const { theme } = useSettings();
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
const selectedCategory = categories.find(c => c.id === value);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={[styles.label, { color: theme.text }]}>Category</Text>
|
||||
<View style={styles.requiredIndicator} />
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
style={styles.scrollView}
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.selectorButton,
|
||||
{ backgroundColor: theme.inputBg, borderColor: theme.borderStrong },
|
||||
]}
|
||||
onPress={() => setShowModal(true)}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
{categories.map((category) => (
|
||||
<TouchableOpacity
|
||||
key={category.id}
|
||||
style={[
|
||||
styles.categoryButton,
|
||||
{ backgroundColor: theme.card, borderColor: theme.borderStrong },
|
||||
value === category.id && { borderColor: theme.accent, backgroundColor: theme.accentSoft, borderWidth: 2 },
|
||||
]}
|
||||
onPress={() => onChange(category.id)}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.colorCircle,
|
||||
{ backgroundColor: category.color },
|
||||
value === category.id && styles.colorCircleSelected,
|
||||
]}
|
||||
/>
|
||||
<Text style={[
|
||||
styles.categoryName,
|
||||
{ color: theme.textSecondary },
|
||||
value === category.id && { color: theme.accent, fontWeight: '600' },
|
||||
]}>
|
||||
{category.name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
{error && <Text style={styles.errorText}>{error}</Text>}
|
||||
<View style={styles.selectorContent}>
|
||||
<View style={styles.selectorRow}>
|
||||
<View style={[styles.colorCircle, { backgroundColor: selectedCategory?.color || '#9E9E9E' }]} />
|
||||
<Text style={[styles.selectorValue, { color: theme.text }]}>{selectedCategory?.name || 'Select category'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Svg width={20} height={20} viewBox="0 0 24 24">
|
||||
<Path d="M9 18l6-6-6-6" stroke={theme.textMuted} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
||||
</Svg>
|
||||
</TouchableOpacity>
|
||||
{error && <Text style={[styles.errorText, { color: '#E53935' }]}>{error}</Text>}
|
||||
|
||||
<Modal visible={showModal} transparent animationType="fade" onRequestClose={() => setShowModal(false)}>
|
||||
<KeyboardAvoidingView
|
||||
style={[styles.modalOverlay, { backgroundColor: 'rgba(0,0,0,0.5)' }]}
|
||||
behavior="padding"
|
||||
>
|
||||
<Pressable style={styles.modalOverlay} onPress={() => setShowModal(false)}>
|
||||
<Pressable style={[styles.modalSheet, { backgroundColor: theme.sheetBg }]}>
|
||||
<View style={styles.modalHeader}>
|
||||
<Text style={[styles.modalTitle, { color: theme.text }]}>Select Category</Text>
|
||||
<TouchableOpacity onPress={() => setShowModal(false)} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
|
||||
<Text style={[styles.closeText, { color: theme.textMuted }]}>✕</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<ScrollView contentContainerStyle={styles.modalContent}>
|
||||
{categories.map((category) => (
|
||||
<TouchableOpacity
|
||||
key={category.id}
|
||||
style={[
|
||||
styles.modalOption,
|
||||
{ backgroundColor: theme.card, borderColor: theme.borderStrong },
|
||||
value === category.id && { borderColor: theme.accent, backgroundColor: theme.accentSoft, borderWidth: 2 },
|
||||
]}
|
||||
onPress={() => { onChange(category.id); setShowModal(false); }}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<View style={[styles.colorCircle, { backgroundColor: category.color }, value === category.id && styles.colorCircleSelected]} />
|
||||
<Text style={[
|
||||
styles.categoryName,
|
||||
{ color: theme.textSecondary },
|
||||
value === category.id && { color: theme.accent, fontWeight: '600' },
|
||||
]}>
|
||||
{category.name}
|
||||
</Text>
|
||||
{value === category.id && (
|
||||
<Svg width={20} height={20} viewBox="0 0 24 24">
|
||||
<Path d="M5 12l5 5 9-10" stroke={theme.accent} strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
||||
</Svg>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
</KeyboardAvoidingView>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
import Svg, { Path } from 'react-native-svg';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
gap: 8,
|
||||
gap: 6,
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
},
|
||||
requiredIndicator: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
color: '#E53935',
|
||||
fontSize: 14,
|
||||
},
|
||||
scrollView: {
|
||||
paddingVertical: 4,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingHorizontal: 16,
|
||||
gap: 10,
|
||||
},
|
||||
categoryButton: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 24,
|
||||
borderWidth: 1,
|
||||
selectorButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
minWidth: 90,
|
||||
justifyContent: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
minHeight: 52,
|
||||
},
|
||||
selectorContent: {
|
||||
flex: 1,
|
||||
},
|
||||
selectorRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
},
|
||||
colorCircle: {
|
||||
width: 12,
|
||||
@@ -99,13 +126,59 @@ const styles = StyleSheet.create({
|
||||
height: 14,
|
||||
borderRadius: 7,
|
||||
},
|
||||
categoryName: {
|
||||
fontSize: 13,
|
||||
selectorValue: {
|
||||
fontSize: 15,
|
||||
fontWeight: '500',
|
||||
},
|
||||
errorText: {
|
||||
fontSize: 12,
|
||||
color: '#E53935',
|
||||
marginLeft: 16,
|
||||
marginLeft: 4,
|
||||
},
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 24,
|
||||
},
|
||||
modalSheet: {
|
||||
width: '100%',
|
||||
maxWidth: 400,
|
||||
borderRadius: 20,
|
||||
overflow: 'hidden',
|
||||
maxHeight: '80%',
|
||||
},
|
||||
modalHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: 20,
|
||||
borderBottomWidth: 1,
|
||||
},
|
||||
modalTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
},
|
||||
closeText: {
|
||||
fontSize: 22,
|
||||
fontWeight: '300',
|
||||
},
|
||||
modalContent: {
|
||||
padding: 12,
|
||||
gap: 8,
|
||||
},
|
||||
modalOption: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 12,
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 16,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
},
|
||||
categoryName: {
|
||||
fontSize: 15,
|
||||
fontWeight: '500',
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user