Update styling and accessibility, refine product and shopping list functionalities
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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%'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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%'
|
||||
|
||||
@@ -29,7 +29,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
modal: {
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 8,
|
||||
borderRadius: 20,
|
||||
gap: 14,
|
||||
padding: 20,
|
||||
width: '100%'
|
||||
|
||||
Reference in New Issue
Block a user