Refactor profile screen layout and user preference handling
Android Release Build / build-android (push) Has been cancelled

This commit is contained in:
2026-10-16 16:07:20 +02:00
parent 326e66bcff
commit 6681a54671
3 changed files with 190 additions and 116 deletions
+4 -7
View File
@@ -16,7 +16,7 @@ export default function BudgetInput({ value, currency, period, onChangePeriod, o
return ( return (
<View style={styles.wrap}> <View style={styles.wrap}>
<Text style={styles.label}>Dein Budget (optional)</Text> <Text style={styles.label}>Betrag (optional)</Text>
<Text style={styles.hint}>Ohne Budget bleibt die Preisbewertung offen.</Text> <Text style={styles.hint}>Ohne Budget bleibt die Preisbewertung offen.</Text>
<View style={styles.row}> <View style={styles.row}>
<Text style={styles.currency}>{currency}</Text> <Text style={styles.currency}>{currency}</Text>
@@ -54,12 +54,7 @@ export default function BudgetInput({ value, currency, period, onChangePeriod, o
const styles = StyleSheet.create({ const styles = StyleSheet.create({
wrap: { wrap: {
backgroundColor: colors.surface, gap: 10
borderColor: colors.border,
borderRadius: 18,
borderWidth: 1,
gap: 10,
padding: 14
}, },
hint: { hint: {
color: colors.muted, color: colors.muted,
@@ -102,6 +97,8 @@ const styles = StyleSheet.create({
borderRadius: 12, borderRadius: 12,
borderWidth: 1, borderWidth: 1,
flexGrow: 1, flexGrow: 1,
minHeight: 44,
justifyContent: 'center',
paddingHorizontal: 10, paddingHorizontal: 10,
paddingVertical: 9 paddingVertical: 9
}, },
+8 -2
View File
@@ -2,9 +2,9 @@ import { StyleSheet, Switch, Text, View } from 'react-native';
import { colors } from '../../utils/constants'; import { colors } from '../../utils/constants';
export default function SettingsSwitchRow({ label, description, value, onValueChange }) { export default function SettingsSwitchRow({ label, description, value, onValueChange, embedded = false }) {
return ( return (
<View style={styles.row}> <View style={[styles.row, embedded && styles.embeddedRow]}>
<View style={styles.copy}> <View style={styles.copy}>
<Text style={styles.label}>{label}</Text> <Text style={styles.label}>{label}</Text>
<Text style={styles.description}>{description}</Text> <Text style={styles.description}>{description}</Text>
@@ -31,6 +31,12 @@ const styles = StyleSheet.create({
gap: 12, gap: 12,
padding: 14 padding: 14
}, },
embeddedRow: {
backgroundColor: 'transparent',
borderWidth: 0,
borderRadius: 0,
padding: 0
},
copy: { copy: {
flex: 1 flex: 1
}, },
+178 -107
View File
@@ -4,108 +4,150 @@ import BudgetInput from '../components/form/BudgetInput';
import SettingsSwitchRow from '../components/form/SettingsSwitchRow'; import SettingsSwitchRow from '../components/form/SettingsSwitchRow';
import AppHeader from '../components/layout/AppHeader'; import AppHeader from '../components/layout/AppHeader';
import ScreenContainer from '../components/layout/ScreenContainer'; import ScreenContainer from '../components/layout/ScreenContainer';
import SectionTitle from '../components/layout/SectionTitle'; import { budgetPeriodLabels, colors, dietLabels } from '../utils/constants';
import { colors, dietLabels } from '../utils/constants'; import { formatPrice } from '../utils/formatPrice';
const dietOptions = ['none', 'vegetarian', 'vegan', 'lowSugar', 'highProtein']; const dietOptions = ['none', 'vegetarian', 'vegan', 'lowSugar', 'highProtein'];
const allergenOptions = ['Erdnüsse', 'Milch', 'Hafer', 'Gluten'];
const priorityOptions = ['low', 'medium', 'high']; const priorityOptions = ['low', 'medium', 'high'];
const priorityLabels = { low: 'Niedrig', medium: 'Mittel', high: 'Hoch' };
export default function ProfileScreen({ preferences, onChangePreferences }) { export default function ProfileScreen({ preferences, onChangePreferences }) {
function updatePreference(key, value) { function updatePreference(key, value) {
onChangePreferences({ onChangePreferences({ ...preferences, [key]: value });
...preferences,
[key]: value
});
} }
function toggleAllergen(allergen) { function toggleAllergen(allergen) {
const current = preferences.avoidAllergens || []; const current = preferences.avoidAllergens || [];
const nextAllergens = current.includes(allergen) updatePreference('avoidAllergens', current.includes(allergen)
? current.filter((item) => item !== allergen) ? current.filter((item) => item !== allergen)
: [...current, allergen]; : [...current, allergen]);
updatePreference('avoidAllergens', nextAllergens);
} }
const allergenCount = preferences.avoidAllergens?.length || 0;
const budget = preferences.maxBudgetPerShoppingTrip;
const budgetSummary = typeof budget === 'number' && budget > 0
? `${formatPrice(budget, preferences.currency)} pro ${budgetPeriodLabels[preferences.budgetPeriod] || 'Woche'}`
: 'Kein Budget festgelegt';
return ( return (
<ScreenContainer> <ScreenContainer>
<AppHeader <AppHeader title="Einstellungen" subtitle="So werden Produkte für dich bewertet" />
title="Profil"
subtitle="Passe die Bewertung an deine Bedürfnisse an"
/>
<SectionTitle>Ernährungsziel</SectionTitle> <SettingsSection
<View style={styles.optionGrid}> title="Ernährung & Allergien"
{dietOptions.map((option) => ( summary={`${dietLabels[preferences.preferredDiet] || 'Kein Ziel'} · ${allergenCount === 0 ? 'Keine Allergene gewählt' : `${allergenCount} Allergene gewählt`}`}
<Pressable >
accessibilityRole="button" <View style={styles.field}>
accessibilityState={{ selected: preferences.preferredDiet === option }} <Text style={styles.fieldLabel}>Ernährungsziel</Text>
key={option} <View style={styles.optionGrid}>
onPress={() => updatePreference('preferredDiet', option)} {dietOptions.map((option) => (
style={[styles.chip, preferences.preferredDiet === option ? styles.activeChip : null]} <SelectionChip
> active={preferences.preferredDiet === option}
<Text style={[styles.chipText, preferences.preferredDiet === option ? styles.activeChipText : null]}> key={option}
{dietLabels[option]} label={dietLabels[option]}
</Text> onPress={() => updatePreference('preferredDiet', option)}
</Pressable> />
))} ))}
</View> </View>
</View>
<View style={styles.divider} />
<View style={styles.field}>
<Text style={styles.fieldLabel}>Allergene vermeiden</Text>
<Text style={styles.fieldHint}>Wähle nur relevante Allergene. Fehlen Produktangaben, bleibt die Bewertung unsicher.</Text>
<View style={styles.optionGrid}>
{allergenOptions.map((allergen) => (
<SelectionChip
active={preferences.avoidAllergens?.includes(allergen)}
danger
key={allergen}
label={allergen}
onPress={() => toggleAllergen(allergen)}
/>
))}
</View>
</View>
</SettingsSection>
<SectionTitle>Allergien vermeiden</SectionTitle> <SettingsSection title="Budget" summary={budgetSummary}>
<Text style={styles.sectionHint}>Wähle nur Allergene, die für dich relevant sind. Fehlende Produktangaben bleiben als unsicher markiert.</Text> <BudgetInput
<View style={styles.optionGrid}> currency={preferences.currency}
{['Erdnüsse', 'Milch', 'Hafer', 'Gluten'].map((allergen) => { onChangePeriod={(value) => updatePreference('budgetPeriod', value)}
const isActive = preferences.avoidAllergens?.includes(allergen); onChangeValue={(value) => updatePreference('maxBudgetPerShoppingTrip', value)}
return ( period={preferences.budgetPeriod || 'week'}
<Pressable value={budget}
accessibilityRole="button" />
accessibilityState={{ selected: isActive }} </SettingsSection>
key={allergen}
onPress={() => toggleAllergen(allergen)}
style={[styles.chip, isActive ? styles.dangerChip : null]}
>
<Text style={[styles.chipText, isActive ? styles.dangerChipText : null]}>{allergen}</Text>
</Pressable>
);
})}
</View>
<BudgetInput <SettingsSection
currency={preferences.currency} title="Bewertung"
onChangePeriod={(value) => updatePreference('budgetPeriod', value)} summary={`Ernährung ${priorityLabels[preferences.nutritionPriority] || 'Mittel'} · Nachhaltigkeit ${priorityLabels[preferences.sustainabilityPriority] || 'Mittel'}`}
onChangeValue={(value) => updatePreference('maxBudgetPerShoppingTrip', value)} >
period={preferences.budgetPeriod || 'week'} <SettingsSwitchRow
value={preferences.maxBudgetPerShoppingTrip} description="Bio-Produkte erhalten in der Nachhaltigkeitsbewertung einen Vorteil."
/> embedded
label="Bio bevorzugen"
onValueChange={(value) => updatePreference('preferBio', value)}
value={preferences.preferBio}
/>
<View style={styles.divider} />
<Text style={styles.fieldLabel}>Prioritäten</Text>
<Text style={styles.fieldHint}>Hoch bedeutet, dass dieser Bereich stärker in die Gesamtbewertung einfliesst.</Text>
<PrioritySelector
label="Ernährung"
onChange={(value) => updatePreference('nutritionPriority', value)}
value={preferences.nutritionPriority}
/>
<PrioritySelector
label="Nachhaltigkeit"
onChange={(value) => updatePreference('sustainabilityPriority', value)}
value={preferences.sustainabilityPriority}
/>
</SettingsSection>
<SettingsSwitchRow <SettingsSection
description="Bio-Produkte erhalten in der Nachhaltigkeitsbewertung einen Vorteil." title="App & Daten"
label="Bio bevorzugen" summary={preferences.offlineModeEnabled ? 'Offline-Modus aktiv' : 'Online-Suche aktiv'}
onValueChange={(value) => updatePreference('preferBio', value)} >
value={preferences.preferBio} <SettingsSwitchRow
/> description="Im Offline-Modus werden nur lokal gespeicherte Produkte gefunden."
embedded
<SettingsSwitchRow label="Offline-Modus"
description="Es werden nur lokal gespeicherte Produkte bewertet." onValueChange={(value) => updatePreference('offlineModeEnabled', value)}
label="Offline-Modus" value={preferences.offlineModeEnabled}
onValueChange={(value) => updatePreference('offlineModeEnabled', value)} />
value={preferences.offlineModeEnabled} </SettingsSection>
/>
<SectionTitle>Prioritäten</SectionTitle>
<PrioritySelector
label="Ernährung"
onChange={(value) => updatePreference('nutritionPriority', value)}
value={preferences.nutritionPriority}
/>
<PrioritySelector
label="Nachhaltigkeit"
onChange={(value) => updatePreference('sustainabilityPriority', value)}
value={preferences.sustainabilityPriority}
/>
</ScreenContainer> </ScreenContainer>
); );
} }
function SettingsSection({ title, summary, children }) {
return (
<View style={styles.section}>
<View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>{title}</Text>
<Text style={styles.sectionSummary}>{summary}</Text>
</View>
<View style={styles.sectionContent}>{children}</View>
</View>
);
}
function SelectionChip({ active, danger = false, label, onPress }) {
return (
<Pressable
accessibilityRole="button"
accessibilityState={{ selected: Boolean(active) }}
onPress={onPress}
style={[styles.chip, active && (danger ? styles.dangerChip : styles.activeChip)]}
>
<Text style={[styles.chipText, active && (danger ? styles.dangerChipText : styles.activeChipText)]}>
{label}
</Text>
</Pressable>
);
}
function PrioritySelector({ label, value, onChange }) { function PrioritySelector({ label, value, onChange }) {
return ( return (
<View style={styles.priorityRow}> <View style={styles.priorityRow}>
@@ -117,10 +159,10 @@ function PrioritySelector({ label, value, onChange }) {
accessibilityState={{ selected: value === option }} accessibilityState={{ selected: value === option }}
key={option} key={option}
onPress={() => onChange(option)} onPress={() => onChange(option)}
style={[styles.priorityChip, value === option ? styles.activeChip : null]} style={[styles.priorityChip, value === option && styles.activeChip]}
> >
<Text style={[styles.chipText, value === option ? styles.activeChipText : null]}> <Text style={[styles.chipText, value === option && styles.activeChipText]}>
{option === 'low' ? 'Niedrig' : option === 'medium' ? 'Mittel' : 'Hoch'} {priorityLabels[option]}
</Text> </Text>
</Pressable> </Pressable>
))} ))}
@@ -130,24 +172,62 @@ function PrioritySelector({ label, value, onChange }) {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
section: {
backgroundColor: colors.surface,
borderColor: colors.border,
borderRadius: 20,
borderWidth: 1,
overflow: 'hidden'
},
sectionHeader: {
backgroundColor: colors.surfaceTint,
gap: 4,
paddingHorizontal: 18,
paddingVertical: 15
},
sectionTitle: {
color: colors.text,
fontSize: 19,
fontWeight: '800'
},
sectionSummary: {
color: colors.primaryDark,
fontSize: 13,
lineHeight: 18
},
sectionContent: {
gap: 14,
padding: 18
},
field: { gap: 10 },
fieldLabel: {
color: colors.text,
fontSize: 15,
fontWeight: '800'
},
fieldHint: {
color: colors.muted,
fontSize: 13,
lineHeight: 19
},
divider: {
backgroundColor: colors.border,
height: 1
},
optionGrid: { optionGrid: {
flexDirection: 'row', flexDirection: 'row',
flexWrap: 'wrap', flexWrap: 'wrap',
gap: 8 gap: 8
}, },
sectionHint: {
color: colors.muted,
fontSize: 13,
lineHeight: 19,
marginTop: -10
},
chip: { chip: {
backgroundColor: colors.surface, backgroundColor: colors.surface,
borderColor: colors.border, borderColor: colors.border,
borderRadius: 12, borderRadius: 12,
borderWidth: 1, borderWidth: 1,
justifyContent: 'center',
minHeight: 44,
paddingHorizontal: 12, paddingHorizontal: 12,
paddingVertical: 10 paddingVertical: 9
}, },
activeChip: { activeChip: {
backgroundColor: colors.primary, backgroundColor: colors.primary,
@@ -162,35 +242,26 @@ const styles = StyleSheet.create({
fontSize: 14, fontSize: 14,
fontWeight: '800' fontWeight: '800'
}, },
activeChipText: { activeChipText: { color: colors.surface },
color: colors.surface dangerChipText: { color: colors.danger },
}, priorityRow: { gap: 8 },
dangerChipText: {
color: colors.danger
},
priorityRow: {
backgroundColor: colors.surface,
borderColor: colors.border,
borderRadius: 16,
borderWidth: 1,
gap: 10,
padding: 14
},
priorityLabel: { priorityLabel: {
color: colors.text, color: colors.text,
fontSize: 15, fontSize: 14,
fontWeight: '800' fontWeight: '700'
}, },
priorityOptions: { priorityOptions: {
flexDirection: 'row', flexDirection: 'row',
gap: 8 gap: 8
}, },
priorityChip: { priorityChip: {
alignItems: 'center',
borderColor: colors.border, borderColor: colors.border,
borderRadius: 12, borderRadius: 12,
borderWidth: 1, borderWidth: 1,
flex: 1, flex: 1,
paddingVertical: 10, justifyContent: 'center',
alignItems: 'center' minHeight: 44,
paddingHorizontal: 4
} }
}); });