foxygit / FORTIS Log in
commits tags

/fortis/app/(tabs)/library.tsx · 18.38 KB

raw
import { useState } from 'react'
import { View, Text, TextInput, ScrollView, TouchableOpacity, StyleSheet, Modal, FlatList } from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'
import { Search, SlidersHorizontal, Plus } from 'lucide-react-native'
import { useTheme } from '../../src/hooks/useTheme'
import { useWorkoutStore } from '../../src/store/useWorkoutStore'
import { EXERCISES, EQUIP_CHIPS, MUSCLE_GROUPS, DIFFICULTIES } from '../../src/constants/exercises'
import { Exercise } from '../../src/constants/types'

const EQUIP_BG: Record<string, string> = {
  Kroppsvikt: '#FEF0EB', Hantel: '#EBF0FE', Kettlebell: '#EAFAEE',
  Skivstång: '#FFF8E6', Kabelmaskin: '#F3EBFE', Maskin: '#F0F0EE',
}

function ExerciseDetailModal({ ex, onClose, onDelete }: { ex: Exercise; onClose: () => void; onDelete?: () => void }) {
  const t = useTheme()
  return (
    <Modal visible transparent animationType="slide" onRequestClose={onClose}>
      <TouchableOpacity style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'flex-end' }} activeOpacity={1} onPress={onClose}>
        <View>
          <ScrollView style={{ backgroundColor: t.card, borderTopLeftRadius: 24, borderTopRightRadius: 24, padding: 24, maxHeight: '90%' }} contentContainerStyle={{ paddingBottom: 40 }}>
            <View style={{ width: 40, height: 4, backgroundColor: t.border, borderRadius: 2, alignSelf: 'center', marginBottom: 16 }} />
            <Text style={{ color: t.text, fontSize: 20, fontWeight: '800', marginBottom: 12 }}>{ex.name}</Text>
            <View style={{ flexDirection: 'row', gap: 8, flexWrap: 'wrap', marginBottom: 16 }}>
              <View style={{ backgroundColor: t.accentLight, borderRadius: 8, paddingHorizontal: 10, paddingVertical: 4 }}><Text style={{ fontSize: 12, fontWeight: '700', color: t.accent }}>{ex.muscle}</Text></View>
              <View style={{ backgroundColor: t.bg, borderRadius: 8, paddingHorizontal: 10, paddingVertical: 4 }}><Text style={{ fontSize: 12, fontWeight: '700', color: t.muted }}>{ex.equip}</Text></View>
              <View style={{ backgroundColor: t.bg, borderRadius: 8, paddingHorizontal: 10, paddingVertical: 4 }}><Text style={{ fontSize: 12, fontWeight: '700', color: t.muted }}>{ex.diff}</Text></View>
              {ex.isCustom && <View style={{ backgroundColor: t.accentLight, borderRadius: 8, paddingHorizontal: 10, paddingVertical: 4 }}><Text style={{ fontSize: 12, fontWeight: '700', color: t.accent }}>EIGEN</Text></View>}
            </View>
            {ex.secondary ? (
              <>
                <Text style={{ color: t.muted, fontSize: 11, fontWeight: '700', letterSpacing: 0.8, textTransform: 'uppercase', marginBottom: 6 }}>Sekundära muskler</Text>
                <Text style={{ color: t.text, fontSize: 14, marginBottom: 16 }}>{ex.secondary}</Text>
              </>
            ) : null}
            <Text style={{ color: t.muted, fontSize: 11, fontWeight: '700', letterSpacing: 0.8, textTransform: 'uppercase', marginBottom: 8 }}>Utförande</Text>
            {ex.instructions.map((step, i) => (
              <View key={i} style={{ flexDirection: 'row', gap: 10, marginBottom: 10 }}>
                <View style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: t.accent, alignItems: 'center', justifyContent: 'center' }}><Text style={{ color: '#FFF', fontSize: 11, fontWeight: '700' }}>{i + 1}</Text></View>
                <Text style={{ flex: 1, color: t.text, fontSize: 14, lineHeight: 20 }}>{step}</Text>
              </View>
            ))}
            {ex.isCustom && onDelete && (
              <TouchableOpacity style={{ backgroundColor: '#FEE2E2', borderRadius: 12, paddingVertical: 14, alignItems: 'center', marginTop: 16 }} onPress={onDelete}>
                <Text style={{ color: '#E53E3E', fontSize: 15, fontWeight: '700' }}>🗑 Ta bort övning</Text>
              </TouchableOpacity>
            )}
            <TouchableOpacity style={{ backgroundColor: t.bg, borderRadius: 12, paddingVertical: 14, alignItems: 'center', marginTop: 8 }} onPress={onClose}>
              <Text style={{ color: t.text, fontSize: 15, fontWeight: '700' }}>Stäng</Text>
            </TouchableOpacity>
          </ScrollView>
        </View>
      </TouchableOpacity>
    </Modal>
  )
}

function Chip({ label, active, onPress, theme }: { label: string; active: boolean; onPress: () => void; theme: ReturnType<typeof useTheme> }) {
  return (
    <TouchableOpacity
      onPress={onPress}
      style={{ paddingHorizontal: 12, paddingVertical: 6, borderRadius: 20, backgroundColor: active ? theme.accent : theme.bg, borderWidth: 1, borderColor: active ? theme.accent : theme.border }}
    >
      <Text style={{ fontSize: 13, fontWeight: '600', color: active ? '#FFF' : theme.text }}>{label}</Text>
    </TouchableOpacity>
  )
}

function CreateExerciseModal({ onClose, onSave }: { onClose: () => void; onSave: (ex: Exercise) => void }) {
  const t = useTheme()
  const [name, setName] = useState('')
  const [icon, setIcon] = useState('')
  const [muscle, setMuscle] = useState('Bröst')
  const [secondary, setSecondary] = useState('')
  const [equip, setEquip] = useState('Kroppsvikt')
  const [diff, setDiff] = useState<Exercise['diff']>('Nybörjare')
  const [type, setType] = useState<Exercise['type']>('Styrka')
  const [instructions, setInstructions] = useState([''])

  const muscles = ['Bröst', 'Rygg', 'Ben', 'Axlar', 'Armar', 'Core', 'Helkropp', 'Kondition']
  const equips = ['Kroppsvikt', 'Hantel', 'Kettlebell', 'Skivstång', 'Kabelmaskin', 'Maskin', 'Motståndsband', 'Löpband']
  const fieldStyle = { backgroundColor: t.bg, borderRadius: 10, paddingHorizontal: 14, paddingVertical: 12, color: t.text, fontSize: 15 as const, marginBottom: 14, borderWidth: 1, borderColor: t.border }
  const labelStyle = { color: t.muted, fontSize: 11 as const, fontWeight: '700' as const, letterSpacing: 0.8, textTransform: 'uppercase' as const, marginBottom: 6 }

  const save = () => {
    if (!name.trim()) return
    onSave({ id: `custom_${Date.now()}`, name: name.trim(), icon: icon.trim() || '⚡', muscle, secondary, equip, diff, type, instructions: instructions.filter((s) => s.trim()), isCustom: true })
  }

  return (
    <Modal visible transparent animationType="slide" onRequestClose={onClose}>
      <TouchableOpacity style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'flex-end' }} activeOpacity={1} onPress={onClose}>
        <View>
          <ScrollView style={{ backgroundColor: t.card, borderTopLeftRadius: 24, borderTopRightRadius: 24, padding: 24, maxHeight: '95%' }} contentContainerStyle={{ paddingBottom: 40 }} keyboardShouldPersistTaps="handled">
            <View style={{ width: 40, height: 4, backgroundColor: t.border, borderRadius: 2, alignSelf: 'center', marginBottom: 16 }} />
            <Text style={{ color: t.text, fontSize: 20, fontWeight: '800', marginBottom: 16 }}>Skapa övning</Text>
            <Text style={labelStyle}>Namn</Text>
            <TextInput style={fieldStyle} value={name} onChangeText={setName} placeholder="Övningens namn" placeholderTextColor={t.muted} />
            <Text style={labelStyle}>Ikon (emoji)</Text>
            <TextInput style={[fieldStyle, { width: 80 }]} value={icon} onChangeText={setIcon} placeholder="💪" placeholderTextColor={t.muted} maxLength={2} />
            <Text style={labelStyle}>Muskelgrupp</Text>
            <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginBottom: 14 }}>
              {muscles.map((m) => <Chip key={m} label={m} active={muscle === m} onPress={() => setMuscle(m)} theme={t} />)}
            </View>
            <Text style={labelStyle}>Utrustning</Text>
            <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginBottom: 14 }}>
              {equips.map((e) => <Chip key={e} label={e} active={equip === e} onPress={() => setEquip(e)} theme={t} />)}
            </View>
            <Text style={labelStyle}>Svårighet</Text>
            <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginBottom: 14 }}>
              {(['Nybörjare', 'Medel', 'Avancerad'] as const).map((d) => <Chip key={d} label={d} active={diff === d} onPress={() => setDiff(d)} theme={t} />)}
            </View>
            <Text style={labelStyle}>Instruktioner</Text>
            {instructions.map((step, i) => (
              <View key={i} style={{ flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 8 }}>
                <TextInput style={[fieldStyle, { flex: 1, marginBottom: 0 }]} value={step} onChangeText={(v) => { const a = [...instructions]; a[i] = v; setInstructions(a) }} placeholder={`Steg ${i + 1}...`} placeholderTextColor={t.muted} />
                <TouchableOpacity style={{ width: 32, height: 32, borderRadius: 8, backgroundColor: '#FEE2E2', alignItems: 'center', justifyContent: 'center' }} onPress={() => { if (instructions.length > 1) setInstructions(instructions.filter((_, j) => j !== i)) }}>
                  <Text style={{ color: '#E53E3E', fontSize: 16 }}>✕</Text>
                </TouchableOpacity>
              </View>
            ))}
            <TouchableOpacity style={{ paddingVertical: 10, alignItems: 'center' }} onPress={() => setInstructions([...instructions, ''])}>
              <Text style={{ color: t.accent, fontSize: 14, fontWeight: '600' }}>+ Lägg till steg</Text>
            </TouchableOpacity>
            <View style={{ flexDirection: 'row', gap: 10, marginTop: 16 }}>
              <TouchableOpacity style={{ flex: 1, backgroundColor: t.bg, borderRadius: 12, paddingVertical: 14, alignItems: 'center' }} onPress={onClose}><Text style={{ color: t.text, fontWeight: '700' }}>Avbryt</Text></TouchableOpacity>
              <TouchableOpacity style={{ flex: 2, backgroundColor: t.accent, borderRadius: 12, paddingVertical: 14, alignItems: 'center' }} onPress={save}><Text style={{ color: '#FFF', fontWeight: '700', fontSize: 15 }}>Spara övning</Text></TouchableOpacity>
            </View>
          </ScrollView>
        </View>
      </TouchableOpacity>
    </Modal>
  )
}

export default function LibraryScreen() {
  const theme = useTheme()
  const customExercises = useWorkoutStore((s) => s.customExercises)
  const saveCustomExercises = useWorkoutStore((s) => s.saveCustomExercises)
  const [search, setSearch] = useState('')
  const [equipFilter, setEquipFilter] = useState<string[]>([])
  const [muscleFilter, setMuscleFilter] = useState('Alla')
  const [diffFilter, setDiffFilter] = useState('Alla')
  const [showFilter, setShowFilter] = useState(false)
  const [selectedEx, setSelectedEx] = useState<Exercise | null>(null)
  const [showCreate, setShowCreate] = useState(false)

  const allExercises = [...EXERCISES, ...customExercises]
  const filtered = allExercises.filter((e) => {
    if (search && !e.name.toLowerCase().includes(search.toLowerCase())) return false
    if (equipFilter.length && !equipFilter.includes(e.equip)) return false
    if (muscleFilter !== 'Alla' && e.muscle !== muscleFilter) return false
    if (diffFilter !== 'Alla' && e.diff !== diffFilter) return false
    return true
  })
  const filterBadge = (muscleFilter !== 'Alla' ? 1 : 0) + (diffFilter !== 'Alla' ? 1 : 0)

  const s = StyleSheet.create({
    safe: { flex: 1, backgroundColor: theme.outer },
    header: { backgroundColor: theme.header, paddingHorizontal: 22, paddingTop: 12, paddingBottom: 16 },
    title: { color: theme.headerText, fontSize: 24, fontWeight: '800', marginBottom: 12 },
    searchRow: { flexDirection: 'row', alignItems: 'center', gap: 8 },
    searchWrap: { flex: 1, flexDirection: 'row', alignItems: 'center', backgroundColor: 'rgba(255,255,255,0.12)', borderRadius: 12, paddingHorizontal: 12, height: 40 },
    searchInput: { flex: 1, color: theme.headerText, fontSize: 15, marginLeft: 6 },
    filterBtn: { flexDirection: 'row', alignItems: 'center', gap: 4, backgroundColor: 'rgba(255,255,255,0.12)', borderRadius: 12, paddingHorizontal: 12, height: 40 },
    filterBtnText: { color: theme.headerText, fontSize: 14, fontWeight: '600' },
    filterBadge: { backgroundColor: theme.accent, borderRadius: 8, paddingHorizontal: 5, paddingVertical: 1 },
    filterBadgeText: { color: '#FFF', fontSize: 10, fontWeight: '800' },
    list: { flex: 1 },
    fab: { position: 'absolute', bottom: 24, right: 24, backgroundColor: theme.accent, borderRadius: 28, paddingHorizontal: 20, paddingVertical: 14, shadowColor: '#000', shadowOpacity: 0.2, shadowRadius: 8, shadowOffset: { width: 0, height: 4 }, elevation: 6 },
    fabText: { color: '#FFF', fontSize: 14, fontWeight: '700' },
    empty: { textAlign: 'center', color: theme.muted, marginTop: 40, fontSize: 14 },
    filterOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'flex-end' },
    filterSheet: { backgroundColor: theme.card, borderTopLeftRadius: 24, borderTopRightRadius: 24, padding: 24, paddingBottom: 40 },
    filterHandle: { width: 40, height: 4, backgroundColor: theme.border, borderRadius: 2, alignSelf: 'center', marginBottom: 16 },
    filterTitle: { color: theme.text, fontSize: 18, fontWeight: '800', marginBottom: 16 },
    filterSectionTitle: { color: theme.muted, fontSize: 11, fontWeight: '700', letterSpacing: 0.8, textTransform: 'uppercase', marginBottom: 10, marginTop: 12 },
    filterChipRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 },
    filterActions: { flexDirection: 'row', gap: 10, marginTop: 20 },
    resetBtn: { flex: 1, backgroundColor: theme.bg, borderRadius: 12, paddingVertical: 14, alignItems: 'center' },
    applyBtn: { flex: 2, backgroundColor: theme.accent, borderRadius: 12, paddingVertical: 14, alignItems: 'center' },
  })

  return (
    <SafeAreaView style={s.safe} edges={['top']}>
      <View style={s.header}>
        <Text style={s.title}>Övningsbibliotek</Text>
        <View style={s.searchRow}>
          <View style={s.searchWrap}>
            <Search size={16} color={theme.muted} />
            <TextInput style={s.searchInput} value={search} onChangeText={setSearch} placeholder="Sök övning..." placeholderTextColor={theme.muted} />
          </View>
          <TouchableOpacity style={s.filterBtn} onPress={() => setShowFilter(true)}>
            <SlidersHorizontal size={16} color={theme.headerText} />
            <Text style={s.filterBtnText}>Filter</Text>
            {filterBadge > 0 && <View style={s.filterBadge}><Text style={s.filterBadgeText}>{filterBadge}</Text></View>}
          </TouchableOpacity>
        </View>
      </View>

      <ScrollView horizontal showsHorizontalScrollIndicator={false} style={{ backgroundColor: theme.card, maxHeight: 52, borderBottomWidth: 1, borderBottomColor: theme.border }} contentContainerStyle={{ paddingHorizontal: 12, paddingVertical: 10, gap: 8, flexDirection: 'row' }}>
        {EQUIP_CHIPS.map((c) => (
          <Chip key={c} label={c} active={equipFilter.includes(c)} onPress={() => setEquipFilter(equipFilter.includes(c) ? equipFilter.filter((x) => x !== c) : [...equipFilter, c])} theme={theme} />
        ))}
      </ScrollView>

      <FlatList
        data={filtered}
        keyExtractor={(e) => e.id}
        style={s.list}
        contentContainerStyle={{ paddingBottom: 100 }}
        renderItem={({ item: e }) => (
          <TouchableOpacity
            onPress={() => setSelectedEx(e)}
            style={[
              { flexDirection: 'row', alignItems: 'center', backgroundColor: theme.card, marginHorizontal: 16, marginTop: 10, borderRadius: 14, padding: 14, gap: 12 },
              e.isCustom && { borderWidth: 1.5, borderColor: theme.accent },
            ]}
          >
            <View style={{ width: 44, height: 44, borderRadius: 12, backgroundColor: EQUIP_BG[e.equip] || '#F5F5F3', alignItems: 'center', justifyContent: 'center' }}>
              <Text style={{ fontSize: 22 }}>{e.icon || '⚡'}</Text>
            </View>
            <View style={{ flex: 1 }}>
              <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
                <Text style={{ color: theme.text, fontSize: 15, fontWeight: '700' }}>{e.name}</Text>
                {e.isCustom && <Text style={{ color: theme.accent, fontSize: 10, fontWeight: '800' }}>EIGEN</Text>}
              </View>
              <Text style={{ color: theme.muted, fontSize: 13, marginTop: 2 }}>{e.muscle} · {e.diff}</Text>
            </View>
            <Text style={{ color: theme.muted, fontSize: 12 }}>{e.equip}</Text>
          </TouchableOpacity>
        )}
        ListEmptyComponent={<Text style={s.empty}>Inga övningar hittades</Text>}
      />

      <TouchableOpacity style={s.fab} onPress={() => setShowCreate(true)}>
        <Text style={s.fabText}>+ Ny övning</Text>
      </TouchableOpacity>

      <Modal visible={showFilter} transparent animationType="slide" onRequestClose={() => setShowFilter(false)}>
        <TouchableOpacity style={s.filterOverlay} activeOpacity={1} onPress={() => setShowFilter(false)}>
          <TouchableOpacity activeOpacity={1}>
            <View style={s.filterSheet}>
              <View style={s.filterHandle} />
              <Text style={s.filterTitle}>Filter</Text>
              <Text style={s.filterSectionTitle}>Muskelgrupp</Text>
              <View style={s.filterChipRow}>
                {MUSCLE_GROUPS.map((m) => <Chip key={m} label={m} active={muscleFilter === m} onPress={() => setMuscleFilter(m)} theme={theme} />)}
              </View>
              <Text style={s.filterSectionTitle}>Svårighet</Text>
              <View style={s.filterChipRow}>
                {DIFFICULTIES.map((d) => <Chip key={d} label={d} active={diffFilter === d} onPress={() => setDiffFilter(d)} theme={theme} />)}
              </View>
              <View style={s.filterActions}>
                <TouchableOpacity style={s.resetBtn} onPress={() => { setMuscleFilter('Alla'); setDiffFilter('Alla'); setShowFilter(false) }}><Text style={{ color: theme.text, fontWeight: '700' }}>Återställ</Text></TouchableOpacity>
                <TouchableOpacity style={s.applyBtn} onPress={() => setShowFilter(false)}><Text style={{ color: '#FFF', fontWeight: '700' }}>Visa resultat</Text></TouchableOpacity>
              </View>
            </View>
          </TouchableOpacity>
        </TouchableOpacity>
      </Modal>

      {selectedEx && (
        <ExerciseDetailModal
          ex={selectedEx}
          onClose={() => setSelectedEx(null)}
          onDelete={selectedEx.isCustom ? () => { saveCustomExercises(customExercises.filter((e) => e.id !== selectedEx.id)); setSelectedEx(null) } : undefined}
        />
      )}
      {showCreate && <CreateExerciseModal onClose={() => setShowCreate(false)} onSave={(ex) => { saveCustomExercises([...customExercises, ex]); setShowCreate(false) }} />}
    </SafeAreaView>
  )
}