Handle error states and accessibility improvements
This commit is contained in:
@@ -3,20 +3,21 @@ import { StyleSheet, TextInput, View } from 'react-native';
|
||||
import PrimaryButton from '../PrimaryButton';
|
||||
import { colors } from '../../utils/constants';
|
||||
|
||||
export default function ManualSearchInput({ value, onChangeText, onSubmit, placeholder = 'Produktname oder Barcode' }) {
|
||||
export default function ManualSearchInput({ value, onChangeText, onSubmit, disabled = false, placeholder = 'Produktname oder Barcode' }) {
|
||||
return (
|
||||
<View style={styles.row}>
|
||||
<TextInput
|
||||
autoCapitalize="none"
|
||||
onChangeText={onChangeText}
|
||||
onSubmitEditing={onSubmit}
|
||||
editable={!disabled}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={colors.muted}
|
||||
returnKeyType="search"
|
||||
style={styles.input}
|
||||
value={value}
|
||||
/>
|
||||
<PrimaryButton label="Suchen" onPress={onSubmit} />
|
||||
<PrimaryButton disabled={disabled} label="Suchen" onPress={onSubmit} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ export default function AppHeader({ title, subtitle, onBack, rightAction }) {
|
||||
<View style={styles.header}>
|
||||
<View style={styles.row}>
|
||||
{onBack ? (
|
||||
<Pressable accessibilityRole="button" onPress={onBack} style={styles.backButton}>
|
||||
<Pressable accessibilityLabel="Zurück" accessibilityRole="button" onPress={onBack} style={styles.backButton}>
|
||||
<Text style={styles.backText}>‹</Text>
|
||||
</Pressable>
|
||||
) : null}
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function BottomTabBar({ activeTab, onChange }) {
|
||||
const isActive = activeTab === tab.id;
|
||||
return (
|
||||
<Pressable
|
||||
accessibilityLabel={`${tab.label} öffnen`}
|
||||
accessibilityRole="button"
|
||||
key={tab.id}
|
||||
onPress={() => onChange(tab.id)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
import PrimaryButton from '../components/PrimaryButton';
|
||||
import AppHeader from '../components/layout/AppHeader';
|
||||
@@ -8,7 +8,7 @@ import ProductCard from '../components/product/ProductCard';
|
||||
import { calculateProductRating } from '../services/ratingService';
|
||||
import { colors } from '../utils/constants';
|
||||
|
||||
export default function HomeScreen({ preferences, recentProducts, onStartScan, onOpenProduct }) {
|
||||
export default function HomeScreen({ preferences, recentProducts, shoppingList, onStartScan, onOpenProduct, onOpenList }) {
|
||||
const frequentProducts = [...recentProducts]
|
||||
.filter((product) => (product.scanCount || 0) > 1)
|
||||
.sort((a, b) => (b.scanCount || 0) - (a.scanCount || 0))
|
||||
@@ -28,6 +28,14 @@ export default function HomeScreen({ preferences, recentProducts, onStartScan, o
|
||||
Prüfe Allergien, Ernährung, Budget und Nachhaltigkeit direkt am Regal.
|
||||
</Text>
|
||||
<PrimaryButton label="Produkt scannen" onPress={onStartScan} />
|
||||
{shoppingList?.items?.length > 0 ? (
|
||||
<Pressable accessibilityRole="button" onPress={onOpenList} style={styles.listShortcut}>
|
||||
<Text style={styles.listShortcutTitle}>Einkaufsliste</Text>
|
||||
<Text style={styles.listShortcutCopy}>
|
||||
{shoppingList.items.filter((item) => !item.checked).length} offene Produkte
|
||||
</Text>
|
||||
</Pressable>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{frequentProducts.length > 0 ? (
|
||||
@@ -119,5 +127,20 @@ const styles = StyleSheet.create({
|
||||
fontSize: 14,
|
||||
lineHeight: 20,
|
||||
marginTop: 4
|
||||
},
|
||||
listShortcut: {
|
||||
backgroundColor: '#eaf4ef',
|
||||
borderRadius: 8,
|
||||
gap: 2,
|
||||
padding: 12
|
||||
},
|
||||
listShortcutTitle: {
|
||||
color: colors.primaryDark,
|
||||
fontSize: 15,
|
||||
fontWeight: '800'
|
||||
},
|
||||
listShortcutCopy: {
|
||||
color: colors.muted,
|
||||
fontSize: 13
|
||||
}
|
||||
});
|
||||
|
||||
@@ -30,9 +30,15 @@ export default function ScanScreen({ preferences, productCache, onScanBarcode, o
|
||||
|
||||
setIsScanning(false);
|
||||
setIsLoadingProduct(true);
|
||||
const result = await onScanBarcode(event.data).finally(() => {
|
||||
let result;
|
||||
|
||||
try {
|
||||
result = await onScanBarcode(event.data);
|
||||
} catch (error) {
|
||||
result = { status: 'error', errorMessage: 'Das Produkt konnte gerade nicht geladen werden.' };
|
||||
} finally {
|
||||
setIsLoadingProduct(false);
|
||||
});
|
||||
}
|
||||
|
||||
if (result?.status !== 'found') {
|
||||
setScanError(result?.errorMessage || 'Der Barcode konnte nicht verarbeitet werden.');
|
||||
@@ -41,10 +47,15 @@ export default function ScanScreen({ preferences, productCache, onScanBarcode, o
|
||||
}
|
||||
|
||||
async function handleManualSearch() {
|
||||
if (!manualSearchQuery.trim() || isSearching) return;
|
||||
|
||||
setIsSearching(true);
|
||||
const results = await searchProducts(manualSearchQuery, productCache, preferences.offlineModeEnabled);
|
||||
setManualResults(results);
|
||||
setIsSearching(false);
|
||||
try {
|
||||
const results = await searchProducts(manualSearchQuery, productCache, preferences.offlineModeEnabled);
|
||||
setManualResults(results);
|
||||
} finally {
|
||||
setIsSearching(false);
|
||||
}
|
||||
}
|
||||
|
||||
function retryScan() {
|
||||
@@ -98,6 +109,7 @@ export default function ScanScreen({ preferences, productCache, onScanBarcode, o
|
||||
<ManualSearchInput
|
||||
onChangeText={setManualSearchQuery}
|
||||
onSubmit={handleManualSearch}
|
||||
disabled={isSearching}
|
||||
value={manualSearchQuery}
|
||||
/>
|
||||
{isSearching ? <Text style={styles.note}>Suche läuft...</Text> : null}
|
||||
|
||||
@@ -33,7 +33,7 @@ export default function ShoppingListScreen({
|
||||
<View style={styles.summary}>
|
||||
<Text style={styles.summaryTitle}>{shoppingList.title}</Text>
|
||||
<Text style={styles.summaryCopy}>
|
||||
Produkte abhaken, löschen oder nach dem Einkauf abschließen.
|
||||
Produkte abhaken, löschen oder nach dem Einkauf abschliessen.
|
||||
</Text>
|
||||
<View style={styles.budgetBox}>
|
||||
<View style={styles.budgetHeader}>
|
||||
@@ -95,7 +95,7 @@ export default function ShoppingListScreen({
|
||||
)}
|
||||
|
||||
{shoppingList.items.length > 0 ? (
|
||||
<SecondaryButton label="Einkauf abschließen" onPress={onCompleteList} />
|
||||
<SecondaryButton label="Einkauf abschliessen" onPress={onCompleteList} />
|
||||
) : null}
|
||||
</ScreenContainer>
|
||||
);
|
||||
|
||||
@@ -9,12 +9,24 @@ const keys = {
|
||||
};
|
||||
|
||||
async function readJson(key, fallbackValue) {
|
||||
const rawValue = await AsyncStorage.getItem(key);
|
||||
return rawValue ? JSON.parse(rawValue) : fallbackValue;
|
||||
try {
|
||||
const rawValue = await AsyncStorage.getItem(key);
|
||||
return rawValue ? JSON.parse(rawValue) : fallbackValue;
|
||||
} catch (error) {
|
||||
// A damaged value should not prevent the app from starting. The next save
|
||||
// will replace it with a valid value.
|
||||
console.warn(`Shoppy konnte ${key} nicht laden.`, error);
|
||||
return fallbackValue;
|
||||
}
|
||||
}
|
||||
|
||||
async function writeJson(key, value) {
|
||||
await AsyncStorage.setItem(key, JSON.stringify(value));
|
||||
try {
|
||||
await AsyncStorage.setItem(key, JSON.stringify(value));
|
||||
} catch (error) {
|
||||
// Storage is best-effort; the in-memory state remains usable.
|
||||
console.warn(`Shoppy konnte ${key} nicht speichern.`, error);
|
||||
}
|
||||
}
|
||||
|
||||
export const storageService = {
|
||||
|
||||
Reference in New Issue
Block a user