diff --git a/VinEye/src/components/my-plants/EditNameBottomSheet.tsx b/VinEye/src/components/my-plants/EditNameBottomSheet.tsx new file mode 100644 index 0000000..856987e --- /dev/null +++ b/VinEye/src/components/my-plants/EditNameBottomSheet.tsx @@ -0,0 +1,140 @@ +import { useEffect, useState } from "react"; +import { + View, + Pressable, + Modal, + KeyboardAvoidingView, + Platform, + TextInput, + TouchableWithoutFeedback, + Keyboard, +} from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { useTranslation } from "react-i18next"; +import { X, Check } from "lucide-react-native"; + +import { Text } from "@/components/ui/text"; +import { colors } from "@/theme/colors"; + +interface EditNameBottomSheetProps { + initialName: string; + onSave: (newName: string) => void; + onClose: () => void; +} + +export function EditNameBottomSheet({ + initialName, + onSave, + onClose, +}: EditNameBottomSheetProps) { + const { t } = useTranslation(); + const insets = useSafeAreaInsets(); + const [name, setName] = useState(initialName); + + useEffect(() => { + setName(initialName); + }, [initialName]); + + const trimmedName = name.trim(); + const isDirty = + trimmedName.length > 0 && trimmedName !== initialName.trim(); + + function handleSave() { + if (!isDirty) return; + onSave(trimmedName); + } + + return ( + + + + + + + + + + + + + + + {t("myPlants.detail.renameTitle")} + + + + + + + + {t("myPlants.detail.renameSubtitle")} + + + + + + + + + + {t("myPlants.actions.cancel")} + + + + + + + + {t("myPlants.detail.renameSave")} + + + + + + + + + ); +} diff --git a/VinEye/src/hooks/useScanDetail.ts b/VinEye/src/hooks/useScanDetail.ts index ee5809d..bad7bf6 100644 --- a/VinEye/src/hooks/useScanDetail.ts +++ b/VinEye/src/hooks/useScanDetail.ts @@ -38,5 +38,33 @@ export function useScanDetail(scanId: string) { await storage.set(storage.KEYS.SCAN_HISTORY, updated); }, [scanId]); - return { scan, loading, error, toggleFavorite, deleteScan, refetch: load }; + const renameScan = useCallback( + async (newName: string) => { + const trimmed = newName.trim(); + const all = await storage.get(storage.KEYS.SCAN_HISTORY); + if (!all) return; + const updated = all.map((s) => + s.id === scanId + ? { ...s, customName: trimmed.length > 0 ? trimmed : undefined } + : s, + ); + await storage.set(storage.KEYS.SCAN_HISTORY, updated); + setScan((prev) => + prev + ? { ...prev, customName: trimmed.length > 0 ? trimmed : undefined } + : prev, + ); + }, + [scanId], + ); + + return { + scan, + loading, + error, + toggleFavorite, + deleteScan, + renameScan, + refetch: load, + }; } diff --git a/VinEye/src/screens/ScanDetailScreen.tsx b/VinEye/src/screens/ScanDetailScreen.tsx index 38eeafa..eb89ecd 100644 --- a/VinEye/src/screens/ScanDetailScreen.tsx +++ b/VinEye/src/screens/ScanDetailScreen.tsx @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { View, ScrollView, @@ -27,6 +27,7 @@ import { Share2, Trash2, AlertCircle, + Pencil, } from "lucide-react-native"; import Animated, { useSharedValue, @@ -37,6 +38,7 @@ import Animated, { import { toast } from "sonner-native"; import { Text } from "@/components/ui/text"; +import { EditNameBottomSheet } from "@/components/my-plants/EditNameBottomSheet"; import { useScanDetail } from "@/hooks/useScanDetail"; import { getCepageById } from "@/utils/cepages"; import { hapticSuccess } from "@/services/haptics"; @@ -94,8 +96,9 @@ export default function ScanDetailScreen({ route }: Props) { const { t, i18n } = useTranslation(); const navigation = useNavigation(); const insets = useSafeAreaInsets(); - const { scan, loading, error, toggleFavorite, deleteScan } = + const { scan, loading, error, toggleFavorite, deleteScan, renameScan } = useScanDetail(scanId); + const [editingName, setEditingName] = useState(false); // Entry animation const contentY = useSharedValue(30); @@ -160,11 +163,27 @@ export default function ScanDetailScreen({ route }: Props) { const isFav = scan.isFavorite === true; const hasImage = !!detection.imageUri; - const heroTitle = cepage + const fallbackTitle = cepage ? cepage.name.fr : detection.result === "vine" ? t("myPlants.detail.results.vine") : t("myPlants.detail.results.unidentified"); + const heroTitle = scan.customName?.trim() || fallbackTitle; + + function handleOpenRename() { + setEditingName(true); + } + + function handleCloseRename() { + setEditingName(false); + } + + async function handleRenameSave(newName: string) { + await renameScan(newName); + setEditingName(false); + hapticSuccess(); + toast.success(t("myPlants.toasts.renamed")); + } async function handleToggleFavorite() { await toggleFavorite(); @@ -281,6 +300,13 @@ export default function ScanDetailScreen({ route }: Props) { fill={isFav ? "#FFB800" : "none"} /> + + + {/* ── Content ── */} @@ -440,6 +466,14 @@ export default function ScanDetailScreen({ route }: Props) { + + {editingName && ( + + )} ); }