Grapevine_Disease_Detection/VinEye/src/components/home/RecentScans.tsx
Yanis 07c42ed40e feat(home): skeleton loading states + cascade entrance animations
Animations FadeInDown.springify().damping(16) avec stagger 60ms entre
sections du HomeScreen :
- SearchHeader (delay 0)
- SearchSection (delay 60)
- RecentScans (delay 120)
- FrequentDiseasesHorizontal (delay 200)
- PracticalGuides (delay 280)

Skeleton loading states :
- LargeDiseaseCardCompactSkeleton : nouveau, simule la structure compact
  (badge + icon + title + desc + footer) — utilisé dans
  FrequentDiseasesHorizontal en remplacement de CarouselCardSkeleton
- ScanListItemSkeleton : nouveau, simule image 64x64 + name + status pill
  + time + confidence tile — utilisé dans RecentScans
- RecentScans / PracticalGuides : nouveau style cardLoading sans
  shadow/elevation Android (qui ne respecte pas l'opacité de FadeInDown
  → flash "rectangle blanc + ombre" pendant l'anim). iOS shadow conservé.

isLoading propagé depuis useDiseases / useGuides / useHistory vers les
sections concernées.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 01:05:37 +02:00

95 lines
2.8 KiB
TypeScript

import { View, StyleSheet, Platform } from "react-native";
import { useNavigation } from "@react-navigation/native";
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { useTranslation } from "react-i18next";
import { ScanListItem } from "@/components/my-plants/ScanListItem";
import SectionHeader from "@/components/home/components/homeheader";
import HomeCta from "@/components/home/HomeCta";
import { ScanListItemSkeleton } from "@/components/ui/Skeleton";
import { useHistory } from "@/hooks/useHistory";
import type { RootStackParamList } from "@/types/navigation";
type Nav = NativeStackNavigationProp<RootStackParamList>;
const MAX_RECENT = 3;
export default function RecentScans() {
const { t } = useTranslation();
const navigation = useNavigation<Nav>();
const { history, isLoading, toggleFavorite, deleteScan } = useHistory();
// Loading + pas encore de cache → skeleton
if (isLoading && history.length === 0) {
return (
<View className="mb-6 mx-5 gap-3">
<SectionHeader
title={t("home.recentScans")}
onViewAll={() => navigation.navigate("Main", { screen: "MyPlants" })}
/>
<View style={styles.cardLoading}>
<ScanListItemSkeleton showSeparator />
<ScanListItemSkeleton showSeparator />
<ScanListItemSkeleton />
</View>
</View>
);
}
if (history.length === 0) {
return <HomeCta />;
}
const recent = history.slice(0, MAX_RECENT);
return (
<View className="mb-6 mx-5 gap-3">
<SectionHeader
title={t("home.recentScans")}
onViewAll={() => navigation.navigate("Main", { screen: "MyPlants" })}
/>
<View style={styles.card}>
{recent.map((scan, index) => (
<ScanListItem
key={scan.id}
scan={scan}
onPress={() => navigation.navigate("ScanDetail", { scanId: scan.id })}
onToggleFavorite={() => toggleFavorite(scan.id)}
onDelete={() => deleteScan(scan.id)}
grouped
showSeparator={index < recent.length - 1}
/>
))}
</View>
</View>
);
}
const styles = StyleSheet.create({
card: {
backgroundColor: "#FFFFFF",
borderRadius: 16,
overflow: "hidden",
borderWidth: 1,
borderColor: "#F0F0F0",
...Platform.select({
ios: {
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.04,
shadowRadius: 8,
},
android: { elevation: 2 },
}),
},
// Loading: pas de shadow / elevation → évite le flash "rectangle blanc + ombre"
// sur Android avant que les skeletons ne fadent in via FadeInDown du parent.
cardLoading: {
backgroundColor: "#FFFFFF",
borderRadius: 16,
overflow: "hidden",
borderWidth: 1,
borderColor: "#F0F0F0",
},
});