Refactor profile screen layout and user preference handling
Android Release Build / build-android (push) Has been cancelled
Android Release Build / build-android (push) Has been cancelled
This commit is contained in:
@@ -16,7 +16,7 @@ export default function BudgetInput({ value, currency, period, onChangePeriod, o
|
||||
|
||||
return (
|
||||
<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>
|
||||
<View style={styles.row}>
|
||||
<Text style={styles.currency}>{currency}</Text>
|
||||
@@ -54,12 +54,7 @@ export default function BudgetInput({ value, currency, period, onChangePeriod, o
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
wrap: {
|
||||
backgroundColor: colors.surface,
|
||||
borderColor: colors.border,
|
||||
borderRadius: 18,
|
||||
borderWidth: 1,
|
||||
gap: 10,
|
||||
padding: 14
|
||||
gap: 10
|
||||
},
|
||||
hint: {
|
||||
color: colors.muted,
|
||||
@@ -102,6 +97,8 @@ const styles = StyleSheet.create({
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
flexGrow: 1,
|
||||
minHeight: 44,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 9
|
||||
},
|
||||
|
||||
@@ -2,9 +2,9 @@ import { StyleSheet, Switch, Text, View } from 'react-native';
|
||||
|
||||
import { colors } from '../../utils/constants';
|
||||
|
||||
export default function SettingsSwitchRow({ label, description, value, onValueChange }) {
|
||||
export default function SettingsSwitchRow({ label, description, value, onValueChange, embedded = false }) {
|
||||
return (
|
||||
<View style={styles.row}>
|
||||
<View style={[styles.row, embedded && styles.embeddedRow]}>
|
||||
<View style={styles.copy}>
|
||||
<Text style={styles.label}>{label}</Text>
|
||||
<Text style={styles.description}>{description}</Text>
|
||||
@@ -31,6 +31,12 @@ const styles = StyleSheet.create({
|
||||
gap: 12,
|
||||
padding: 14
|
||||
},
|
||||
embeddedRow: {
|
||||
backgroundColor: 'transparent',
|
||||
borderWidth: 0,
|
||||
borderRadius: 0,
|
||||
padding: 0
|
||||
},
|
||||
copy: {
|
||||
flex: 1
|
||||
},
|
||||
|
||||
+150
-79
@@ -4,94 +4,95 @@ import BudgetInput from '../components/form/BudgetInput';
|
||||
import SettingsSwitchRow from '../components/form/SettingsSwitchRow';
|
||||
import AppHeader from '../components/layout/AppHeader';
|
||||
import ScreenContainer from '../components/layout/ScreenContainer';
|
||||
import SectionTitle from '../components/layout/SectionTitle';
|
||||
import { colors, dietLabels } from '../utils/constants';
|
||||
import { budgetPeriodLabels, colors, dietLabels } from '../utils/constants';
|
||||
import { formatPrice } from '../utils/formatPrice';
|
||||
|
||||
const dietOptions = ['none', 'vegetarian', 'vegan', 'lowSugar', 'highProtein'];
|
||||
const allergenOptions = ['Erdnüsse', 'Milch', 'Hafer', 'Gluten'];
|
||||
const priorityOptions = ['low', 'medium', 'high'];
|
||||
const priorityLabels = { low: 'Niedrig', medium: 'Mittel', high: 'Hoch' };
|
||||
|
||||
export default function ProfileScreen({ preferences, onChangePreferences }) {
|
||||
function updatePreference(key, value) {
|
||||
onChangePreferences({
|
||||
...preferences,
|
||||
[key]: value
|
||||
});
|
||||
onChangePreferences({ ...preferences, [key]: value });
|
||||
}
|
||||
|
||||
function toggleAllergen(allergen) {
|
||||
const current = preferences.avoidAllergens || [];
|
||||
const nextAllergens = current.includes(allergen)
|
||||
updatePreference('avoidAllergens', current.includes(allergen)
|
||||
? current.filter((item) => item !== allergen)
|
||||
: [...current, allergen];
|
||||
updatePreference('avoidAllergens', nextAllergens);
|
||||
: [...current, allergen]);
|
||||
}
|
||||
|
||||
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 (
|
||||
<ScreenContainer>
|
||||
<AppHeader
|
||||
title="Profil"
|
||||
subtitle="Passe die Bewertung an deine Bedürfnisse an"
|
||||
/>
|
||||
<AppHeader title="Einstellungen" subtitle="So werden Produkte für dich bewertet" />
|
||||
|
||||
<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}>
|
||||
{dietOptions.map((option) => (
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityState={{ selected: preferences.preferredDiet === option }}
|
||||
<SelectionChip
|
||||
active={preferences.preferredDiet === option}
|
||||
key={option}
|
||||
label={dietLabels[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>
|
||||
|
||||
<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 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
|
||||
currency={preferences.currency}
|
||||
onChangePeriod={(value) => updatePreference('budgetPeriod', value)}
|
||||
onChangeValue={(value) => updatePreference('maxBudgetPerShoppingTrip', value)}
|
||||
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
|
||||
description="Bio-Produkte erhalten in der Nachhaltigkeitsbewertung einen Vorteil."
|
||||
embedded
|
||||
label="Bio bevorzugen"
|
||||
onValueChange={(value) => updatePreference('preferBio', value)}
|
||||
value={preferences.preferBio}
|
||||
/>
|
||||
|
||||
<SettingsSwitchRow
|
||||
description="Es werden nur lokal gespeicherte Produkte bewertet."
|
||||
label="Offline-Modus"
|
||||
onValueChange={(value) => updatePreference('offlineModeEnabled', value)}
|
||||
value={preferences.offlineModeEnabled}
|
||||
/>
|
||||
|
||||
<SectionTitle>Prioritäten</SectionTitle>
|
||||
<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)}
|
||||
@@ -102,10 +103,51 @@ export default function ProfileScreen({ preferences, onChangePreferences }) {
|
||||
onChange={(value) => updatePreference('sustainabilityPriority', value)}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
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 }) {
|
||||
return (
|
||||
<View style={styles.priorityRow}>
|
||||
@@ -117,10 +159,10 @@ function PrioritySelector({ label, value, onChange }) {
|
||||
accessibilityState={{ selected: value === option }}
|
||||
key={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]}>
|
||||
{option === 'low' ? 'Niedrig' : option === 'medium' ? 'Mittel' : 'Hoch'}
|
||||
<Text style={[styles.chipText, value === option && styles.activeChipText]}>
|
||||
{priorityLabels[option]}
|
||||
</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
@@ -130,24 +172,62 @@ function PrioritySelector({ label, value, onChange }) {
|
||||
}
|
||||
|
||||
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: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: 8
|
||||
},
|
||||
sectionHint: {
|
||||
color: colors.muted,
|
||||
fontSize: 13,
|
||||
lineHeight: 19,
|
||||
marginTop: -10
|
||||
},
|
||||
chip: {
|
||||
backgroundColor: colors.surface,
|
||||
borderColor: colors.border,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
justifyContent: 'center',
|
||||
minHeight: 44,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10
|
||||
paddingVertical: 9
|
||||
},
|
||||
activeChip: {
|
||||
backgroundColor: colors.primary,
|
||||
@@ -162,35 +242,26 @@ const styles = StyleSheet.create({
|
||||
fontSize: 14,
|
||||
fontWeight: '800'
|
||||
},
|
||||
activeChipText: {
|
||||
color: colors.surface
|
||||
},
|
||||
dangerChipText: {
|
||||
color: colors.danger
|
||||
},
|
||||
priorityRow: {
|
||||
backgroundColor: colors.surface,
|
||||
borderColor: colors.border,
|
||||
borderRadius: 16,
|
||||
borderWidth: 1,
|
||||
gap: 10,
|
||||
padding: 14
|
||||
},
|
||||
activeChipText: { color: colors.surface },
|
||||
dangerChipText: { color: colors.danger },
|
||||
priorityRow: { gap: 8 },
|
||||
priorityLabel: {
|
||||
color: colors.text,
|
||||
fontSize: 15,
|
||||
fontWeight: '800'
|
||||
fontSize: 14,
|
||||
fontWeight: '700'
|
||||
},
|
||||
priorityOptions: {
|
||||
flexDirection: 'row',
|
||||
gap: 8
|
||||
},
|
||||
priorityChip: {
|
||||
alignItems: 'center',
|
||||
borderColor: colors.border,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
flex: 1,
|
||||
paddingVertical: 10,
|
||||
alignItems: 'center'
|
||||
justifyContent: 'center',
|
||||
minHeight: 44,
|
||||
paddingHorizontal: 4
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user