foxygit / FORTIS Log in
commits tags

/fortis/app/(tabs)/nutrition.tsx · 15.83 KB

raw
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>
  )
}