Files
Shoppy/src/screens/ProductDetailScreen.js

299 lines
9.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useMemo, useState } from 'react';
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
import PrimaryButton from '../components/PrimaryButton';
import SecondaryButton from '../components/SecondaryButton';
import AppHeader from '../components/layout/AppHeader';
import ScreenContainer from '../components/layout/ScreenContainer';
import SectionTitle from '../components/layout/SectionTitle';
import ProductImage from '../components/product/ProductImage';
import ProductScoreBadge from '../components/product/ProductScoreBadge';
import RatingCategoryRow from '../components/product/RatingCategoryRow';
import { calculateProductRating } from '../services/ratingService';
import { colors } from '../utils/constants';
import { formatPrice } from '../utils/formatPrice';
export default function ProductDetailScreen({
product,
preferences,
onBack,
onAddToList,
onOpenList,
onSaveManualPrice,
onShowRatingCriteria,
onShowAlternatives
}) {
const [savedMessage, setSavedMessage] = useState('');
const [manualPrice, setManualPrice] = useState('');
const [priceError, setPriceError] = useState('');
const hasProductPrice = typeof product.price === 'number';
const currency = product.currency || preferences.currency;
const parsedManualPrice = parseManualPrice(manualPrice);
const pricedProduct = useMemo(() => ({
...product,
currency,
price: hasProductPrice ? product.price : parsedManualPrice
}), [currency, hasProductPrice, parsedManualPrice, product]);
const rating = useMemo(() => calculateProductRating(pricedProduct, preferences), [pricedProduct, preferences]);
const recommendation = rating.overallStatus === 'good' ? 'Passt gut zu dir' : rating.overallStatus === 'medium' ? 'Genauer prüfen' : 'Lieber meiden';
function addToList() {
if (!hasProductPrice && manualPrice.trim() && parsedManualPrice === undefined) {
setSavedMessage('');
setPriceError('Bitte gib einen gültigen Preis ein oder lasse das Feld leer.');
return;
}
if (!hasProductPrice && typeof pricedProduct.price === 'number') {
onSaveManualPrice(pricedProduct, pricedProduct.price);
}
onAddToList(pricedProduct);
setPriceError('');
setSavedMessage('Zur Einkaufsliste hinzugefügt.');
}
return (
<ScreenContainer>
<AppHeader title="Produkt im Check" subtitle="Bewertung nach deinen Einstellungen" onBack={onBack} />
<View style={styles.productPanel}>
<ProductImage imageUrl={product.imageUrl} productName={product.name} size="large" />
<View style={styles.topRow}>
<View style={styles.copy}>
<Text style={styles.brand}>{product.brand || 'Ohne Marke'}</Text>
<Text style={styles.name}>{product.name}</Text>
<Text style={styles.meta}>
{product.category || 'Produkt'} · {formatPrice(pricedProduct.price, currency)}
</Text>
<Text style={styles.source}>{sourceLabel(product.dataSource)}</Text>
</View>
<ProductScoreBadge status={rating.overallStatus} score={rating.overallScore} />
</View>
<View style={[styles.recommendation, styles[`${rating.overallStatus}Recommendation`]]}>
<Text style={[styles.recommendationTitle, styles[`${rating.overallStatus}Text`]]}>{recommendation}</Text>
<Text style={styles.recommendationCopy}>Tippe unten auf ein Kriterium, um die Bewertung nachzuvollziehen.</Text>
</View>
</View>
{!hasProductPrice ? (
<View style={styles.pricePanel}>
<Text style={styles.priceTitle}>Ladenpreis ergänzen</Text>
<Text style={styles.priceCopy}>
Optional. Ohne Preis kannst du das Produkt speichern; die Einkaufssumme bleibt dann unvollständig.
</Text>
<View style={styles.priceInputRow}>
<Text style={styles.currency}>{currency}</Text>
<TextInput
accessibilityLabel="Ladenpreis"
keyboardType="decimal-pad"
onChangeText={(value) => {
setManualPrice(value);
setPriceError('');
setSavedMessage('');
}}
placeholder="z. B. 3.50"
placeholderTextColor={colors.muted}
style={styles.priceInput}
value={manualPrice}
/>
</View>
{priceError ? <Text style={styles.errorText}>{priceError}</Text> : null}
</View>
) : null}
{savedMessage ? (
<View style={styles.savedPanel}>
<Text style={styles.saved}>{savedMessage}</Text>
<Pressable accessibilityRole="button" onPress={onOpenList}>
<Text style={styles.savedLink}>Einkaufsliste ansehen ›</Text>
</Pressable>
</View>
) : null}
<PrimaryButton label="Zur Einkaufsliste hinzufügen" onPress={addToList} />
<SecondaryButton label="Bessere Alternativen suchen" onPress={() => onShowAlternatives(product)} />
<SectionTitle>Bewertungskriterien</SectionTitle>
{Object.entries(rating.categoryScores).map(([category, categoryRating]) => (
<RatingCategoryRow
category={category}
key={category}
onPress={() => onShowRatingCriteria(pricedProduct, category)}
rating={categoryRating}
/>
))}
<SectionTitle>Produktdaten</SectionTitle>
<View style={styles.dataPanel}>
<Text style={styles.dataText}>Barcode: {product.barcode}</Text>
<Text style={styles.dataText}>Zutaten: {(product.ingredients || []).join(', ') || 'Keine Angaben'}</Text>
<Text style={styles.dataText}>Allergene: {(product.allergens || []).join(', ') || 'Keine Angaben'}</Text>
<Text style={styles.dataText}>Nutri-Score: {product.nutrition?.nutriScore || 'Keine Angabe'}</Text>
<Text style={styles.dataText}>Eco-Score: {product.sustainability?.ecoScore || 'Keine Angabe'}</Text>
</View>
</ScreenContainer>
);
}
function parseManualPrice(value) {
const normalizedValue = value.trim().replace(',', '.');
const parsedValue = Number(normalizedValue);
if (!normalizedValue || !Number.isFinite(parsedValue) || parsedValue <= 0) {
return undefined;
}
return Math.round(parsedValue * 100) / 100;
}
function sourceLabel(source) {
const labels = {
cache: 'Quelle: lokaler Cache',
local: 'Quelle: lokale Beispieldaten',
'open-food-facts': 'Quelle: Open Food Facts'
};
return labels[source] || 'Quelle: unbekannt';
}
const styles = StyleSheet.create({
productPanel: {
backgroundColor: colors.surface,
borderColor: colors.border,
borderRadius: 20,
borderWidth: 1,
gap: 14,
padding: 18
},
topRow: {
alignItems: 'flex-start',
flexDirection: 'row',
gap: 12
},
copy: {
flex: 1
},
brand: {
color: colors.primary,
fontSize: 13,
fontWeight: '800',
textTransform: 'uppercase'
},
name: {
color: colors.text,
fontSize: 25,
fontWeight: '800',
lineHeight: 31,
marginTop: 4
},
meta: {
color: colors.muted,
fontSize: 15,
marginTop: 6
},
source: {
alignSelf: 'flex-start',
backgroundColor: '#eef3ef',
borderRadius: 8,
color: colors.primaryDark,
fontSize: 12,
fontWeight: '800',
marginTop: 10,
paddingHorizontal: 8,
paddingVertical: 4
},
recommendation: {
borderRadius: 14,
gap: 4,
padding: 14
},
goodRecommendation: { backgroundColor: '#e4f4e9' },
mediumRecommendation: { backgroundColor: '#fff3d7' },
criticalRecommendation: { backgroundColor: '#fee9e7' },
recommendationTitle: {
fontSize: 17,
fontWeight: '800'
},
goodText: { color: colors.success },
mediumText: { color: '#8a5914' },
criticalText: { color: colors.danger },
recommendationCopy: {
color: colors.text,
fontSize: 13,
lineHeight: 19
},
pricePanel: {
backgroundColor: colors.surface,
borderColor: colors.border,
borderRadius: 18,
borderWidth: 1,
gap: 10,
padding: 16
},
priceTitle: {
color: colors.text,
fontSize: 17,
fontWeight: '800'
},
priceCopy: {
color: colors.muted,
fontSize: 14,
lineHeight: 20
},
priceInputRow: {
alignItems: 'center',
flexDirection: 'row',
gap: 10
},
currency: {
color: colors.muted,
fontSize: 16,
fontWeight: '800'
},
priceInput: {
borderColor: colors.border,
borderRadius: 12,
borderWidth: 1,
color: colors.text,
flex: 1,
fontSize: 16,
paddingHorizontal: 12,
paddingVertical: 10
},
errorText: {
color: colors.danger,
fontSize: 13,
fontWeight: '800'
},
dataPanel: {
backgroundColor: colors.surface,
borderColor: colors.border,
borderRadius: 18,
borderWidth: 1,
gap: 8,
padding: 14
},
dataText: {
color: colors.muted,
fontSize: 14,
lineHeight: 20
},
saved: {
color: colors.success,
fontSize: 14,
fontWeight: '800'
},
savedPanel: {
backgroundColor: colors.surfaceTint,
borderRadius: 14,
gap: 6,
padding: 14
},
savedLink: {
color: colors.primary,
fontSize: 14,
fontWeight: '800'
}
});