feat(map): preview mode + inline rename + floating actions tweaks
Comportement 2-clicks sur la liste des scans :
- 1er clic : map zoom sur le scan + sheet snap au plus bas + tile preview
unique avec hint 'Appuyez à nouveau pour voir les détails'
- 2e clic même scan : navigate vers ScanDetail
- Clic autre scan en preview : switch preview + zoom
Rename inline dans le BottomSheet existant (plus de 2e sheet superposé) :
- Bouton retour (ChevronLeft) au lieu de croix
- Form (BottomSheetTextInput) avec keyboardBehavior='interactive' +
android_keyboardInputMode='adjustResize' pour ne pas masquer l'input
- Boutons Annuler/Enregistrer avec icônes (X / Check), border 1.5px primary,
shadow primary[900] sur le Save, minHeight 56px
- snapToIndex(2) à l'ouverture du form (85%) puis snapToIndex(0) après
save/cancel pour redonner la map à voir
- containerStyle { zIndex:100, elevation:100 } pour passer au-dessus des
FloatingActions quand le sheet s'expand
FloatingActions : couleur d'icône via prop color (className n'est pas
supporté pour la couleur sur lucide-react-native sans cssInterop).
actionsSlot : zIndex/elevation 1 pour passer derrière le sheet expanded.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7457e64996
commit
98c446cd35
|
|
@ -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 (
|
||||
<View style={styles.column} collapsable={false}>
|
||||
<View className="gap-3 shadow-2xl" collapsable={false}>
|
||||
<ActionButton active={activeAction === "layers"} onPress={onLayers}>
|
||||
<Layers
|
||||
size={22}
|
||||
color={activeAction === "layers" ? "#FFFFFF" : colors.primary[800]}
|
||||
size={20}
|
||||
color={getIconColor(activeAction === "layers")}
|
||||
strokeWidth={2.2}
|
||||
/>
|
||||
</ActionButton>
|
||||
|
||||
<ActionButton active={activeAction === "locate"} onPress={onLocate}>
|
||||
<LocateFixed
|
||||
size={22}
|
||||
color={activeAction === "locate" ? "#FFFFFF" : colors.primary[800]}
|
||||
size={20}
|
||||
color={getIconColor(activeAction === "locate")}
|
||||
strokeWidth={2.2}
|
||||
/>
|
||||
</ActionButton>
|
||||
|
||||
<ActionButton active={activeAction === "satellite"} onPress={onSatellite}>
|
||||
<Satellite
|
||||
size={22}
|
||||
color={activeAction === "satellite" ? "#FFFFFF" : colors.primary[800]}
|
||||
size={20}
|
||||
color={getIconColor(activeAction === "satellite")}
|
||||
strokeWidth={2.2}
|
||||
/>
|
||||
</ActionButton>
|
||||
|
|
@ -55,45 +62,14 @@ function ActionButton({ children, onPress, active }: ActionButtonProps) {
|
|||
return (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
className={`w-10 h-10 rounded-full items-center justify-center shadow-lg ${
|
||||
active ? "bg-primary" : "bg-card border border-border"
|
||||
}`}
|
||||
style={({ pressed }) => [
|
||||
styles.button,
|
||||
active ? styles.buttonActive : styles.buttonInactive,
|
||||
pressed && { transform: [{ scale: 0.95 }] },
|
||||
{ transform: [{ scale: pressed ? 0.95 : 1 }] },
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<BottomSheet, MapBottomSheetProps>(
|
||||
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<BottomSheet>(null);
|
||||
const [renamingScan, setRenamingScan] = useState<ScanRecord | null>(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<BottomSheet, MapBottomSheetProps>(
|
|||
}
|
||||
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 (
|
||||
<>
|
||||
<BottomSheet
|
||||
ref={ref}
|
||||
ref={internalRef}
|
||||
index={defaultIndex}
|
||||
snapPoints={snapPoints}
|
||||
handleIndicatorStyle={styles.handleIndicator}
|
||||
backgroundStyle={styles.background}
|
||||
containerStyle={styles.sheetContainer}
|
||||
enableDynamicSizing={false}
|
||||
enablePanDownToClose={false}
|
||||
keyboardBehavior="interactive"
|
||||
keyboardBlurBehavior="restore"
|
||||
android_keyboardInputMode="adjustResize"
|
||||
>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>{t("map.scannedPlants")}</Text>
|
||||
<Text style={styles.count}>
|
||||
{t("map.plantCount", { count: scans.length })}
|
||||
</Text>
|
||||
</View>
|
||||
{renamingScan ? (
|
||||
<BottomSheetScrollView
|
||||
contentContainerStyle={[
|
||||
styles.renameWrap,
|
||||
{ paddingBottom: 24 },
|
||||
]}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={styles.renameHeader}>
|
||||
<Pressable
|
||||
onPress={handleCancelRename}
|
||||
hitSlop={10}
|
||||
style={styles.backBtn}
|
||||
>
|
||||
<ChevronLeft size={20} color={colors.neutral[700]} />
|
||||
</Pressable>
|
||||
<Text style={styles.title}>{t("map.rename.title")}</Text>
|
||||
<View style={styles.backBtnPlaceholder} />
|
||||
</View>
|
||||
<Text style={styles.renameSubtitle}>
|
||||
{t("map.rename.subtitle")}
|
||||
</Text>
|
||||
<BottomSheetTextInput
|
||||
value={draftName}
|
||||
onChangeText={setDraftName}
|
||||
placeholder={t("map.rename.placeholder")}
|
||||
placeholderTextColor={colors.neutral[400]}
|
||||
autoFocus
|
||||
maxLength={64}
|
||||
returnKeyType="done"
|
||||
onSubmitEditing={handleConfirmRename}
|
||||
style={styles.renameInput}
|
||||
/>
|
||||
<View style={styles.renameActions}>
|
||||
<Pressable
|
||||
onPress={handleCancelRename}
|
||||
style={({ pressed }) => [
|
||||
styles.renameBtn,
|
||||
styles.renameBtnGhost,
|
||||
pressed && { opacity: 0.7 },
|
||||
]}
|
||||
>
|
||||
<View style={styles.renameBtnInner}>
|
||||
<X
|
||||
size={18}
|
||||
color={colors.neutral[800]}
|
||||
strokeWidth={2.4}
|
||||
/>
|
||||
<RNText style={styles.renameBtnGhostLabel}>
|
||||
{t("common.cancel")}
|
||||
</RNText>
|
||||
</View>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={handleConfirmRename}
|
||||
style={({ pressed }) => [
|
||||
styles.renameBtn,
|
||||
styles.renameBtnPrimary,
|
||||
pressed && { opacity: 0.85 },
|
||||
]}
|
||||
>
|
||||
<View style={styles.renameBtnInner}>
|
||||
<Check size={18} color="#FFFFFF" strokeWidth={2.6} />
|
||||
<RNText style={styles.renameBtnPrimaryLabel}>
|
||||
{t("map.rename.save")}
|
||||
</RNText>
|
||||
</View>
|
||||
</Pressable>
|
||||
</View>
|
||||
</BottomSheetScrollView>
|
||||
) : previewScan ? (
|
||||
<View>
|
||||
<View style={styles.previewHeader}>
|
||||
<Text style={styles.title}>{t("map.preview.title")}</Text>
|
||||
<Pressable
|
||||
onPress={onPreviewClose}
|
||||
hitSlop={10}
|
||||
style={styles.previewCloseBtn}
|
||||
>
|
||||
<X size={18} color={colors.neutral[600]} />
|
||||
</Pressable>
|
||||
</View>
|
||||
<View style={styles.previewBody}>
|
||||
<ScanRow
|
||||
scan={previewScan}
|
||||
isLast
|
||||
onPress={() => onScanPress?.(previewScan)}
|
||||
onEdit={() => handleStartRename(previewScan)}
|
||||
/>
|
||||
<Text style={styles.previewHint}>
|
||||
{t("map.preview.tapHint")}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>{t("map.scannedPlants")}</Text>
|
||||
<Text style={styles.count}>
|
||||
{t("map.plantCount", { count: scans.length })}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{scans.length === 0 ? (
|
||||
{scans.length === 0 ? (
|
||||
<View style={styles.emptyState}>
|
||||
<View style={styles.emptyIconWrap}>
|
||||
<MapPin size={32} color={colors.primary[800]} strokeWidth={2} />
|
||||
|
|
@ -112,68 +263,10 @@ export const MapBottomSheet = forwardRef<BottomSheet, MapBottomSheetProps>(
|
|||
/>
|
||||
))}
|
||||
</BottomSheetScrollView>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</BottomSheet>
|
||||
|
||||
<Modal
|
||||
visible={renamingScan !== null}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={handleCancelRename}
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === "ios" ? "padding" : undefined}
|
||||
style={styles.modalOverlay}
|
||||
>
|
||||
<Pressable style={styles.modalBackdrop} onPress={handleCancelRename} />
|
||||
<View style={styles.modalCard}>
|
||||
<View style={styles.modalHeader}>
|
||||
<Text style={styles.modalTitle}>{t("map.rename.title")}</Text>
|
||||
<Pressable onPress={handleCancelRename} hitSlop={10}>
|
||||
<X size={20} color={colors.neutral[600]} />
|
||||
</Pressable>
|
||||
</View>
|
||||
<Text style={styles.modalSubtitle}>{t("map.rename.subtitle")}</Text>
|
||||
<TextInput
|
||||
style={styles.modalInput}
|
||||
value={draftName}
|
||||
onChangeText={setDraftName}
|
||||
placeholder={t("map.rename.placeholder")}
|
||||
placeholderTextColor={colors.neutral[400]}
|
||||
autoFocus
|
||||
maxLength={64}
|
||||
returnKeyType="done"
|
||||
onSubmitEditing={handleConfirmRename}
|
||||
/>
|
||||
<View style={styles.modalActions}>
|
||||
<Pressable
|
||||
onPress={handleCancelRename}
|
||||
style={({ pressed }) => [
|
||||
styles.modalButton,
|
||||
styles.modalButtonGhost,
|
||||
pressed && { opacity: 0.7 },
|
||||
]}
|
||||
>
|
||||
<Text style={styles.modalButtonGhostLabel}>
|
||||
{t("common.cancel")}
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={handleConfirmRename}
|
||||
style={({ pressed }) => [
|
||||
styles.modalButton,
|
||||
styles.modalButtonPrimary,
|
||||
pressed && { opacity: 0.85 },
|
||||
]}
|
||||
>
|
||||
<Text style={styles.modalButtonPrimaryLabel}>
|
||||
{t("map.rename.save")}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<Map<string, object>>(new Map());
|
||||
const activeFilterRef = useRef<string | null>(null);
|
||||
const [activeFilter, setActiveFilter] = useState<string | null>(null);
|
||||
const [previewScan, setPreviewScan] = useState<ScanRecord | null>(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}
|
||||
>
|
||||
<FloatingSearch activeFilter={activeFilter} onFilterPress={handleFilterPress} />
|
||||
<FloatingSearch
|
||||
activeFilter={activeFilter}
|
||||
onFilterPress={handleFilterPress}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<MapBottomSheet
|
||||
ref={sheetRef}
|
||||
scans={locatedScans}
|
||||
onScanPress={handleScanPress}
|
||||
onRename={handleRename}
|
||||
onScanCta={() => navigation.navigate("Main", { screen: "Scanner" })}
|
||||
defaultIndex={isEmpty ? 1 : 0}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={styles.actionsSlot}
|
||||
pointerEvents="box-none"
|
||||
|
|
@ -207,6 +223,17 @@ export default function MapScreen() {
|
|||
activeAction={activeFilter === "myLocation" ? "locate" : "layers"}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<MapBottomSheet
|
||||
ref={sheetRef}
|
||||
scans={locatedScans}
|
||||
previewScan={previewScan}
|
||||
onPreviewClose={handlePreviewClose}
|
||||
onScanPress={handleScanPress}
|
||||
onRename={handleRename}
|
||||
onScanCta={() => navigation.navigate("Main", { screen: "Scanner" })}
|
||||
defaultIndex={isEmpty ? 1 : 0}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue