import { useEffect, useMemo, useState } from 'react';
import { BackHandler, SafeAreaView, StyleSheet, Text, View } from 'react-native';
import { StatusBar } from 'expo-status-bar';
import BottomTabBar from './src/components/layout/BottomTabBar';
import AlternativesScreen from './src/screens/AlternativesScreen';
import HomeScreen from './src/screens/HomeScreen';
import ProductDetailScreen from './src/screens/ProductDetailScreen';
import ProfileScreen from './src/screens/ProfileScreen';
import RatingCriteriaDetailScreen from './src/screens/RatingCriteriaDetailScreen';
import ScanScreen from './src/screens/ScanScreen';
import ShoppingListScreen from './src/screens/ShoppingListScreen';
import { lookupProductByBarcode } from './src/services/productApi';
import { storageService } from './src/services/storageService';
import { colors, defaultPreferences } from './src/utils/constants';
const initialShoppingList = {
id: 'active-list',
title: 'Aktueller Einkauf',
items: [],
estimatedTotal: 0,
currency: 'CHF',
status: 'active',
createdAt: new Date().toISOString()
};
export default function App() {
const [activeTab, setActiveTab] = useState('home');
const [route, setRoute] = useState({ name: 'tabs' });
const [preferences, setPreferences] = useState(defaultPreferences);
const [shoppingList, setShoppingList] = useState(initialShoppingList);
const [productCache, setProductCache] = useState({});
const [manualPrices, setManualPrices] = useState({});
const [recentProducts, setRecentProducts] = useState([]);
const [isHydrated, setIsHydrated] = useState(false);
const [storageError, setStorageError] = useState('');
useEffect(() => {
let isMounted = true;
async function hydrate() {
try {
const [storedPreferences, storedShoppingList, storedProductCache, storedManualPrices, storedRecentProducts] = await Promise.all([
storageService.loadPreferences(defaultPreferences),
storageService.loadShoppingList(initialShoppingList),
storageService.loadProductCache(),
storageService.loadManualPrices(),
storageService.loadRecentProducts()
]);
if (isMounted) {
setPreferences({ ...defaultPreferences, ...storedPreferences });
setShoppingList({ ...initialShoppingList, ...storedShoppingList, items: storedShoppingList?.items || [] });
setProductCache(storedProductCache || {});
setManualPrices(storedManualPrices || {});
setRecentProducts(Array.isArray(storedRecentProducts) ? storedRecentProducts : []);
setIsHydrated(true);
}
} catch (error) {
if (isMounted) setStorageError('Deine lokalen Daten konnten nicht geladen werden.');
}
}
hydrate();
return () => {
isMounted = false;
};
}, []);
function retryHydration() {
setStorageError('');
setIsHydrated(false);
// Reloading the app is not necessary for the normal path, but this keeps
// the retry action explicit if a storage adapter fails temporarily.
Promise.all([
storageService.loadPreferences(defaultPreferences),
storageService.loadShoppingList(initialShoppingList),
storageService.loadProductCache(),
storageService.loadManualPrices(),
storageService.loadRecentProducts()
]).then(([nextPreferences, nextList, nextCache, nextPrices, nextRecent]) => {
setPreferences({ ...defaultPreferences, ...nextPreferences });
setShoppingList({ ...initialShoppingList, ...nextList, items: nextList?.items || [] });
setProductCache(nextCache || {});
setManualPrices(nextPrices || {});
setRecentProducts(Array.isArray(nextRecent) ? nextRecent : []);
setIsHydrated(true);
}).catch(() => setStorageError('Bitte starte Shoppinguin erneut, um die lokalen Daten zu laden.'));
}
useEffect(() => {
if (isHydrated) storageService.savePreferences(preferences);
}, [isHydrated, preferences]);
useEffect(() => {
if (isHydrated) storageService.saveShoppingList(shoppingList);
}, [isHydrated, shoppingList]);
useEffect(() => {
if (isHydrated) storageService.saveProductCache(productCache);
}, [isHydrated, productCache]);
useEffect(() => {
if (isHydrated) storageService.saveManualPrices(manualPrices);
}, [isHydrated, manualPrices]);
useEffect(() => {
if (isHydrated) storageService.saveRecentProducts(recentProducts);
}, [isHydrated, recentProducts]);
const routeProduct = route.product;
useEffect(() => {
const subscription = BackHandler.addEventListener('hardwareBackPress', goBackFromRoute);
return () => subscription.remove();
}, [route]);
const tabScreen = useMemo(() => {
if (activeTab === 'scan') {
return (
);
}
if (activeTab === 'list') {
return (
setActiveTab('scan')}
onToggleItem={toggleListItem}
onUpdateItemPrice={updateListItemPrice}
preferences={preferences}
shoppingList={shoppingList}
/>
);
}
if (activeTab === 'profile') {
return (
);
}
return (
setActiveTab('list')}
onStartScan={() => setActiveTab('scan')}
preferences={preferences}
recentProducts={recentProducts}
shoppingList={shoppingList}
/>
);
}, [activeTab, manualPrices, preferences, productCache, recentProducts, shoppingList]);
async function scanProductByBarcode(barcode) {
const result = await lookupProductByBarcode(
barcode,
productCache,
preferences.offlineModeEnabled
);
if (result.status === 'found') {
const pricedProduct = applyManualPrice(result.product);
openProduct(pricedProduct);
}
return result;
}
function openProduct(product) {
const pricedProduct = applyManualPrice(product);
addProductToCache(pricedProduct);
addRecentProduct(pricedProduct);
setRoute({ name: 'productDetail', product: pricedProduct });
}
function showAlternatives(product) {
setRoute({ name: 'alternatives', product });
}
function showRatingCriteria(product, category) {
setRoute({ name: 'ratingCriteria', product, category });
}
function addProductToCache(product) {
setProductCache((current) => ({
...current,
[product.barcode]: product
}));
}
function addRecentProduct(product) {
setRecentProducts((current) => {
const withoutDuplicate = current.filter((item) => item.id !== product.id);
const previous = current.find((item) => item.id === product.id);
const scanCount = (previous?.scanCount || 0) + 1;
return [{ ...product, scanCount, lastScannedAt: new Date().toISOString() }, ...withoutDuplicate].slice(0, 8);
});
}
function applyManualPrice(product) {
const manualPrice = manualPrices[priceKey(product)];
if (typeof manualPrice !== 'number') {
return product;
}
return {
...product,
currency: product.currency || preferences.currency,
price: manualPrice
};
}
function saveManualPriceForProduct(product, price) {
const key = priceKey(product);
setManualPrices((current) => ({
...current,
[key]: price
}));
setProductCache((current) => {
const cachedProduct = current[product.barcode];
if (!cachedProduct) {
return current;
}
return {
...current,
[product.barcode]: {
...cachedProduct,
currency: product.currency || preferences.currency,
price,
updatedAt: new Date().toISOString()
}
};
});
setRecentProducts((current) =>
current.map((item) =>
item.id === product.id
? { ...item, currency: product.currency || preferences.currency, price }
: item
)
);
}
function addProductToShoppingList(product) {
setShoppingList((current) => {
const existingItem = current.items.find((item) => item.productId === product.id);
const items = existingItem
? current.items.map((item) =>
item.productId === product.id
? {
...item,
quantity: item.quantity + 1,
packageQuantityGrams: item.packageQuantityGrams ?? product.packageQuantityGrams,
packageQuantityLabel: item.packageQuantityLabel ?? product.packageQuantityLabel,
servingQuantityGrams: item.servingQuantityGrams ?? product.servingQuantityGrams,
nutrition: item.nutrition ?? product.nutrition
}
: item
)
: [
...current.items,
{
id: `item-${Date.now()}`,
productId: product.id,
productName: product.name,
quantity: 1,
estimatedPrice: product.price,
currency: product.currency || current.currency,
barcode: product.barcode,
packageQuantityGrams: product.packageQuantityGrams,
packageQuantityLabel: product.packageQuantityLabel,
servingQuantityGrams: product.servingQuantityGrams,
nutrition: product.nutrition,
checked: false,
addedAt: new Date().toISOString()
}
];
return {
...current,
currency: product.currency || current.currency,
estimatedTotal: calculateEstimatedTotal(items),
items
};
});
}
function updateListItemPrice(itemId, price) {
const itemToUpdate = shoppingList.items.find((item) => item.id === itemId);
if (itemToUpdate) {
saveManualPriceForProduct({
id: itemToUpdate.productId,
barcode: itemToUpdate.barcode,
currency: itemToUpdate.currency || shoppingList.currency
}, price);
}
setShoppingList((current) => {
const items = current.items.map((item) =>
item.id === itemId ? { ...item, estimatedPrice: price } : item
);
return {
...current,
estimatedTotal: calculateEstimatedTotal(items),
items
};
});
}
function toggleListItem(itemId) {
setShoppingList((current) => ({
...current,
items: current.items.map((item) =>
item.id === itemId ? { ...item, checked: !item.checked } : item
)
}));
}
function deleteListItem(itemId) {
setShoppingList((current) => {
const items = current.items.filter((item) => item.id !== itemId);
return {
...current,
estimatedTotal: calculateEstimatedTotal(items),
items
};
});
}
function completeShoppingList() {
setShoppingList({
...initialShoppingList,
createdAt: new Date().toISOString()
});
}
function goBackToTabs() {
setRoute({ name: 'tabs' });
}
function goBackFromRoute() {
if (route.name === 'ratingCriteria' && routeProduct) {
setRoute({ name: 'productDetail', product: routeProduct });
return true;
}
if (route.name === 'alternatives' && routeProduct) {
setRoute({ name: 'productDetail', product: routeProduct });
return true;
}
if (route.name === 'productDetail' && routeProduct) {
setRoute({ name: 'tabs' });
return true;
}
return false;
}
function renderRoute() {
if (!isHydrated) {
return (
{storageError || 'Shoppinguin wird geladen...'}
{storageError ? Erneut versuchen : null}
);
}
if (route.name === 'productDetail' && routeProduct) {
return (
);
}
if (route.name === 'ratingCriteria' && routeProduct) {
return (
);
}
if (route.name === 'alternatives' && routeProduct) {
return (
{
addProductToShoppingList(product);
setActiveTab('list');
setRoute({ name: 'tabs' });
}}
onOpenProduct={openProduct}
preferences={preferences}
product={routeProduct}
productCache={productCache}
/>
);
}
return tabScreen;
}
return (
{renderRoute()}
{route.name === 'tabs' && isHydrated ? (
) : null}
);
}
function calculateEstimatedTotal(items) {
return items.reduce((sum, item) => {
const price = typeof item.estimatedPrice === 'number' ? item.estimatedPrice : 0;
return sum + price * item.quantity;
}, 0);
}
function priceKey(product) {
return product.barcode || product.id;
}
const styles = StyleSheet.create({
container: {
backgroundColor: colors.background,
flex: 1
},
loading: {
alignItems: 'center',
flex: 1,
justifyContent: 'center',
padding: 24
},
loadingText: {
color: colors.muted,
fontSize: 16,
fontWeight: '700'
},
retryText: {
color: colors.primary,
fontSize: 15,
fontWeight: '800',
marginTop: 16
}
});