diff --git a/VinEye/src/components/map/FloatingActions.tsx b/VinEye/src/components/map/FloatingActions.tsx index aa77c4f..d4406af 100644 --- a/VinEye/src/components/map/FloatingActions.tsx +++ b/VinEye/src/components/map/FloatingActions.tsx @@ -1,4 +1,5 @@ -import { View, Pressable, StyleSheet } from "react-native"; +import React from "react"; +import { View, Pressable } from "react-native"; import { Layers, LocateFixed, Satellite } from "lucide-react-native"; import { colors } from "@/theme/colors"; @@ -12,6 +13,10 @@ interface FloatingActionsProps { activeAction?: FloatingActionId; } +function getIconColor(isActive: boolean) { + return isActive ? "#FFFFFF" : colors.primary[800]; +} + export function FloatingActions({ onLayers, onLocate, @@ -19,25 +24,27 @@ export function FloatingActions({ activeAction, }: FloatingActionsProps) { return ( - + + + @@ -55,45 +62,14 @@ function ActionButton({ children, onPress, active }: ActionButtonProps) { return ( [ - styles.button, - active ? styles.buttonActive : styles.buttonInactive, - pressed && { transform: [{ scale: 0.95 }] }, + { transform: [{ scale: pressed ? 0.95 : 1 }] }, ]} > {children} ); } - -const styles = StyleSheet.create({ - column: { - gap: 12, - elevation: 24, - }, - button: { - width: 56, - height: 56, - borderRadius: 999, - alignItems: "center", - justifyContent: "center", - }, - buttonInactive: { - backgroundColor: "#FFFFFF", - borderWidth: 1, - borderColor: "#E5E7EB", - shadowColor: "#000", - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.18, - shadowRadius: 12, - elevation: 24, - }, - buttonActive: { - backgroundColor: colors.primary[900], - shadowColor: colors.primary[900], - shadowOffset: { width: 0, height: 6 }, - shadowOpacity: 0.3, - shadowRadius: 12, - elevation: 24, - }, -}); diff --git a/VinEye/src/components/map/MapBottomSheet.tsx b/VinEye/src/components/map/MapBottomSheet.tsx index f87518a..c374113 100644 --- a/VinEye/src/components/map/MapBottomSheet.tsx +++ b/VinEye/src/components/map/MapBottomSheet.tsx @@ -1,17 +1,25 @@ -import { forwardRef, useMemo, useState } from "react"; +import { + forwardRef, + useEffect, + useImperativeHandle, + useMemo, + useRef, + useState, +} from "react"; import { View, Pressable, StyleSheet, - Modal, - TextInput, - KeyboardAvoidingView, - Platform, + Text as RNText, } from "react-native"; -import BottomSheet, { BottomSheetScrollView } from "@gorhom/bottom-sheet"; +import BottomSheet, { + BottomSheetScrollView, + BottomSheetTextInput, +} from "@gorhom/bottom-sheet"; import { useTranslation } from "react-i18next"; import { ChevronRight, + ChevronLeft, AlertTriangle, Leaf, Clock, @@ -19,6 +27,7 @@ import { X, ScanLine, MapPin, + Check, } from "lucide-react-native"; import { Text } from "@/components/ui/text"; @@ -29,6 +38,8 @@ import type { ScanRecord, ScanStatus } from "@/types/detection"; interface MapBottomSheetProps { scans: ScanRecord[]; + previewScan?: ScanRecord | null; + onPreviewClose?: () => void; onScanPress?: (scan: ScanRecord) => void; onRename?: (scanId: string, newName: string) => void; onScanCta?: () => void; @@ -36,15 +47,49 @@ interface MapBottomSheetProps { } export const MapBottomSheet = forwardRef( - function MapBottomSheet({ scans, onScanPress, onRename, onScanCta, defaultIndex = 0 }, ref) { + function MapBottomSheet( + { + scans, + previewScan, + onPreviewClose, + onScanPress, + onRename, + onScanCta, + defaultIndex = 0, + }, + ref, + ) { const { t } = useTranslation(); const snapPoints = useMemo(() => ["20%", "55%", "85%"], []); + const internalRef = useRef(null); const [renamingScan, setRenamingScan] = useState(null); const [draftName, setDraftName] = useState(""); + useImperativeHandle( + ref, + () => + ({ + snapToIndex: (i: number) => internalRef.current?.snapToIndex(i), + snapToPosition: (p: number | string) => + internalRef.current?.snapToPosition(p), + expand: () => internalRef.current?.expand(), + collapse: () => internalRef.current?.collapse(), + close: () => internalRef.current?.close(), + forceClose: () => internalRef.current?.forceClose(), + }) as BottomSheet, + [], + ); + + useEffect(() => { + if (renamingScan) { + setDraftName(getScanDisplayName(renamingScan, t)); + } + }, [renamingScan, t]); + function handleStartRename(scan: ScanRecord) { setRenamingScan(scan); - setDraftName(getScanDisplayName(scan, t)); + // remonte à 85% pour bien voir l'input + boutons au-dessus du clavier + internalRef.current?.snapToIndex(2); } function handleConfirmRename() { @@ -53,31 +98,137 @@ export const MapBottomSheet = forwardRef( } setRenamingScan(null); setDraftName(""); + // redescend pour voir la map + internalRef.current?.snapToIndex(0); } function handleCancelRename() { setRenamingScan(null); setDraftName(""); + // redescend pour voir la map + internalRef.current?.snapToIndex(0); } return ( <> - - {t("map.scannedPlants")} - - {t("map.plantCount", { count: scans.length })} - - + {renamingScan ? ( + + + + + + {t("map.rename.title")} + + + + {t("map.rename.subtitle")} + + + + [ + styles.renameBtn, + styles.renameBtnGhost, + pressed && { opacity: 0.7 }, + ]} + > + + + + {t("common.cancel")} + + + + [ + styles.renameBtn, + styles.renameBtnPrimary, + pressed && { opacity: 0.85 }, + ]} + > + + + + {t("map.rename.save")} + + + + + + ) : previewScan ? ( + + + {t("map.preview.title")} + + + + + + onScanPress?.(previewScan)} + onEdit={() => handleStartRename(previewScan)} + /> + + {t("map.preview.tapHint")} + + + + ) : ( + <> + + {t("map.scannedPlants")} + + {t("map.plantCount", { count: scans.length })} + + - {scans.length === 0 ? ( + {scans.length === 0 ? ( @@ -112,68 +263,10 @@ export const MapBottomSheet = forwardRef( /> ))} + )} + )} - - - - - - - {t("map.rename.title")} - - - - - {t("map.rename.subtitle")} - - - [ - styles.modalButton, - styles.modalButtonGhost, - pressed && { opacity: 0.7 }, - ]} - > - - {t("common.cancel")} - - - [ - styles.modalButton, - styles.modalButtonPrimary, - pressed && { opacity: 0.85 }, - ]} - > - - {t("map.rename.save")} - - - - - - ); } @@ -235,6 +328,10 @@ function ScanRow({ scan, isLast, onPress, onEdit }: ScanRowProps) { } const styles = StyleSheet.create({ + sheetContainer: { + zIndex: 100, + elevation: 100, + }, background: { backgroundColor: "#FFFFFF", borderTopLeftRadius: 28, @@ -258,6 +355,34 @@ const styles = StyleSheet.create({ paddingTop: 8, paddingBottom: 12, }, + previewHeader: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + paddingHorizontal: 20, + paddingTop: 8, + paddingBottom: 4, + }, + previewCloseBtn: { + width: 32, + height: 32, + borderRadius: 999, + alignItems: "center", + justifyContent: "center", + backgroundColor: colors.neutral[100], + }, + previewBody: { + paddingHorizontal: 20, + paddingBottom: 20, + gap: 8, + }, + previewHint: { + fontSize: 12, + fontWeight: "500", + color: colors.neutral[500], + textAlign: "center", + paddingTop: 4, + }, title: { fontSize: 18, fontWeight: "700", @@ -359,75 +484,91 @@ const styles = StyleSheet.create({ fontSize: 14, fontWeight: "600", }, - modalOverlay: { - flex: 1, - justifyContent: "center", - alignItems: "center", - paddingHorizontal: 24, - }, - modalBackdrop: { - ...StyleSheet.absoluteFillObject, - backgroundColor: "rgba(0,0,0,0.45)", - }, - modalCard: { - width: "100%", - maxWidth: 400, - backgroundColor: "#FFFFFF", - borderRadius: 24, - padding: 20, + // Rename inline form + renameWrap: { + paddingHorizontal: 20, + paddingTop: 4, + paddingBottom: 24, gap: 12, }, - modalHeader: { + renameHeader: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", }, - modalTitle: { - fontSize: 18, - fontWeight: "700", - color: colors.neutral[900], + backBtn: { + width: 36, + height: 36, + borderRadius: 999, + alignItems: "center", + justifyContent: "center", + backgroundColor: colors.neutral[100], }, - modalSubtitle: { + backBtnPlaceholder: { + width: 36, + height: 36, + }, + renameSubtitle: { fontSize: 13, color: colors.neutral[600], lineHeight: 18, + paddingHorizontal: 4, }, - modalInput: { + renameInput: { borderWidth: 1, borderColor: colors.neutral[300], - borderRadius: 12, + borderRadius: 14, paddingHorizontal: 14, - paddingVertical: 12, - fontSize: 15, + paddingVertical: 14, + fontSize: 16, color: colors.neutral[900], backgroundColor: "#FAFAFA", }, - modalActions: { + renameActions: { flexDirection: "row", - gap: 10, - marginTop: 4, + paddingTop: 16, + gap: 12, }, - modalButton: { - flex: 1, - paddingVertical: 12, - borderRadius: 12, + renameBtn: { + flexGrow: 1, + flexShrink: 1, + flexBasis: 0, + paddingVertical: 16, + paddingHorizontal: 12, + borderRadius: 14, + minHeight: 56, alignItems: "center", justifyContent: "center", }, - modalButtonGhost: { - backgroundColor: colors.neutral[100], + renameBtnInner: { + flexDirection: "row", + alignItems: "center", }, - modalButtonGhostLabel: { - fontSize: 15, - fontWeight: "600", + renameBtnGhost: { + backgroundColor: colors.neutral[200], + borderWidth: 1.5, + borderColor: colors.neutral[400], + }, + renameBtnGhostLabel: { + fontSize: 16, + fontWeight: "700", color: colors.neutral[800], + letterSpacing: 0.2, + marginLeft: 8, }, - modalButtonPrimary: { + renameBtnPrimary: { backgroundColor: colors.primary[800], + shadowColor: colors.primary[900], + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.25, + shadowRadius: 8, + elevation: 4, }, - modalButtonPrimaryLabel: { - fontSize: 15, - fontWeight: "600", + renameBtnPrimaryLabel: { + fontSize: 16, + fontWeight: "700", color: "#FFFFFF", + letterSpacing: 0.2, + marginLeft: 8, }, }); diff --git a/VinEye/src/screens/MapScreen.tsx b/VinEye/src/screens/MapScreen.tsx index 89a58ba..db13263 100644 --- a/VinEye/src/screens/MapScreen.tsx +++ b/VinEye/src/screens/MapScreen.tsx @@ -8,7 +8,11 @@ import { LinearGradient } from "expo-linear-gradient"; import { toast } from "sonner-native"; import { useTranslation } from "react-i18next"; -import { VineyardMapView, type VineyardMapHandle, type MapRegion } from "@/components/map/MapView"; +import { + VineyardMapView, + type VineyardMapHandle, + type MapRegion, +} from "@/components/map/MapView"; import { FloatingSearch } from "@/components/map/FloatingSearch"; import { FloatingActions } from "@/components/map/FloatingActions"; import { MapBottomSheet } from "@/components/map/MapBottomSheet"; @@ -27,8 +31,12 @@ const DEFAULT_REGION: MapRegion = { longitudeDelta: 0.12, }; -function hasLocation(scan: ScanRecord): scan is ScanRecord & { latitude: number; longitude: number } { - return typeof scan.latitude === "number" && typeof scan.longitude === "number"; +function hasLocation( + scan: ScanRecord, +): scan is ScanRecord & { latitude: number; longitude: number } { + return ( + typeof scan.latitude === "number" && typeof scan.longitude === "number" + ); } function computeInitialRegion(scans: ScanRecord[]): MapRegion { @@ -67,18 +75,27 @@ export default function MapScreen() { const geojsonCache = useRef>(new Map()); const activeFilterRef = useRef(null); const [activeFilter, setActiveFilter] = useState(null); + const [previewScan, setPreviewScan] = useState(null); activeFilterRef.current = activeFilter; useFocusEffect( useCallback(() => { reload(); - }, [reload]) + }, [reload]), ); const locatedScans = useMemo(() => history.filter(hasLocation), [history]); const initialRegion = useMemo(() => computeInitialRegion(history), [history]); function handleScanPress(scan: ScanRecord) { + // 2nd click on the same scan in preview → open detail + if (previewScan?.id === scan.id) { + setPreviewScan(null); + navigation.navigate("ScanDetail", { scanId: scan.id }); + return; + } + + // 1st click (or click on a different scan) → preview mode if (hasLocation(scan)) { mapRef.current?.animateToRegion({ latitude: scan.latitude, @@ -89,7 +106,12 @@ export default function MapScreen() { } setActiveFilter(null); mapRef.current?.highlightGeoJSON(null); - navigation.navigate("ScanDetail", { scanId: scan.id }); + setPreviewScan(scan); + sheetRef.current?.snapToIndex(0); + } + + function handlePreviewClose() { + setPreviewScan(null); } async function handleLocateUser() { @@ -183,18 +205,12 @@ export default function MapScreen() { pointerEvents="box-none" collapsable={false} > - + - navigation.navigate("Main", { screen: "Scanner" })} - defaultIndex={isEmpty ? 1 : 0} - /> - + + navigation.navigate("Main", { screen: "Scanner" })} + defaultIndex={isEmpty ? 1 : 0} + /> ); } @@ -228,14 +255,12 @@ const styles = StyleSheet.create({ left: 0, right: 0, paddingHorizontal: 16, - zIndex: 20, - elevation: 24, }, actionsSlot: { position: "absolute", right: 16, top: "30%", - zIndex: 20, - elevation: 24, + zIndex: 1, + elevation: 1, }, });