etfoglasi-frontend/components/CollapsibleCategory.tsx

193 lines
7.7 KiB
TypeScript

import React, { useCallback, useEffect, useRef, useState } from "react";
import {
View, Text, TouchableOpacity,
ActivityIndicator, LayoutAnimation, useColorScheme,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { Entry, entriesApi } from "../services/api";
import ExpandableItem from "./ExpandableItem";
// ─── Props ─────────────────────────────────────────────────────────────────
// items removed — now fetched from API using groupName
export interface CollapsibleCategoryProps {
id: string;
label: string;
icon: keyof typeof Ionicons.glyphMap;
color: string;
darkColor: string;
groupName: string; // passed to /entries?groupName=
defaultOpen?: boolean;
}
// ─── Component ─────────────────────────────────────────────────────────────
export default function CollapsibleCategory({
label,
icon,
color,
darkColor,
groupName,
defaultOpen = false,
}: CollapsibleCategoryProps) {
const dark = useColorScheme() === "dark";
const accentColor = dark ? darkColor : color;
const [open, setOpen] = useState(defaultOpen);
const [items, setItems] = useState<Entry[]>([]);
const [page, setPage] = useState(0);
const [hasMore, setHasMore] = useState(true);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const initialised = useRef(false);
const fetchPage = useCallback(async (pageNum: number) => {
if (loading) return;
setLoading(true);
setError("");
try {
const data = await entriesApi.getEntries({ groupName, page: pageNum });
setItems((prev) =>
pageNum === 0 ? data.content : [...prev, ...data.content]
);
setHasMore(!data.last);
setPage(pageNum);
} catch {
setError("Couldn't load entries. Tap to retry.");
} finally {
setLoading(false);
}
}, [groupName, loading]);
// Fetch first page the first time the accordion opens
useEffect(() => {
if (open && !initialised.current) {
initialised.current = true;
fetchPage(0);
}
}, [open]);
const toggle = () => {
LayoutAnimation.configureNext({
duration: 260,
create: { type: "easeInEaseOut", property: "opacity" },
update: { type: "spring", springDamping: 0.85 },
});
setOpen((v) => !v);
};
return (
<View className="mb-4">
{/* ── Header — untouched from your original ── */}
<TouchableOpacity
onPress={toggle}
activeOpacity={0.8}
className={`
mx-4 flex-row items-center px-4 py-3.5 rounded-2xl
${dark
? "bg-obsidian-50 border border-border-dark"
: "bg-parchment-100 border border-border-light"
}
`}
>
<View
className="w-8 h-8 rounded-xl items-center justify-center mr-3"
style={{ backgroundColor: accentColor + "25" }}
>
<Ionicons name={icon} size={16} color={accentColor} />
</View>
<Text
className={`flex-1 text-sm font-sans font-bold tracking-wide ${
dark ? "text-ink-dark" : "text-ink-light"
}`}
>
{label}
</Text>
{/* Count badge — shows loaded count, spins while first load */}
<View
className="px-2 py-0.5 rounded-full mr-3"
style={{ backgroundColor: accentColor + "20" }}
>
{loading && items.length === 0 ? (
<ActivityIndicator size="small" color={accentColor} style={{ width: 24 }} />
) : (
<Text
className="text-xs font-sans font-semibold"
style={{ color: accentColor }}
>
{items.length}
</Text>
)}
</View>
<Ionicons
name={open ? "chevron-up" : "chevron-down"}
size={16}
color={dark ? "#706D67" : "#8A8278"}
/>
</TouchableOpacity>
{/* ── Body ── */}
{open && (
<View className="mt-2">
{/* Error */}
{error !== "" && (
<TouchableOpacity
onPress={() => fetchPage(page)}
className="mx-4 py-4 items-center"
>
<Text className={`text-sm font-sans ${dark ? "text-muted-dark" : "text-muted-light"}`}>
{error}
</Text>
</TouchableOpacity>
)}
{/* Items — same ExpandableItem you already have */}
{items.map((item) => (
<ExpandableItem key={item.id} item={item} />
))}
{/* Load more */}
{hasMore && items.length > 0 && (
<TouchableOpacity
onPress={() => fetchPage(page + 1)}
disabled={loading}
className={`mx-4 mt-1 mb-1 py-3 rounded-2xl border items-center ${
dark
? "bg-obsidian-50 border-border-dark"
: "bg-parchment-100 border-border-light"
}`}
activeOpacity={0.7}
>
{loading ? (
<ActivityIndicator size="small" color={accentColor} />
) : (
<View className="flex-row items-center gap-1.5">
<Ionicons name="chevron-down" size={13} color={accentColor} />
<Text
className="text-xs font-sans font-semibold"
style={{ color: accentColor }}
>
Load more
</Text>
</View>
)}
</TouchableOpacity>
)}
{/* End of list */}
{!hasMore && items.length > 0 && (
<Text className={`text-center text-xs font-sans py-3 ${
dark ? "text-muted-dark" : "text-muted-light"
}`}>
All {items.length} entries loaded
</Text>
)}
</View>
)}
</View>
);
}