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
}, },
+150 -79
View File
@@ -4,94 +4,95 @@ 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
title="Ernährung & Allergien"
summary={`${dietLabels[preferences.preferredDiet] || 'Kein Ziel'} · ${allergenCount === 0 ? 'Keine Allergene gewählt' : `${allergenCount} Allergene gewählt`}`}
>
<View style={styles.field}>
<Text style={styles.fieldLabel}>Ernährungsziel</Text>
<View style={styles.optionGrid}> <View style={styles.optionGrid}>
{dietOptions.map((option) => ( {dietOptions.map((option) => (
<Pressable <SelectionChip
accessibilityRole="button" active={preferences.preferredDiet === option}
accessibilityState={{ selected: preferences.preferredDiet === option }}
key={option} key={option}
label={dietLabels[option]}
onPress={() => updatePreference('preferredDiet', option)} onPress={() => updatePreference('preferredDiet', option)}
style={[styles.chip, preferences.preferredDiet === option ? styles.activeChip : null]} />
>
<Text style={[styles.chipText, preferences.preferredDiet === option ? styles.activeChipText : null]}>
{dietLabels[option]}
</Text>
</Pressable>
))} ))}
</View> </View>
<SectionTitle>Allergien vermeiden</SectionTitle>
<Text style={styles.sectionHint}>Wähle nur Allergene, die für dich relevant sind. Fehlende Produktangaben bleiben als unsicher markiert.</Text>
<View style={styles.optionGrid}>
{['Erdnüsse', 'Milch', 'Hafer', 'Gluten'].map((allergen) => {
const isActive = preferences.avoidAllergens?.includes(allergen);
return (
<Pressable
accessibilityRole="button"
accessibilityState={{ selected: isActive }}
key={allergen}
onPress={() => toggleAllergen(allergen)}
style={[styles.chip, isActive ? styles.dangerChip : null]}
>
<Text style={[styles.chipText, isActive ? styles.dangerChipText : null]}>{allergen}</Text>
</Pressable>
);
})}
</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>
<SettingsSection title="Budget" summary={budgetSummary}>
<BudgetInput <BudgetInput
currency={preferences.currency} currency={preferences.currency}
onChangePeriod={(value) => updatePreference('budgetPeriod', value)} onChangePeriod={(value) => updatePreference('budgetPeriod', value)}
onChangeValue={(value) => updatePreference('maxBudgetPerShoppingTrip', value)} onChangeValue={(value) => updatePreference('maxBudgetPerShoppingTrip', value)}
period={preferences.budgetPeriod || 'week'} period={preferences.budgetPeriod || 'week'}
value={preferences.maxBudgetPerShoppingTrip} value={budget}
/> />
</SettingsSection>
<SettingsSection
title="Bewertung"
summary={`Ernährung ${priorityLabels[preferences.nutritionPriority] || 'Mittel'} · Nachhaltigkeit ${priorityLabels[preferences.sustainabilityPriority] || 'Mittel'}`}
>
<SettingsSwitchRow <SettingsSwitchRow
description="Bio-Produkte erhalten in der Nachhaltigkeitsbewertung einen Vorteil." description="Bio-Produkte erhalten in der Nachhaltigkeitsbewertung einen Vorteil."
embedded
label="Bio bevorzugen" label="Bio bevorzugen"
onValueChange={(value) => updatePreference('preferBio', value)} onValueChange={(value) => updatePreference('preferBio', value)}
value={preferences.preferBio} value={preferences.preferBio}
/> />
<View style={styles.divider} />
<SettingsSwitchRow <Text style={styles.fieldLabel}>Prioritäten</Text>
description="Es werden nur lokal gespeicherte Produkte bewertet." <Text style={styles.fieldHint}>Hoch bedeutet, dass dieser Bereich stärker in die Gesamtbewertung einfliesst.</Text>
label="Offline-Modus"
onValueChange={(value) => updatePreference('offlineModeEnabled', value)}
value={preferences.offlineModeEnabled}
/>
<SectionTitle>Prioritäten</SectionTitle>
<PrioritySelector <PrioritySelector
label="Ernährung" label="Ernährung"
onChange={(value) => updatePreference('nutritionPriority', value)} onChange={(value) => updatePreference('nutritionPriority', value)}
@@ -102,10 +103,51 @@ export default function ProfileScreen({ preferences, onChangePreferences }) {
onChange={(value) => updatePreference('sustainabilityPriority', value)} onChange={(value) => updatePreference('sustainabilityPriority', value)}
value={preferences.sustainabilityPriority} value={preferences.sustainabilityPriority}
/> />
</SettingsSection>
<SettingsSection
title="App & Daten"
summary={preferences.offlineModeEnabled ? 'Offline-Modus aktiv' : 'Online-Suche aktiv'}
>
<SettingsSwitchRow
description="Im Offline-Modus werden nur lokal gespeicherte Produkte gefunden."
embedded
label="Offline-Modus"
onValueChange={(value) => updatePreference('offlineModeEnabled', value)}
value={preferences.offlineModeEnabled}
/>
</SettingsSection>
</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
} }
}); });