/** * RepotSetupModal — a dedicated, light-themed modal for recording setup changes * DURING a repot. Only reachable from the RepotModal's "Update setup" button. * * Purpose (Bekky, 2026-08-15): the one-and-done repot flow. A user who already * knows what they're doing hits Repot → Update setup → fills in soil/pot/photo * → "Save and Repot" records BOTH the setup update AND the repot event in one * action. No lost repot, no re-fetching guidance. * * This is deliberately a SIMPLIFIED setup form (soil, pot, photo only) — the * full setup form on the plant detail page is untouched. The plant detail page * stays as-is; this modal is ONLY for the repot flow. * * ALWAYS light (overrides dark theme — native Alert renders dark on Xiaomi). */ import React, { useEffect, useState } from 'react'; import { ActivityIndicator, Image, Modal, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, useWindowDimensions, View, } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { green, dark, line, haptic, pressWithHaptic } from '../constants/theme'; import { GardenChoiceChips, GardenMultiChips } from './BaseUI'; import { potSizeOptions, drainageOptions, mediumOptions, customMediumComponents, } from '../types/appTypes'; import { getPotTypeOptionsForSpace } from '../utils/formatters'; import type { PlantSetupProfile } from '../types/appTypes'; import type { GardenContextPhoto } from '../types/garden'; import type { SpaceType } from '../types/garden'; const EdgeModal = Modal as any; interface RepotSetupModalProps { visible: boolean; plantName: string; /** Pre-filled from the plant's existing setup (if any). */ initialSetup?: { potType?: PlantSetupProfile['potType']; potSize?: PlantSetupProfile['potSize']; drainage?: PlantSetupProfile['drainage']; mediumTypes?: PlantSetupProfile['mediumTypes']; customMedium?: string; customMediumComponents?: string[]; setupPhotoUri?: string; setupPhotos?: GardenContextPhoto[]; } | null; /** The plant's space type — used to keep pot-type options cohesive with the * setup form (Bekky, 2026-08-23). */ spaceType?: SpaceType | null; saving: boolean; onBack: () => void; onCancel: () => void; /** Save the setup AND record the repot event. */ onSaveAndRepot: (input: { potType: PlantSetupProfile['potType']; potSize: PlantSetupProfile['potSize']; drainage: PlantSetupProfile['drainage']; mediumTypes: PlantSetupProfile['mediumTypes']; customMedium?: string; customMediumComponents?: string[]; setupPhotoUri?: string; setupPhotos: GardenContextPhoto[]; }) => void; /** Open the photo picker (camera/gallery) for the setup photo. */ onPickPhoto: () => void; /** A photo just picked via onPickPhoto — merged into the modal's photo state. */ pickedPhoto?: GardenContextPhoto | null; } export function RepotSetupModal({ visible, plantName, initialSetup, spaceType, saving, onBack, onCancel, onSaveAndRepot, onPickPhoto, pickedPhoto, }: RepotSetupModalProps) { const [potType, setPotType] = useState>('unsure'); const [potSize, setPotSize] = useState>('unsure'); const [drainage, setDrainage] = useState>('unsure'); const [mediumTypes, setMediumTypes] = useState([]); const [customMedium, setCustomMedium] = useState(''); const [customMixComponents, setCustomMixComponents] = useState([]); const [setupPhotos, setSetupPhotos] = useState([]); // PROVEN pattern (SettingsPanel): pixel-based card/scroll heights computed // from the actual window + safe-area insets. Percentage-of-card maxHeight is // ambiguous for a content-sized card, which is what kept overflowing. // Cap the ScrollView's OWN height so the tall form scrolls inside and the // footer buttons (siblings OUTSIDE the ScrollView) stay pinned & reachable. const insets = useSafeAreaInsets(); const { height: windowHeight } = useWindowDimensions(); const topPad = Math.max(insets.top, 16) + 12; const bottomPad = Math.max(insets.bottom, 16) + 16; const cardMaxHeight = Math.max(320, windowHeight - topPad - bottomPad); const scrollMaxHeight = Math.max(180, cardMaxHeight - 118); // Seed from the plant's existing setup when the modal opens. useEffect(() => { if (visible) { setPotType(initialSetup?.potType || 'unsure'); setPotSize(initialSetup?.potSize || 'unsure'); setDrainage(initialSetup?.drainage || 'unsure'); setMediumTypes(initialSetup?.mediumTypes || []); setCustomMedium(initialSetup?.customMedium || ''); setCustomMixComponents(initialSetup?.customMediumComponents || []); setSetupPhotos(initialSetup?.setupPhotos || (initialSetup?.setupPhotoUri ? [{ id: 'existing', uri: initialSetup.setupPhotoUri } as GardenContextPhoto] : [])); } }, [visible, initialSetup]); // Merge a freshly-picked photo into the modal's photo state. useEffect(() => { if (visible && pickedPhoto) { setSetupPhotos(current => { const without = (current || []).filter(p => p.id !== pickedPhoto.id); return [pickedPhoto, ...without]; }); } }, [visible, pickedPhoto]); if (!visible) return null; const toggleMedium = (medium: string) => { setMediumTypes(current => { const selected = current || []; const next = selected.includes(medium as NonNullable) ? selected.filter(v => v !== medium) : [...selected, medium as NonNullable]; return next; }); }; const toggleCustomComponent = (component: string) => { setCustomMixComponents(current => { const selected = current || []; return selected.includes(component) ? selected.filter(v => v !== component) : [...selected, component]; }); }; const handleSaveAndRepot = () => { onSaveAndRepot({ potType, potSize, drainage, mediumTypes, customMedium: customMedium.trim() || undefined, customMediumComponents: customMixComponents, setupPhotoUri: setupPhotos[0]?.uri, setupPhotos, }); }; return ( ← Back 🪴 Repot {plantName} Record your new setup while you repot. Save and Repot does both at once. {/* Pot */} Pot Pot type {potType !== 'ground' && potType !== 'garden_bed' && potType !== 'raised_bed' ? ( <> Pot size o.value !== 'not_applicable')} value={potSize === 'not_applicable' ? 'unsure' : potSize} onChange={setPotSize} /> Drainage o.value !== 'not_applicable')} value={drainage === 'not_applicable' ? 'unsure' : drainage} onChange={setDrainage} /> ) : null} {/* Soil / medium — MUST mirror the setup form's store-bought mixes exactly (Bekky, 2026-08-23). Both use the same flat mediumOptions list (all non-custom), so repot and setup can never drift. Custom is captured via the text input below. */} Soil Growing medium Pick what's in the pot — you can choose more than one. Store-bought mixes o.value !== 'custom' && o.value !== 'garden_soil')} values={mediumTypes} onToggle={toggleMedium} /> Custom Anything else? {/* Photo */} Photo Snap a photo of the new pot and soil so Pixie can see the setup. {setupPhotos.length ? ( ) : null} {setupPhotos.length ? 'Change Photo' : 'Add Photo'} {/* Actions — INSIDE the scroll, last thing you reach (cutting-modal pattern: no fixed footer, buttons sit at the end of the content). */} Cancel {saving ? : Save and Repot} ); } const s = StyleSheet.create({ overlay: { flex: 1, backgroundColor: 'rgba(16, 42, 16, 0.6)', justifyContent: 'center', alignItems: 'center', padding: 20, }, card: { width: '100%', maxWidth: 400, maxHeight: '92%', backgroundColor: '#FFFCF5', borderRadius: 24, padding: 20, shadowColor: '#000', shadowOpacity: 0.25, shadowRadius: 20, elevation: 12, }, headerRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 }, backButton: { minWidth: 60, minHeight: 34, borderRadius: 17, borderWidth: 1.5, borderColor: line, backgroundColor: '#FFF9EE', alignItems: 'center', justifyContent: 'center', paddingHorizontal: 10 }, backText: { color: dark, fontSize: 13, fontWeight: '800' }, title: { color: dark, fontSize: 18, fontWeight: '900', flex: 1, textAlign: 'center', marginHorizontal: 6 }, headerSpacer: { width: 60 }, // flexGrow:0 (like RepotModal/cutting modal). The pixel maxHeight is applied // inline from useWindowDimensions so the tall form stays bounded, and the // action buttons now live INSIDE this scroll (end-of-content, cutting-modal style). scroll: { flexGrow: 0 }, scrollContent: { gap: 8, paddingBottom: 20 }, helper: { color: '#5D5A4E', fontSize: 13, lineHeight: 19, marginBottom: 4 }, sectionTitle: { color: dark, fontSize: 16, fontWeight: '900', marginTop: 10, marginBottom: 2 }, fieldLabel: { color: dark, fontSize: 13, fontWeight: '900', marginTop: 8, marginBottom: 4 }, fieldHelper: { color: '#706A58', fontSize: 12, lineHeight: 16, marginBottom: 4 }, groupLabel: { color: '#706A58', fontSize: 12, fontWeight: '800', marginTop: 8, marginBottom: 2 }, input: { backgroundColor: '#FFFDF7', borderWidth: 1, borderColor: 'rgba(191,217,180,0.95)', borderRadius: 14, paddingHorizontal: 14, paddingVertical: 10, fontSize: 15, color: dark, marginTop: 6 }, photo: { width: '100%', height: 160, borderRadius: 14, marginBottom: 8, backgroundColor: '#E8F5D3' }, photoButton: { minHeight: 44, borderRadius: 999, borderWidth: 1.5, borderColor: line, backgroundColor: '#FFF9EE', alignItems: 'center', justifyContent: 'center', paddingHorizontal: 16 }, photoButtonText: { color: dark, fontSize: 14, fontWeight: '800' }, actionRow: { flexDirection: 'row', gap: 12, marginTop: 16 }, cancelButton: { flex: 1, minHeight: 50, borderRadius: 999, borderWidth: 1.5, borderColor: '#B85C4A', backgroundColor: '#FCE9E6', alignItems: 'center', justifyContent: 'center' }, cancelText: { color: '#8E2F1E', fontSize: 15, fontWeight: '900' }, saveButton: { flex: 1, minHeight: 50, borderRadius: 999, backgroundColor: green, alignItems: 'center', justifyContent: 'center' }, saveDisabled: { opacity: 0.6 }, saveText: { color: '#fff', fontSize: 15, fontWeight: '900' }, });