Update styling and accessibility, refine product and shopping list functionalities

This commit is contained in:
2026-10-16 15:49:22 +02:00
parent 6938afb26c
commit defc1fbc13
24 changed files with 484 additions and 162 deletions
+13 -4
View File
@@ -2,7 +2,7 @@ import { Pressable, StyleSheet, Text } from 'react-native';
import { colors } from '../utils/constants';
export default function PrimaryButton({ label, onPress, disabled = false }) {
export default function PrimaryButton({ label, onPress, disabled = false, tone = 'default' }) {
return (
<Pressable
accessibilityRole="button"
@@ -10,11 +10,12 @@ export default function PrimaryButton({ label, onPress, disabled = false }) {
onPress={onPress}
style={({ pressed }) => [
styles.button,
tone === 'light' ? styles.lightButton : null,
pressed ? styles.buttonPressed : null,
disabled ? styles.disabled : null
]}
>
<Text style={[styles.label, disabled ? styles.disabledLabel : null]}>{label}</Text>
<Text style={[styles.label, tone === 'light' ? styles.lightLabel : null, disabled ? styles.disabledLabel : null]}>{label}</Text>
</Pressable>
);
}
@@ -23,17 +24,25 @@ const styles = StyleSheet.create({
button: {
alignItems: 'center',
backgroundColor: colors.primary,
borderRadius: 8,
borderRadius: 14,
justifyContent: 'center',
minHeight: 50,
paddingHorizontal: 20,
paddingVertical: 14
},
buttonPressed: {
opacity: 0.82
},
lightButton: {
backgroundColor: colors.surface
},
label: {
color: colors.surface,
fontSize: 16,
fontWeight: '700'
fontWeight: '800'
},
lightLabel: {
color: colors.primaryDark
},
disabled: {
backgroundColor: colors.border
+3 -1
View File
@@ -23,8 +23,10 @@ const styles = StyleSheet.create({
button: {
alignItems: 'center',
borderColor: colors.primary,
borderRadius: 8,
borderRadius: 14,
borderWidth: 1,
justifyContent: 'center',
minHeight: 48,
paddingHorizontal: 18,
paddingVertical: 12
},
+25 -6
View File
@@ -1,3 +1,4 @@
import { useState } from 'react';
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
import { budgetPeriodLabels, colors } from '../../utils/constants';
@@ -5,16 +6,28 @@ import { budgetPeriodLabels, colors } from '../../utils/constants';
const budgetPeriods = ['day', 'week', 'month', 'shoppingTrip'];
export default function BudgetInput({ value, currency, period, onChangePeriod, onChangeValue }) {
const [inputValue, setInputValue] = useState(String(value ?? ''));
function updateValue(text) {
setInputValue(text);
const parsedValue = Number(text.replace(',', '.'));
onChangeValue(text.trim() && Number.isFinite(parsedValue) && parsedValue > 0 ? parsedValue : undefined);
}
return (
<View style={styles.wrap}>
<Text style={styles.label}>Maximales Einkaufsbudget</Text>
<Text style={styles.label}>Dein Budget (optional)</Text>
<Text style={styles.hint}>Ohne Budget bleibt die Preisbewertung offen.</Text>
<View style={styles.row}>
<Text style={styles.currency}>{currency}</Text>
<TextInput
accessibilityLabel="Budgetbetrag"
keyboardType="decimal-pad"
onChangeText={(text) => onChangeValue(Number(text.replace(',', '.')) || 0)}
onChangeText={updateValue}
placeholder="z. B. 45.00"
placeholderTextColor={colors.muted}
style={styles.input}
value={String(value ?? '')}
value={inputValue}
/>
</View>
<View style={styles.periodGrid}>
@@ -23,6 +36,7 @@ export default function BudgetInput({ value, currency, period, onChangePeriod, o
return (
<Pressable
accessibilityRole="button"
accessibilityState={{ selected: isActive }}
key={option}
onPress={() => onChangePeriod(option)}
style={[styles.periodChip, isActive ? styles.activePeriodChip : null]}
@@ -42,11 +56,16 @@ const styles = StyleSheet.create({
wrap: {
backgroundColor: colors.surface,
borderColor: colors.border,
borderRadius: 8,
borderRadius: 18,
borderWidth: 1,
gap: 10,
padding: 14
},
hint: {
color: colors.muted,
fontSize: 13,
lineHeight: 18
},
label: {
color: colors.text,
fontSize: 15,
@@ -64,7 +83,7 @@ const styles = StyleSheet.create({
},
input: {
borderColor: colors.border,
borderRadius: 8,
borderRadius: 12,
borderWidth: 1,
color: colors.text,
flex: 1,
@@ -80,7 +99,7 @@ const styles = StyleSheet.create({
periodChip: {
alignItems: 'center',
borderColor: colors.border,
borderRadius: 8,
borderRadius: 12,
borderWidth: 1,
flexGrow: 1,
paddingHorizontal: 10,
+2 -1
View File
@@ -7,6 +7,7 @@ export default function ManualSearchInput({ value, onChangeText, onSubmit, disab
return (
<View style={styles.row}>
<TextInput
accessibilityLabel="Produktname oder Barcode"
autoCapitalize="none"
onChangeText={onChangeText}
onSubmitEditing={onSubmit}
@@ -29,7 +30,7 @@ const styles = StyleSheet.create({
input: {
backgroundColor: colors.surface,
borderColor: colors.border,
borderRadius: 8,
borderRadius: 14,
borderWidth: 1,
color: colors.text,
fontSize: 16,
+2 -1
View File
@@ -10,6 +10,7 @@ export default function SettingsSwitchRow({ label, description, value, onValueCh
<Text style={styles.description}>{description}</Text>
</View>
<Switch
accessibilityLabel={label}
onValueChange={onValueChange}
thumbColor={value ? colors.primary : '#f4f3f4'}
trackColor={{ false: colors.border, true: '#b7dbcf' }}
@@ -24,7 +25,7 @@ const styles = StyleSheet.create({
alignItems: 'center',
backgroundColor: colors.surface,
borderColor: colors.border,
borderRadius: 8,
borderRadius: 16,
borderWidth: 1,
flexDirection: 'row',
gap: 12,
+7 -6
View File
@@ -23,7 +23,7 @@ export default function AppHeader({ title, subtitle, onBack, rightAction }) {
const styles = StyleSheet.create({
header: {
paddingTop: 6
paddingBottom: 2
},
row: {
alignItems: 'center',
@@ -33,11 +33,11 @@ const styles = StyleSheet.create({
backButton: {
alignItems: 'center',
borderColor: colors.border,
borderRadius: 8,
borderRadius: 14,
borderWidth: 1,
height: 40,
height: 44,
justifyContent: 'center',
width: 40
width: 44
},
backText: {
color: colors.primary,
@@ -49,8 +49,9 @@ const styles = StyleSheet.create({
},
title: {
color: colors.text,
fontSize: 28,
fontWeight: '800'
fontSize: 29,
fontWeight: '800',
letterSpacing: -0.6
},
subtitle: {
color: colors.muted,
+23 -11
View File
@@ -3,21 +3,23 @@ import { Pressable, StyleSheet, Text, View } from 'react-native';
import { colors } from '../../utils/constants';
const tabs = [
{ id: 'home', label: 'Home', icon: '⌂' },
{ id: 'scan', label: 'Scan', icon: '□' },
{ id: 'list', label: 'Liste', icon: '✓' },
{ id: 'profile', label: 'Profil', icon: '◦' }
{ id: 'home', label: 'Start', icon: '⌂' },
{ id: 'scan', label: 'Scannen', icon: '▣' },
{ id: 'list', label: 'Liste', icon: '☷' },
{ id: 'profile', label: 'Profil', icon: '◉' }
];
export default function BottomTabBar({ activeTab, onChange }) {
return (
<View style={styles.wrap}>
<View style={styles.inner}>
{tabs.map((tab) => {
const isActive = activeTab === tab.id;
return (
<Pressable
accessibilityLabel={`${tab.label} öffnen`}
accessibilityRole="button"
accessibilityState={{ selected: isActive }}
key={tab.id}
onPress={() => onChange(tab.id)}
style={[styles.tab, isActive ? styles.activeTab : null]}
@@ -27,6 +29,7 @@ export default function BottomTabBar({ activeTab, onChange }) {
</Pressable>
);
})}
</View>
</View>
);
}
@@ -37,26 +40,35 @@ const styles = StyleSheet.create({
borderColor: colors.border,
borderTopWidth: 1,
bottom: 0,
flexDirection: 'row',
gap: 8,
left: 0,
padding: 10,
paddingHorizontal: 12,
paddingTop: 9,
paddingBottom: 12,
position: 'absolute',
right: 0
},
inner: {
alignSelf: 'center',
flexDirection: 'row',
gap: 6,
maxWidth: 720,
width: '100%'
},
tab: {
alignItems: 'center',
borderRadius: 8,
borderRadius: 13,
flex: 1,
gap: 2,
paddingVertical: 8
minHeight: 58,
justifyContent: 'center',
paddingVertical: 6
},
activeTab: {
backgroundColor: '#e5f1ec'
backgroundColor: colors.surfaceTint
},
icon: {
color: colors.muted,
fontSize: 18,
fontSize: 20,
fontWeight: '800'
},
label: {
+8 -4
View File
@@ -8,7 +8,7 @@ export default function ScreenContainer({ children, scroll = true }) {
}
return (
<ScrollView contentContainerStyle={styles.content} style={styles.container}>
<ScrollView contentContainerStyle={styles.content} keyboardShouldPersistTaps="handled" style={styles.container}>
{children}
</ScrollView>
);
@@ -20,8 +20,12 @@ const styles = StyleSheet.create({
flex: 1
},
content: {
gap: 18,
padding: 20,
paddingBottom: 110
alignSelf: 'center',
gap: 20,
maxWidth: 720,
paddingHorizontal: 20,
paddingTop: 22,
paddingBottom: 124,
width: '100%'
}
});
+3 -2
View File
@@ -9,8 +9,9 @@ export default function SectionTitle({ children }) {
const styles = StyleSheet.create({
title: {
color: colors.text,
fontSize: 18,
fontSize: 20,
fontWeight: '800',
marginTop: 4
letterSpacing: -0.3,
marginTop: 6
}
});
+12 -10
View File
@@ -13,6 +13,7 @@ export default function ShoppingListItem({ item, onToggle, onDelete, onUpdatePri
}, [item.estimatedPrice]);
function savePrice() {
if (!priceText.trim() && typeof item.estimatedPrice !== 'number') return;
const parsedPrice = parsePrice(priceText);
if (parsedPrice === undefined) {
@@ -26,12 +27,12 @@ export default function ShoppingListItem({ item, onToggle, onDelete, onUpdatePri
return (
<View style={styles.row}>
<Pressable accessibilityRole="checkbox" onPress={onToggle} style={[styles.checkbox, item.checked ? styles.checked : null]}>
<Pressable accessibilityLabel={`${item.productName} ${item.checked ? 'als offen markieren' : 'abhaken'}`} accessibilityRole="checkbox" accessibilityState={{ checked: item.checked }} onPress={onToggle} style={[styles.checkbox, item.checked ? styles.checked : null]}>
<Text style={styles.checkText}>{item.checked ? '✓' : ''}</Text>
</Pressable>
<View style={styles.copy}>
<Text style={[styles.name, item.checked ? styles.done : null]}>{item.productName}</Text>
<Text style={styles.meta}>Menge {item.quantity} · {formatPrice(item.estimatedPrice, item.currency || 'CHF')}</Text>
<Text style={styles.meta}>{item.quantity} × {formatPrice(item.estimatedPrice, item.currency || 'CHF')}{item.quantity > 1 && typeof item.estimatedPrice === 'number' ? ` · ${formatPrice(item.estimatedPrice * item.quantity, item.currency || 'CHF')}` : ''}</Text>
<View style={styles.priceRow}>
<Text style={styles.currency}>{item.currency || 'CHF'}</Text>
<TextInput
@@ -51,7 +52,7 @@ export default function ShoppingListItem({ item, onToggle, onDelete, onUpdatePri
</View>
{priceError ? <Text style={styles.errorText}>{priceError}</Text> : null}
</View>
<Pressable accessibilityRole="button" onPress={onDelete} style={styles.deleteButton}>
<Pressable accessibilityLabel={`${item.productName} löschen`} accessibilityRole="button" onPress={onDelete} style={styles.deleteButton}>
<Text style={styles.deleteText}>Löschen</Text>
</Pressable>
</View>
@@ -78,7 +79,7 @@ const styles = StyleSheet.create({
alignItems: 'center',
backgroundColor: colors.surface,
borderColor: colors.border,
borderRadius: 8,
borderRadius: 18,
borderWidth: 1,
flexDirection: 'row',
gap: 12,
@@ -87,11 +88,11 @@ const styles = StyleSheet.create({
checkbox: {
alignItems: 'center',
borderColor: colors.primary,
borderRadius: 6,
borderRadius: 10,
borderWidth: 2,
height: 28,
height: 44,
justifyContent: 'center',
width: 28
width: 44
},
checked: {
backgroundColor: colors.primary
@@ -129,7 +130,7 @@ const styles = StyleSheet.create({
},
priceInput: {
borderColor: colors.border,
borderRadius: 8,
borderRadius: 10,
borderWidth: 1,
color: colors.text,
fontSize: 14,
@@ -144,8 +145,9 @@ const styles = StyleSheet.create({
marginTop: 4
},
deleteButton: {
paddingHorizontal: 8,
paddingVertical: 6
justifyContent: 'center',
minHeight: 44,
paddingHorizontal: 4
},
deleteText: {
color: colors.danger,
@@ -9,7 +9,10 @@ export default function AlternativeProductCard({ item, onChoose, onOpen }) {
<View style={styles.wrap}>
<ProductCard product={item.product} rating={item.rating} onPress={onOpen} />
<Text style={styles.reason}>{item.reason}</Text>
<PrimaryButton label="Beste Alternative wählen" onPress={onChoose} />
<PrimaryButton
label={typeof item.product.price === 'number' ? 'Zur Liste hinzufügen' : 'Produkt ansehen'}
onPress={onChoose}
/>
</View>
);
}
+20 -4
View File
@@ -20,7 +20,10 @@ export default function ProductCard({ product, rating, onPress }) {
</Text>
<Text style={styles.source}>{sourceLabel(product.dataSource)}</Text>
</View>
</View>
<View style={styles.footer}>
{rating ? <ProductScoreBadge status={rating.overallStatus} score={rating.overallScore} /> : null}
<Text style={styles.openLabel}>Details ansehen ›</Text>
</View>
</Pressable>
);
@@ -40,7 +43,7 @@ const styles = StyleSheet.create({
card: {
backgroundColor: colors.surface,
borderColor: colors.border,
borderRadius: 8,
borderRadius: 18,
borderWidth: 1,
gap: 10,
padding: 16
@@ -50,12 +53,25 @@ const styles = StyleSheet.create({
flexDirection: 'row',
gap: 12
},
footer: {
alignItems: 'center',
borderTopColor: colors.border,
borderTopWidth: 1,
flexDirection: 'row',
justifyContent: 'space-between',
paddingTop: 11
},
openLabel: {
color: colors.primary,
fontSize: 13,
fontWeight: '800'
},
copy: {
flex: 1
},
name: {
color: colors.text,
fontSize: 17,
fontSize: 16,
fontWeight: '800'
},
meta: {
@@ -67,11 +83,11 @@ const styles = StyleSheet.create({
color: colors.muted,
fontSize: 13,
lineHeight: 18,
marginTop: 8
marginTop: 5
},
source: {
alignSelf: 'flex-start',
backgroundColor: '#eef3ef',
backgroundColor: colors.surfaceTint,
borderRadius: 8,
color: colors.primaryDark,
fontSize: 12,
+1 -1
View File
@@ -36,7 +36,7 @@ export default function ProductScoreBadge({ status, score }) {
const styles = StyleSheet.create({
badge: {
alignSelf: 'flex-start',
borderRadius: 8,
borderRadius: 999,
paddingHorizontal: 10,
paddingVertical: 6
},
+1 -1
View File
@@ -34,7 +34,7 @@ const styles = StyleSheet.create({
alignItems: 'center',
backgroundColor: colors.surface,
borderColor: colors.border,
borderRadius: 8,
borderRadius: 16,
borderWidth: 1,
flexDirection: 'row',
gap: 12,
+2 -2
View File
@@ -27,7 +27,7 @@ export default function BarcodeScannerView({ isLoadingProduct, isScanning, onBar
const styles = StyleSheet.create({
wrap: {
backgroundColor: '#000000',
borderRadius: 8,
borderRadius: 18,
height: 320,
overflow: 'hidden'
},
@@ -42,7 +42,7 @@ const styles = StyleSheet.create({
},
frame: {
borderColor: colors.surface,
borderRadius: 8,
borderRadius: 14,
borderWidth: 3,
height: 150,
width: '82%'
+1 -1
View File
@@ -29,7 +29,7 @@ const styles = StyleSheet.create({
},
modal: {
backgroundColor: colors.surface,
borderRadius: 8,
borderRadius: 20,
gap: 14,
padding: 20,
width: '100%'