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 (
|
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
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user