anisav-mobile/app/account/lists.tsx
2026-07-19 06:05:12 +05:00

209 lines
6 KiB
TypeScript
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 { useCallback, useEffect, useMemo, useState } from 'react'
import {
FlatList,
Pressable,
RefreshControl,
ScrollView,
StyleSheet,
Text,
View,
useWindowDimensions,
} from 'react-native'
import { useRouter } from 'expo-router'
import Ionicons from '@expo/vector-icons/Ionicons'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import type { CollectionType, Release } from '../../src/lib/types'
import { api } from '../../src/lib/api'
import { useAuth } from '../../src/lib/AuthContext'
import { colors, spacing } from '../../src/lib/theme'
import AnimeCard from '../../src/components/AnimeCard'
import Loader from '../../src/components/Loader'
import Chip from '../../src/components/Chip'
type TabKey = CollectionType | 'FAVORITES'
const TABS: Array<{ key: TabKey; label: string }> = [
{ key: 'WATCHING', label: 'Смотрю' },
{ key: 'PLANNED', label: 'Запланировано' },
{ key: 'WATCHED', label: 'Просмотрено' },
{ key: 'POSTPONED', label: 'Отложено' },
{ key: 'ABANDONED', label: 'Брошено' },
{ key: 'FAVORITES', label: 'Избранное' },
]
export default function MyListsScreen() {
const router = useRouter()
const insets = useSafeAreaInsets()
const { width } = useWindowDimensions()
const { user, loading: authLoading } = useAuth()
const [tab, setTab] = useState<TabKey>('WATCHING')
const [releases, setReleases] = useState<Release[] | null>(null)
const [refreshing, setRefreshing] = useState(false)
const load = useCallback((currentTab: TabKey) => {
const req =
currentTab === 'FAVORITES' ? api.favoritesList(1, 48) : api.collectionList(currentTab, 1, 48)
return req.then((res) => setReleases(res.data)).catch(() => setReleases([]))
}, [])
useEffect(() => {
if (!user) return
setReleases(null)
load(tab)
}, [tab, user, load])
async function onRefresh() {
setRefreshing(true)
await load(tab)
setRefreshing(false)
}
const numColumns = Math.max(3, Math.floor((width - spacing(4) * 2) / 120))
const paddedReleases = useMemo(() => {
if (!releases) return []
const remainder = releases.length % numColumns
if (remainder === 0) return releases
return [...releases, ...Array(numColumns - remainder).fill(null)]
}, [releases, numColumns])
return (
<View style={styles.container}>
<View style={[styles.header, { paddingTop: insets.top + spacing(2) }]}>
<Pressable onPress={() => router.back()} hitSlop={10} style={styles.backButton}>
<Ionicons name="chevron-back" size={24} color={colors.text} />
</Pressable>
<Text style={styles.headerTitle}>Мои списки</Text>
</View>
{authLoading && <Loader />}
{!authLoading && !user && (
<View style={styles.empty}>
<Text style={styles.emptyText}>Войдите, чтобы видеть свои списки</Text>
<Pressable style={styles.loginButton} onPress={() => router.push('/login')}>
<Text style={styles.loginButtonText}>Войти</Text>
</Pressable>
</View>
)}
{!authLoading && user && (
<>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.tabs}
>
{TABS.map((t) => (
<Pressable key={t.key} onPress={() => setTab(t.key)}>
<Chip accent={tab === t.key}>{t.label}</Chip>
</Pressable>
))}
</ScrollView>
{!releases && <Loader />}
{releases && releases.length === 0 && (
<View style={styles.empty}>
<Text style={styles.emptyText}>Список пуст</Text>
</View>
)}
{releases && releases.length > 0 && (
<FlatList
key={`grid-${numColumns}`}
data={paddedReleases}
numColumns={numColumns}
keyExtractor={(item, index) => (item ? String(item.id) : `empty-${index}`)}
columnWrapperStyle={styles.row}
contentContainerStyle={[styles.grid, { paddingBottom: insets.bottom + spacing(6) }]}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor={colors.accent}
colors={[colors.accent]}
/>
}
renderItem={({ item }) =>
item ? (
<View style={styles.gridItem}>
<AnimeCard release={item} onPress={() => router.push(`/title/${item.alias || item.id}`)} />
</View>
) : (
<View style={styles.gridItem} />
)
}
/>
)}
</>
)}
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
header: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing(3),
paddingHorizontal: spacing(4),
paddingBottom: spacing(3),
},
backButton: {
width: 36,
height: 36,
borderRadius: 18,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: colors.surfaceRaised,
},
headerTitle: {
color: colors.text,
fontSize: 19,
fontWeight: '700',
},
tabs: {
gap: spacing(2),
paddingHorizontal: spacing(4),
paddingBottom: spacing(3),
},
empty: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: spacing(8),
gap: spacing(4),
},
emptyText: {
color: colors.textFaint,
fontSize: 14,
textAlign: 'center',
lineHeight: 20,
},
loginButton: {
backgroundColor: colors.accent,
paddingHorizontal: spacing(6),
paddingVertical: spacing(3),
borderRadius: 999,
},
loginButtonText: {
color: colors.text,
fontSize: 14,
fontWeight: '700',
},
grid: {
padding: spacing(4),
gap: spacing(3),
},
row: {
gap: spacing(3),
},
gridItem: {
flex: 1,
},
})