/** * Collection screen - displays user's scanned cards. */ import React, { useState, useCallback, useMemo } from "react"; import { StyleSheet, View, Text, FlatList, Image, Pressable, TextInput, RefreshControl, Dimensions, ActivityIndicator, } from "react-native"; import { FontAwesome } from "@expo/vector-icons"; import { useQuery } from "convex/react"; import { useRouter } from "expo-router"; import { api } from "../../convex/_generated/api"; import { useCurrentUser } from "@/lib/hooks"; const { width: SCREEN_WIDTH } = Dimensions.get("window"); const CARD_WIDTH = (SCREEN_WIDTH - 48) / 3; const CARD_HEIGHT = CARD_WIDTH * (88 / 63); // MTG aspect ratio interface CollectionItem { id: string; cardId: string; name: string; setCode: string; imageUri?: string; quantity: number; isFoil: boolean; addedAt: number; } export default function CollectionScreen() { const router = useRouter(); const [searchQuery, setSearchQuery] = useState(""); const [refreshing, setRefreshing] = useState(false); const [sortBy, setSortBy] = useState<"name" | "set" | "recent">("recent"); // Get authenticated user const { user, isAuthenticated } = useCurrentUser(); const userId = user?._id ?? null; // Fetch collection from Convex const rawCollection = useQuery( api.collections.getByUser, userId ? { userId: userId as any } : "skip" ); // Transform Convex data to UI format const collection = useMemo(() => { if (!rawCollection) return []; return rawCollection.map((entry) => ({ id: entry._id, cardId: entry.cardId, name: entry.card?.name || "Unknown", setCode: entry.card?.setCode || "???", imageUri: entry.card?.imageUri, quantity: entry.quantity, isFoil: entry.isFoil, addedAt: entry.addedAt, })); }, [rawCollection]); // Filter collection by search query const filteredCollection = useMemo( () => collection.filter((item) => item.name.toLowerCase().includes(searchQuery.toLowerCase()) ), [collection, searchQuery] ); // Sort collection const sortedCollection = useMemo( () => [...filteredCollection].sort((a, b) => { switch (sortBy) { case "name": return a.name.localeCompare(b.name); case "set": return a.setCode.localeCompare(b.setCode); case "recent": default: return b.addedAt - a.addedAt; } }), [filteredCollection, sortBy] ); const onRefresh = useCallback(async () => { setRefreshing(true); // Convex automatically syncs - just show loading briefly await new Promise((resolve) => setTimeout(resolve, 500)); setRefreshing(false); }, []); const renderCard = useCallback( ({ item }: { item: CollectionItem }) => ( router.push({ pathname: "/modal", params: { collectionEntryId: item.id, cardId: item.cardId, }, }) } > {item.imageUri ? ( ) : ( )} {item.quantity > 1 && ( ×{item.quantity} )} {item.isFoil && ( )} ), [router] ); const renderEmpty = useCallback( () => ( No Cards Yet {userId ? "Start scanning cards to build your collection!" : "Sign in to start building your collection!"} ), [userId] ); // Loading state if (rawCollection === undefined && userId) { return ( Loading collection... ); } // Total cards count const totalCount = collection.reduce((sum, item) => sum + item.quantity, 0); return ( {/* Search bar */} {searchQuery.length > 0 && ( setSearchQuery("")}> )} {/* Stats and sort bar */} {sortedCollection.length} unique {collection.length > 0 && ` (${totalCount} total)`} setSortBy("recent")} > Recent setSortBy("name")} > Name setSortBy("set")} > Set {/* Card grid */} item.id} numColumns={3} contentContainerStyle={styles.gridContent} ListEmptyComponent={renderEmpty} refreshControl={ } /> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: "#1a1a1a", }, searchContainer: { flexDirection: "row", alignItems: "center", backgroundColor: "#2a2a2a", marginHorizontal: 16, marginTop: 16, marginBottom: 8, paddingHorizontal: 12, borderRadius: 10, }, searchIcon: { marginRight: 8, }, searchInput: { flex: 1, height: 44, color: "#fff", fontSize: 16, }, statsBar: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", paddingHorizontal: 16, paddingVertical: 8, }, statsText: { color: "#888", fontSize: 14, }, sortButtons: { flexDirection: "row", gap: 4, }, sortButton: { paddingHorizontal: 10, paddingVertical: 6, borderRadius: 6, }, sortButtonActive: { backgroundColor: "#333", }, sortButtonText: { color: "#666", fontSize: 12, fontWeight: "500", }, sortButtonTextActive: { color: "#fff", }, gridContent: { padding: 16, paddingTop: 8, }, cardContainer: { width: CARD_WIDTH, height: CARD_HEIGHT, marginRight: 8, marginBottom: 8, borderRadius: 8, overflow: "hidden", }, cardImage: { width: "100%", height: "100%", backgroundColor: "#2a2a2a", borderRadius: 8, }, cardPlaceholder: { justifyContent: "center", alignItems: "center", }, quantityBadge: { position: "absolute", top: 4, right: 4, backgroundColor: "rgba(0,0,0,0.8)", paddingHorizontal: 6, paddingVertical: 2, borderRadius: 4, }, quantityText: { color: "#fff", fontSize: 11, fontWeight: "600", }, foilBadge: { position: "absolute", top: 4, left: 4, backgroundColor: "rgba(0,0,0,0.8)", padding: 4, borderRadius: 4, }, emptyContainer: { flex: 1, justifyContent: "center", alignItems: "center", paddingTop: 100, paddingHorizontal: 40, }, emptyTitle: { marginTop: 20, fontSize: 22, fontWeight: "bold", color: "#fff", }, emptyText: { marginTop: 8, fontSize: 16, color: "#888", textAlign: "center", }, loadingContainer: { flex: 1, justifyContent: "center", alignItems: "center", backgroundColor: "#1a1a1a", }, loadingText: { marginTop: 16, color: "#888", fontSize: 16, }, });