312 lines
8.7 KiB
TypeScript
312 lines
8.7 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
FlatList,
|
|
TouchableOpacity,
|
|
TextInput,
|
|
} from 'react-native';
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
|
import { RootStackParamList } from '../navigation/types';
|
|
import { Glaze } from '../types';
|
|
import { getAllGlazes, searchGlazes } from '../lib/db/repositories';
|
|
import { Button, Card } from '../components';
|
|
import { colors, spacing, typography, borderRadius } from '../lib/theme';
|
|
import { useAuth } from '../contexts/AuthContext';
|
|
|
|
type NavigationProp = NativeStackNavigationProp<RootStackParamList, 'GlazePicker'>;
|
|
type RouteProps = RouteProp<RootStackParamList, 'GlazePicker'>;
|
|
|
|
export const GlazePickerScreen: React.FC = () => {
|
|
const navigation = useNavigation<NavigationProp>();
|
|
const route = useRoute<RouteProps>();
|
|
const { user } = useAuth();
|
|
const { selectedGlazeIds = [] } = route.params || {};
|
|
|
|
const [glazes, setGlazes] = useState<Glaze[]>([]);
|
|
const [selected, setSelected] = useState<string[]>(selectedGlazeIds);
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
loadGlazes();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (searchQuery.trim()) {
|
|
searchGlazesHandler();
|
|
} else {
|
|
loadGlazes();
|
|
}
|
|
}, [searchQuery]);
|
|
|
|
const loadGlazes = async () => {
|
|
if (!user) return;
|
|
|
|
try {
|
|
const all = await getAllGlazes(user.id);
|
|
setGlazes(all);
|
|
} catch (error) {
|
|
console.error('Failed to load glazes:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const searchGlazesHandler = async () => {
|
|
if (!user) return;
|
|
|
|
try {
|
|
const results = await searchGlazes(searchQuery, user.id);
|
|
setGlazes(results);
|
|
} catch (error) {
|
|
console.error('Failed to search glazes:', error);
|
|
}
|
|
};
|
|
|
|
const toggleGlaze = (glazeId: string) => {
|
|
if (selected.includes(glazeId)) {
|
|
setSelected(selected.filter(id => id !== glazeId));
|
|
} else {
|
|
setSelected([...selected, glazeId]);
|
|
}
|
|
};
|
|
|
|
const handleSave = () => {
|
|
// Navigate back to the EXACT StepEditor instance we came from
|
|
// Using the same _editorKey ensures React Navigation finds the correct instance
|
|
console.log('GlazePicker: Navigating back to StepEditor with glazes:', selected);
|
|
|
|
navigation.navigate({
|
|
name: 'StepEditor',
|
|
params: {
|
|
projectId: route.params.projectId,
|
|
stepId: route.params.stepId,
|
|
selectedGlazeIds: selected,
|
|
_editorKey: route.params._editorKey, // Same key = same instance!
|
|
_timestamp: Date.now(),
|
|
},
|
|
merge: true, // Merge params with existing screen instead of creating new one
|
|
} as any);
|
|
};
|
|
|
|
const renderGlaze = ({ item }: { item: Glaze }) => {
|
|
const isSelected = selected.includes(item.id);
|
|
|
|
return (
|
|
<TouchableOpacity onPress={() => toggleGlaze(item.id)}>
|
|
<Card style={[styles.glazeCard, isSelected ? styles.glazeCardSelected : null]}>
|
|
<View style={styles.glazeMainContent}>
|
|
{item.color && (
|
|
<View style={[styles.colorPreview, { backgroundColor: item.color }]} />
|
|
)}
|
|
<View style={styles.glazeInfo}>
|
|
<View style={styles.glazeHeader}>
|
|
<Text style={styles.glazeBrand}>{item.brand}</Text>
|
|
<View style={styles.badges}>
|
|
{item.isMix && <Text style={styles.mixBadge}>Mix</Text>}
|
|
{item.isCustom && !item.isMix && <Text style={styles.customBadge}>Custom</Text>}
|
|
</View>
|
|
</View>
|
|
<Text style={styles.glazeName}>{item.name}</Text>
|
|
{item.code && <Text style={styles.glazeCode}>Code: {item.code}</Text>}
|
|
{item.finish && (
|
|
<Text style={styles.glazeFinish}>
|
|
{item.finish.charAt(0).toUpperCase() + item.finish.slice(1)}
|
|
</Text>
|
|
)}
|
|
{item.mixRatio && (
|
|
<Text style={styles.glazeMixRatio}>Ratio: {item.mixRatio}</Text>
|
|
)}
|
|
</View>
|
|
</View>
|
|
</Card>
|
|
</TouchableOpacity>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top']}>
|
|
<View style={styles.header}>
|
|
<TextInput
|
|
style={styles.searchInput}
|
|
placeholder="Search glazes..."
|
|
value={searchQuery}
|
|
onChangeText={setSearchQuery}
|
|
placeholderTextColor={colors.textSecondary}
|
|
/>
|
|
</View>
|
|
|
|
<Text style={styles.selectedCount}>
|
|
Selected: {selected.length} glaze{selected.length !== 1 ? 's' : ''}
|
|
</Text>
|
|
|
|
<FlatList
|
|
data={glazes}
|
|
renderItem={renderGlaze}
|
|
keyExtractor={(item) => item.id}
|
|
contentContainerStyle={styles.listContent}
|
|
ListEmptyComponent={
|
|
<View style={styles.emptyContainer}>
|
|
<Text style={styles.emptyText}>
|
|
{searchQuery ? 'No glazes found' : 'No glazes in catalog'}
|
|
</Text>
|
|
</View>
|
|
}
|
|
/>
|
|
|
|
<View style={styles.footer}>
|
|
<Button
|
|
title="Mix Glazes"
|
|
onPress={() => navigation.navigate('GlazeMixer', {
|
|
projectId: route.params.projectId,
|
|
stepId: route.params.stepId,
|
|
_editorKey: route.params._editorKey, // Pass editor key through
|
|
})}
|
|
variant="outline"
|
|
style={styles.mixButton}
|
|
/>
|
|
<Button
|
|
title="Save Selection"
|
|
onPress={handleSave}
|
|
disabled={selected.length === 0}
|
|
/>
|
|
</View>
|
|
</SafeAreaView>
|
|
);
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: colors.backgroundSecondary,
|
|
},
|
|
header: {
|
|
paddingHorizontal: spacing.lg,
|
|
paddingTop: spacing.md,
|
|
paddingBottom: spacing.md,
|
|
backgroundColor: colors.background,
|
|
},
|
|
searchInput: {
|
|
backgroundColor: colors.backgroundSecondary,
|
|
borderRadius: borderRadius.md,
|
|
borderWidth: 2,
|
|
borderColor: colors.border,
|
|
paddingHorizontal: spacing.md,
|
|
paddingVertical: spacing.md,
|
|
fontSize: typography.fontSize.md,
|
|
color: colors.text,
|
|
},
|
|
selectedCount: {
|
|
paddingHorizontal: spacing.lg,
|
|
paddingVertical: spacing.sm,
|
|
fontSize: typography.fontSize.sm,
|
|
fontWeight: typography.fontWeight.bold,
|
|
color: colors.textSecondary,
|
|
textTransform: 'uppercase',
|
|
},
|
|
listContent: {
|
|
paddingHorizontal: spacing.md,
|
|
paddingBottom: spacing.xl,
|
|
},
|
|
glazeCard: {
|
|
marginBottom: spacing.md,
|
|
},
|
|
glazeCardSelected: {
|
|
borderWidth: 3,
|
|
borderColor: colors.primary,
|
|
backgroundColor: colors.primaryLight + '20',
|
|
},
|
|
glazeMainContent: {
|
|
flexDirection: 'row',
|
|
gap: spacing.md,
|
|
},
|
|
colorPreview: {
|
|
width: 50,
|
|
height: 50,
|
|
borderRadius: borderRadius.md,
|
|
borderWidth: 2,
|
|
borderColor: colors.border,
|
|
},
|
|
glazeInfo: {
|
|
flex: 1,
|
|
},
|
|
glazeHeader: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
marginBottom: spacing.xs,
|
|
},
|
|
glazeBrand: {
|
|
fontSize: typography.fontSize.sm,
|
|
fontWeight: typography.fontWeight.bold,
|
|
color: colors.primary,
|
|
textTransform: 'uppercase',
|
|
letterSpacing: 0.5,
|
|
},
|
|
badges: {
|
|
flexDirection: 'row',
|
|
gap: spacing.xs,
|
|
},
|
|
customBadge: {
|
|
fontSize: typography.fontSize.xs,
|
|
fontWeight: typography.fontWeight.bold,
|
|
color: colors.background,
|
|
backgroundColor: colors.info,
|
|
paddingHorizontal: spacing.sm,
|
|
paddingVertical: spacing.xs,
|
|
borderRadius: borderRadius.full,
|
|
},
|
|
mixBadge: {
|
|
fontSize: typography.fontSize.xs,
|
|
fontWeight: typography.fontWeight.bold,
|
|
color: colors.background,
|
|
backgroundColor: colors.primary,
|
|
paddingHorizontal: spacing.sm,
|
|
paddingVertical: spacing.xs,
|
|
borderRadius: borderRadius.full,
|
|
},
|
|
glazeName: {
|
|
fontSize: typography.fontSize.md,
|
|
fontWeight: typography.fontWeight.bold,
|
|
color: colors.text,
|
|
marginBottom: spacing.xs,
|
|
},
|
|
glazeCode: {
|
|
fontSize: typography.fontSize.sm,
|
|
color: colors.textSecondary,
|
|
},
|
|
glazeFinish: {
|
|
fontSize: typography.fontSize.sm,
|
|
color: colors.textSecondary,
|
|
fontStyle: 'italic',
|
|
},
|
|
glazeMixRatio: {
|
|
fontSize: typography.fontSize.sm,
|
|
color: colors.textSecondary,
|
|
fontStyle: 'italic',
|
|
},
|
|
emptyContainer: {
|
|
padding: spacing.xl,
|
|
alignItems: 'center',
|
|
},
|
|
emptyText: {
|
|
fontSize: typography.fontSize.md,
|
|
color: colors.textSecondary,
|
|
},
|
|
footer: {
|
|
padding: spacing.md,
|
|
backgroundColor: colors.background,
|
|
borderTopWidth: 2,
|
|
borderTopColor: colors.border,
|
|
flexDirection: 'row',
|
|
gap: spacing.md,
|
|
},
|
|
mixButton: {
|
|
flex: 1,
|
|
},
|
|
});
|