commits
tags
import { useState } from 'react'
import { View, Text, ScrollView, TouchableOpacity, StyleSheet, Modal, TextInput, FlatList } from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'
import { Plus, X, ChevronLeft } from 'lucide-react-native'
import { useTheme } from '../../src/hooks/useTheme'
import { useProfileStore } from '../../src/store/useProfileStore'
import { useNutritionStore, todayFoodLog } from '../../src/store/useNutritionStore'
import { FOODS } from '../../src/constants/foods'
import { FoodLogEntry, FoodItem } from '../../src/constants/types'
import { today } from '../../src/lib/macroCalc'
const MEALS: { key: FoodLogEntry['meal']; label: string; icon: string }[] = [
{ key: 'breakfast', label: 'Frukost', icon: '🌅' },
{ key: 'lunch', label: 'Lunch', icon: '☀️' },
{ key: 'dinner', label: 'Middag', icon: '🌙' },
{ key: 'snack', label: 'Mellanmål', icon: '🍎' },
]
function AddFoodModal({ meal, onClose }: { meal: FoodLogEntry['meal']; onClose: () => void }) {
const theme = useTheme()
const addFoodEntry = useNutritionStore((s) => s.addFoodEntry)
const customFoods = useNutritionStore((s) => s.customFoods)
const addCustomFood = useNutritionStore((s) => s.addCustomFood)
const removeCustomFood = useNutritionStore((s) => s.removeCustomFood)
const [mode, setMode] = useState<'list' | 'create'>('list')
const [search, setSearch] = useState('')
const [selected, setSelected] = useState<FoodItem | null>(null)
const [amount, setAmount] = useState('100')
const [cfName, setCfName] = useState('')
const [cfCal, setCfCal] = useState('')
const [cfProt, setCfProt] = useState('')
const [cfCarbs, setCfCarbs] = useState('')
const [cfFat, setCfFat] = useState('')
const allFoods = [...FOODS, ...customFoods]
const filtered = allFoods.filter((f) => !search || f.name.toLowerCase().includes(search.toLowerCase()))
const macros = selected ? {
cal: Math.round(selected.cal * Number(amount) / 100),
prot: Math.round(selected.prot * Number(amount) / 100 * 10) / 10,
carbs: Math.round(selected.carbs * Number(amount) / 100 * 10) / 10,
fat: Math.round(selected.fat * Number(amount) / 100 * 10) / 10,
} : null
const confirm = () => {
if (!selected || !amount) return
const g = Number(amount)
if (!g) return
addFoodEntry({
id: `fl${Date.now()}`,
date: today(),
meal,
foodId: selected.id,
foodName: selected.name,
amountG: g,
cal: Math.round(selected.cal * g / 100),
prot: Math.round(selected.prot * g / 100 * 10) / 10,
carbs: Math.round(selected.carbs * g / 100 * 10) / 10,
fat: Math.round(selected.fat * g / 100 * 10) / 10,
})
onClose()
}
const saveCustomFood = async () => {
if (!cfName.trim() || !cfCal) return
await addCustomFood({
id: `cf${Date.now()}`,
name: cfName.trim(),
cal: Number(cfCal) || 0,
prot: Number(cfProt) || 0,
carbs: Number(cfCarbs) || 0,
fat: Number(cfFat) || 0,
isCustom: true,
})
setMode('list')
setCfName(''); setCfCal(''); setCfProt(''); setCfCarbs(''); setCfFat('')
}
const s = StyleSheet.create({
overlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'flex-end' },
sheet: { backgroundColor: theme.card, borderTopLeftRadius: 24, borderTopRightRadius: 24, padding: 20, paddingBottom: 40, maxHeight: '90%' },
handle: { width: 40, height: 4, backgroundColor: theme.border, borderRadius: 2, alignSelf: 'center', marginBottom: 16 },
title: { color: theme.text, fontSize: 18, fontWeight: '800', marginBottom: 12 },
searchWrap: { flexDirection: 'row', alignItems: 'center', backgroundColor: theme.bg, borderRadius: 10, paddingHorizontal: 12, height: 42, borderWidth: 1, borderColor: theme.border, marginBottom: 12 },
searchInput: { flex: 1, color: theme.text, fontSize: 15 },
foodItem: { paddingVertical: 10, paddingHorizontal: 4, borderBottomWidth: 1, borderBottomColor: theme.border, flexDirection: 'row' as const, alignItems: 'center' as const },
foodItemActive: { backgroundColor: theme.accentLight },
foodName: { color: theme.text, fontSize: 14, fontWeight: '500' as const },
foodNameActive: { color: theme.accent, fontWeight: '700' as const },
foodMeta: { color: theme.muted, fontSize: 12, marginRight: 4 },
preview: { backgroundColor: theme.bg, borderRadius: 12, padding: 14, marginTop: 12, gap: 8 },
previewTitle: { color: theme.text, fontSize: 14, fontWeight: '700', marginBottom: 4 },
previewRow: { flexDirection: 'row' as const, justifyContent: 'space-between' as const },
previewLabel: { color: theme.muted, fontSize: 13 },
previewVal: { color: theme.text, fontSize: 13, fontWeight: '600' },
amountRow: { flexDirection: 'row' as const, alignItems: 'center' as const, gap: 8, marginBottom: 12 },
amountLabel: { color: theme.text, fontSize: 14 },
amountInput: { backgroundColor: theme.bg, borderRadius: 10, paddingHorizontal: 12, paddingVertical: 10, color: theme.text, fontSize: 15, width: 80, borderWidth: 1, borderColor: theme.border, textAlign: 'center' as const },
confirmBtn: { backgroundColor: theme.accent, borderRadius: 12, paddingVertical: 14, alignItems: 'center' as const, marginTop: 12 },
confirmBtnText: { color: '#FFF', fontSize: 15, fontWeight: '700' },
fieldLabel: { color: theme.muted, fontSize: 11, fontWeight: '700' as const, letterSpacing: 0.8, textTransform: 'uppercase' as const, marginBottom: 6 },
fieldInput: { backgroundColor: theme.bg, borderRadius: 10, paddingHorizontal: 12, paddingVertical: 10, color: theme.text, fontSize: 15, borderWidth: 1, borderColor: theme.border, marginBottom: 12 },
})
return (
<Modal visible transparent animationType="slide" onRequestClose={onClose}>
<TouchableOpacity style={s.overlay} activeOpacity={1} onPress={onClose}>
<TouchableOpacity activeOpacity={1}>
<View style={s.sheet}>
<View style={s.handle} />
{mode === 'create' ? (
<ScrollView keyboardShouldPersistTaps="handled" showsVerticalScrollIndicator={false}>
<TouchableOpacity onPress={() => setMode('list')} style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 14 }}>
<ChevronLeft size={18} color={theme.accent} />
<Text style={{ color: theme.accent, fontSize: 14, fontWeight: '600' }}>Tillbaka</Text>
</TouchableOpacity>
<Text style={s.title}>Nytt livsmedel</Text>
<Text style={s.fieldLabel}>Namn</Text>
<TextInput style={s.fieldInput} value={cfName} onChangeText={setCfName} placeholder="Ex. Hemmagjord granola" placeholderTextColor={theme.muted} />
<Text style={s.fieldLabel}>Kalorier (per 100g)</Text>
<TextInput style={s.fieldInput} value={cfCal} onChangeText={setCfCal} keyboardType="decimal-pad" placeholder="kcal" placeholderTextColor={theme.muted} />
<View style={{ flexDirection: 'row', gap: 10 }}>
{[['Protein (g)', cfProt, setCfProt], ['Kolhydrater (g)', cfCarbs, setCfCarbs], ['Fett (g)', cfFat, setCfFat]].map(([label, val, setter]) => (
<View key={label as string} style={{ flex: 1 }}>
<Text style={s.fieldLabel}>{label as string}</Text>
<TextInput style={s.fieldInput} value={val as string} onChangeText={setter as (v: string) => void} keyboardType="decimal-pad" placeholder="g" placeholderTextColor={theme.muted} />
</View>
))}
</View>
<TouchableOpacity style={s.confirmBtn} onPress={saveCustomFood}>
<Text style={s.confirmBtnText}>Spara livsmedel</Text>
</TouchableOpacity>
</ScrollView>
) : (
<>
<Text style={s.title}>Lägg till mat</Text>
<View style={s.searchWrap}>
<TextInput style={s.searchInput} value={search} onChangeText={setSearch} placeholder="Sök livsmedel..." placeholderTextColor={theme.muted} />
</View>
<FlatList
data={filtered}
keyExtractor={(f) => f.id}
style={{ maxHeight: 200 }}
renderItem={({ item: f }) => (
<TouchableOpacity style={[s.foodItem, selected?.id === f.id && s.foodItemActive]} onPress={() => setSelected(f)}>
<View style={{ flex: 1 }}>
<Text style={[s.foodName, selected?.id === f.id && s.foodNameActive]}>{f.name}</Text>
{f.isCustom && <Text style={{ color: theme.muted, fontSize: 10, fontWeight: '700' }}>EGET</Text>}
</View>
<Text style={s.foodMeta}>{f.cal} kcal/100g</Text>
{f.isCustom && (
<TouchableOpacity
onPress={() => { removeCustomFood(f.id); if (selected?.id === f.id) setSelected(null) }}
style={{ padding: 6 }}
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
>
<X size={14} color={theme.muted} />
</TouchableOpacity>
)}
</TouchableOpacity>
)}
ListFooterComponent={
<TouchableOpacity onPress={() => setMode('create')} style={{ flexDirection: 'row', alignItems: 'center', gap: 6, paddingVertical: 12, paddingHorizontal: 4 }}>
<Plus size={15} color={theme.accent} />
<Text style={{ color: theme.accent, fontSize: 14, fontWeight: '600' }}>Skapa eget livsmedel</Text>
</TouchableOpacity>
}
/>
{selected && (
<View style={s.preview}>
<Text style={s.previewTitle}>{selected.name}</Text>
<View style={s.amountRow}>
<Text style={s.amountLabel}>Mängd (g):</Text>
<TextInput style={s.amountInput} value={amount} onChangeText={setAmount} keyboardType="numeric" />
</View>
{macros && (
<>
<View style={s.previewRow}><Text style={s.previewLabel}>Kalorier</Text><Text style={s.previewVal}>{macros.cal} kcal</Text></View>
<View style={s.previewRow}><Text style={s.previewLabel}>Protein</Text><Text style={s.previewVal}>{macros.prot}g</Text></View>
<View style={s.previewRow}><Text style={s.previewLabel}>Kolhydrater</Text><Text style={s.previewVal}>{macros.carbs}g</Text></View>
<View style={s.previewRow}><Text style={s.previewLabel}>Fett</Text><Text style={s.previewVal}>{macros.fat}g</Text></View>
</>
)}
<TouchableOpacity style={s.confirmBtn} onPress={confirm}>
<Text style={s.confirmBtnText}>Lägg till</Text>
</TouchableOpacity>
</View>
)}
</>
)}
</View>
</TouchableOpacity>
</TouchableOpacity>
</Modal>
)
}
export default function NutritionScreen() {
const theme = useTheme()
const macroGoals = useProfileStore((s) => s.macroGoals)
const foodLog = useNutritionStore((s) => s.foodLog)
const removeFoodEntry = useNutritionStore((s) => s.removeFoodEntry)
const [addMeal, setAddMeal] = useState<FoodLogEntry['meal'] | null>(null)
const log = todayFoodLog(foodLog)
const eaten = { cal: log.reduce((s, e) => s + e.cal, 0), prot: log.reduce((s, e) => s + e.prot, 0), carbs: log.reduce((s, e) => s + e.carbs, 0), fat: log.reduce((s, e) => s + e.fat, 0) }
const s = StyleSheet.create({
safe: { flex: 1, backgroundColor: theme.outer },
header: { backgroundColor: theme.header, paddingHorizontal: 22, paddingTop: 12, paddingBottom: 20 },
title: { color: theme.headerText, fontSize: 24, fontWeight: '800' },
scroll: { flex: 1 },
summaryCard: { backgroundColor: theme.card, margin: 16, borderRadius: 16, padding: 20 },
summaryTitle: { color: theme.text, fontSize: 16, fontWeight: '800', marginBottom: 12 },
calRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 4 },
calNum: { color: theme.accent, fontSize: 28, fontWeight: '900' },
calGoal: { color: theme.muted, fontSize: 14 },
macroRow: { flexDirection: 'row', gap: 8, marginTop: 12 },
macroBox: { flex: 1, backgroundColor: theme.bg, borderRadius: 10, padding: 10, alignItems: 'center' },
macroVal: { color: theme.text, fontSize: 16, fontWeight: '800' },
macroLabel: { color: theme.muted, fontSize: 11, marginTop: 2 },
mealCard: { backgroundColor: theme.card, marginHorizontal: 16, marginBottom: 10, borderRadius: 16, overflow: 'hidden' },
mealHeader: { flexDirection: 'row', alignItems: 'center', padding: 16, borderBottomWidth: 1, borderBottomColor: theme.border },
mealIcon: { fontSize: 20, marginRight: 8 },
mealName: { color: theme.text, fontSize: 16, fontWeight: '700', flex: 1 },
mealCal: { color: theme.muted, fontSize: 13 },
addMealBtn: { padding: 4 },
foodEntryRow: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 10, gap: 8 },
foodEntryName: { flex: 1, color: theme.text, fontSize: 14 },
foodEntryMeta: { color: theme.muted, fontSize: 12 },
removeBtn: { padding: 4 },
spacer: { height: 24 },
})
return (
<SafeAreaView style={s.safe} edges={['top']}>
<View style={s.header}>
<Text style={s.title}>Kost</Text>
</View>
<ScrollView style={s.scroll} contentContainerStyle={{ paddingBottom: 32 }} showsVerticalScrollIndicator={false}>
<View style={s.summaryCard}>
<Text style={s.summaryTitle}>Idag</Text>
<View style={s.calRow}>
<Text style={s.calNum}>{eaten.cal}</Text>
<Text style={s.calGoal}>/ {macroGoals.calories} kcal</Text>
</View>
<View style={s.macroRow}>
{[
{ label: 'Protein', val: eaten.prot, goal: macroGoals.proteinG },
{ label: 'Kolh.', val: eaten.carbs, goal: macroGoals.carbsG },
{ label: 'Fett', val: eaten.fat, goal: macroGoals.fatG },
].map((m) => (
<View key={m.label} style={s.macroBox}>
<Text style={s.macroVal}>{m.val}g</Text>
<Text style={s.macroLabel}>{m.label}</Text>
</View>
))}
</View>
</View>
{MEALS.map((meal) => {
const entries = log.filter((e) => e.meal === meal.key)
const mealCal = entries.reduce((s, e) => s + e.cal, 0)
return (
<View key={meal.key} style={s.mealCard}>
<View style={s.mealHeader}>
<Text style={s.mealIcon}>{meal.icon}</Text>
<Text style={s.mealName}>{meal.label}</Text>
{mealCal > 0 && <Text style={s.mealCal}>{mealCal} kcal</Text>}
<TouchableOpacity style={s.addMealBtn} onPress={() => setAddMeal(meal.key)}>
<Plus size={20} color={theme.accent} />
</TouchableOpacity>
</View>
{entries.map((e) => (
<View key={e.id} style={s.foodEntryRow}>
<Text style={s.foodEntryName}>{e.foodName}</Text>
<Text style={s.foodEntryMeta}>{e.amountG}g · {e.cal} kcal</Text>
<TouchableOpacity style={s.removeBtn} onPress={() => removeFoodEntry(e.id)}>
<X size={16} color={theme.muted} />
</TouchableOpacity>
</View>
))}
</View>
)
})}
<View style={s.spacer} />
</ScrollView>
{addMeal && <AddFoodModal meal={addMeal} onClose={() => setAddMeal(null)} />}
</SafeAreaView>
)
}