/** * CuttingGuidelinesModal — Shows cutting propagation methods for a plant. * Centered, padded, readable. Only cutting-appropriate rooting methods are * shown (seed/germination methods are filtered out — they belong to the * separate seed-packet flow, not the cutting flow). */ import React, { useMemo, useState } from 'react'; import { ActivityIndicator, Modal, ScrollView, StyleSheet, Text, TouchableOpacity, useWindowDimensions, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import type { PropagationInfo, RootingMethod } from '../types/propagation'; import { ROOTING_METHOD_ICONS, ROOTING_METHOD_LABELS, DIFFICULTY_COLORS, SUCCESS_RATE_LABELS, cuttingMethodLabel, filterCuttingMethods, isCuttingMethod, isShortTimeframe, normalizeSuccessRate, cleanPropagationFreeText, } from '../types/propagation'; import { GlossaryText } from './GlossaryText'; import { green, dark, line } from '../constants/theme'; /** Title-case a plant name ("sweet olive" → "Sweet Olive"). Preserves * scientific names and acronyms (e.g. "Osmanthus fragrans" stays as-is). */ function titleCasePlantName(name: string): string { if (!name) return name; // If it looks like a scientific name (two words, second lowercase), keep it. const words = name.trim().split(/\s+/); if (words.length >= 2 && /^[a-z]/.test(words[1] || '') && /^[A-Z]/.test(words[0] || '')) { return name.trim(); } return words .map(w => (w ? w.charAt(0).toUpperCase() + w.slice(1) : w)) .join(' '); } interface CuttingGuidelinesModalProps { visible: boolean; plantName: string; propagationInfo: PropagationInfo | null; loading: boolean; onTakeCutting: (method: RootingMethod) => void; onClose: () => void; /** When the plant can't be propagated — offer to add the identified plant to the garden. */ onAddToGarden?: () => void; /** When the plant can't be propagated — offer to save it to the wishlist. */ onAddToWishlist?: () => void; /** Optional live weather snapshot — renders a real-time cutting-condition line. */ weather?: import('../types/care').WeatherSnapshot | null; } export function CuttingGuidelinesModal({ visible, plantName, propagationInfo, loading, onTakeCutting, onClose, onAddToGarden, onAddToWishlist, weather, }: CuttingGuidelinesModalProps) { const [selectedMethod, setSelectedMethod] = useState(null); // Only cutting-appropriate methods — seed/germination/division never appear here. // Also clean clipped free-text (risks/warning) and normalize success-rate // aliases so the modal renders uniformly (Bekky, 2026-08-20). const cuttingMethods = useMemo( () => { const cleaned = cleanPropagationFreeText(propagationInfo); return filterCuttingMethods(cleaned?.methods).map(m => { const normalizedRate = normalizeSuccessRate(m.successRate); return normalizedRate && normalizedRate !== m.successRate ? { ...m, successRate: normalizedRate } : m; }); }, [propagationInfo] ); const notRecommended = useMemo( () => (propagationInfo?.notRecommended || []).filter(nr => isCuttingMethod(nr.method)), [propagationInfo] ); // Real-time cutting-condition tip derived from the live weather snapshot // (Bekky, 2026-08-20). Best-effort — no weather = no line. const weatherTip = useMemo(() => { if (!weather) return null; const today = weather.daily?.[0]; const raining = weather.weatherCode >= 61 || (today && today.precipitationMm >= 2); const humid = weather.humidityPct != null && weather.humidityPct >= 70; const dry = weather.humidityPct != null && weather.humidityPct <= 40; const cold = today && today.tempMinC != null && today.tempMinC < 12; const hot = weather.temperatureC != null && weather.temperatureC >= 30; if (raining || humid) { return "🌧️ Humid and wet right now — cuttings root faster in moisture, but watch for rot in soggy conditions."; } if (dry) { return "💨 Dry air right now — mist cuttings or cover them to keep humidity up."; } if (cold) { return "❄️ Cool weather — rooting will be slower; consider a warmer, sheltered spot."; } if (hot) { return "☀️ Hot today — keep cuttings out of direct midday sun and watch for drying out."; } return null; }, [weather]); // Pixel-based card height (Bekky rule #2 — no percentage maxHeight on modal cards). const insets = useSafeAreaInsets(); const { height: windowHeight } = useWindowDimensions(); const cardMaxHeight = Math.max(320, windowHeight - (insets.top + 24) - (insets.bottom + 24)); if (!visible) return null; return ( <> ✂️ Take a Cutting {titleCasePlantName(plantName)} {/* The ✕ is hidden only in the can't-be-propagated dead-end, where "Back to Scan" replaces it (Bekky, 2026-08-20). It stays in the successful state so the modal can still be dismissed. */} {!(propagationInfo && !propagationInfo.canPropagate) && !loading ? ( ) : null} {loading ? ( Pixie is researching propagation methods... ) : !propagationInfo ? ( No propagation info available for this plant. ) : !propagationInfo.canPropagate ? ( This plant cannot be easily propagated from cuttings. {propagationInfo.generalNote ? ( {propagationInfo.generalNote} ) : null} {onAddToGarden ? ( Add to Garden ) : null} {onAddToWishlist ? ( Add to Wishlist ) : null} Back to Scan ) : cuttingMethods.length === 0 ? ( Pixie couldn't find specific cutting methods for this plant. {propagationInfo.generalNote ? ( {propagationInfo.generalNote} ) : null} 🌱 Seed starting is coming to the Scan tab soon! ) : ( {weatherTip ? ( {weatherTip} ) : null} {propagationInfo.generalNote ? ( ) : null} Choose a rooting method {cuttingMethods.map((method, i) => { const isOpen = selectedMethod === method.method; return ( setSelectedMethod(isOpen ? null : method.method)} > {ROOTING_METHOD_ICONS[method.method]} {cuttingMethodLabel(method.method)} {SUCCESS_RATE_LABELS[method.successRate] ? ( {SUCCESS_RATE_LABELS[method.successRate]} ) : null} {method.timeframe ? ( {method.timeframe} ) : null} {method.difficulty} {isOpen && ( {(method.instructions || []).length > 0 ? ( How to do it {(method.instructions || []).map((step, si) => ( {si + 1} ))} ) : ( Pixie is still gathering the steps for this method. )} {method.timeframe ? ( ⏱ Timeframe {method.timeframe} ) : null} {method.risks ? ( ⚠️ {method.risks} ) : null} {method.warning ? ( ❗ {method.warning} ) : null} onTakeCutting(method.method)} > ✂️ Take Cutting )} ); })} {notRecommended.length > 0 ? ( Not recommended for this plant: {notRecommended.map((nr, i) => ( ❌ {ROOTING_METHOD_LABELS[nr.method] || cuttingMethodLabel(nr.method)} — {nr.reason} ))} ) : null} )} ); } const s = StyleSheet.create({ overlay: { flex: 1, backgroundColor: 'rgba(16, 42, 16, 0.65)', justifyContent: 'center', alignItems: 'center', padding: 24, }, card: { width: '100%', maxWidth: 460, backgroundColor: '#FFFCF5', borderRadius: 24, padding: 24, paddingBottom: 20, maxHeight: '88%', shadowColor: '#000', shadowOpacity: 0.25, shadowRadius: 24, shadowOffset: { width: 0, height: 8 }, elevation: 12, }, header: { marginBottom: 16, alignItems: 'center', }, headerClose: { position: 'absolute', top: -4, right: -4, width: 34, height: 34, borderRadius: 17, backgroundColor: '#FFF9EE', borderWidth: 1.5, borderColor: line, alignItems: 'center', justifyContent: 'center', }, headerCloseText: { color: dark, fontSize: 16, fontWeight: '800', lineHeight: 18, }, emoji: { fontSize: 34, marginBottom: 4, }, title: { fontSize: 22, fontWeight: '800', color: dark, textAlign: 'center', }, subtitle: { fontSize: 15, color: '#5D5A4E', textAlign: 'center', marginTop: 4, width: '100%', flexWrap: 'wrap', lineHeight: 20, paddingHorizontal: 8, }, loadingWrap: { paddingVertical: 40, alignItems: 'center', }, loadingText: { fontSize: 15, color: '#5D5A4E', textAlign: 'center', lineHeight: 22, }, deadEndActions: { flexDirection: 'column', width: '100%', marginTop: 18, }, deadEndPrimary: { minHeight: 48, borderRadius: 24, backgroundColor: '#2E6B3F', alignItems: 'center', justifyContent: 'center', paddingHorizontal: 20, marginBottom: 10, }, deadEndPrimaryText: { color: '#FFFFFF', fontSize: 16, fontWeight: '800', }, deadEndSecondary: { minHeight: 48, borderRadius: 24, backgroundColor: '#EFF3EC', borderWidth: 1, borderColor: '#C9D4C2', alignItems: 'center', justifyContent: 'center', paddingHorizontal: 20, }, deadEndSecondaryText: { color: '#2E6B3F', fontSize: 16, fontWeight: '700', }, deadEndTertiary: { minHeight: 48, borderRadius: 24, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 20, }, deadEndTertiaryText: { color: '#8A8778', fontSize: 16, fontWeight: '700', }, noteText: { fontSize: 14, color: '#5D5A4E', textAlign: 'center', lineHeight: 21, marginBottom: 14, }, weatherTipText: { fontSize: 14, color: '#2E6B3F', textAlign: 'center', lineHeight: 21, marginBottom: 12, padding: 12, borderRadius: 14, backgroundColor: '#EAF5E2', borderWidth: 1, borderColor: '#C9E3BC', fontWeight: '600', }, seedHintText: { fontSize: 14, color: '#5A9E4B', textAlign: 'center', lineHeight: 21, marginTop: 2, fontWeight: '700', }, methodsList: { flexGrow: 0, }, methodsListContent: { paddingBottom: 16, }, sectionLabel: { fontSize: 13, fontWeight: '800', color: '#8A8571', textTransform: 'uppercase', letterSpacing: 0.6, marginBottom: 10, }, methodCard: { borderRadius: 18, borderWidth: 1.5, borderColor: line, backgroundColor: '#FFF9EE', padding: 16, marginBottom: 10, }, methodCardSelected: { borderColor: green, backgroundColor: '#F0F9E8', }, methodHeader: { flexDirection: 'row', alignItems: 'center', gap: 12, }, methodIcon: { fontSize: 30, }, methodInfo: { flex: 1, }, methodName: { fontSize: 17, fontWeight: '800', color: dark, }, methodSuccessRate: { fontSize: 13, fontWeight: '700', color: '#2E6B3F', marginTop: 3, }, methodTimeframe: { fontSize: 13.5, color: '#5D5A4E', marginTop: 2, lineHeight: 19, }, difficultyBadge: { borderRadius: 999, paddingHorizontal: 12, paddingVertical: 5, }, difficultyText: { fontSize: 12.5, fontWeight: '900', textTransform: 'capitalize', }, methodDetails: { marginTop: 14, paddingTop: 14, borderTopWidth: 1, borderTopColor: line, }, detailsLabel: { fontSize: 14, fontWeight: '800', color: dark, marginBottom: 8, }, instructionsBlock: { marginBottom: 4, }, instructionRow: { flexDirection: 'row', alignItems: 'flex-start', marginBottom: 8, gap: 10, }, instructionNumber: { width: 22, height: 22, borderRadius: 11, backgroundColor: green + '22', color: dark, fontSize: 12.5, fontWeight: '900', textAlign: 'center', lineHeight: 22, overflow: 'hidden', }, instructionStep: { flex: 1, fontSize: 14.5, color: '#2F302A', lineHeight: 21, }, noInstructionsText: { fontSize: 14, color: '#5D5A4E', fontStyle: 'italic', lineHeight: 20, marginBottom: 6, }, factRow: { marginTop: 4, }, factRowInline: { flexDirection: 'row', alignItems: 'center', gap: 8, }, factLabel: { fontSize: 13.5, fontWeight: '800', color: '#8A8571', }, factValue: { fontSize: 14, fontWeight: '700', color: '#2F302A', marginTop: 2, lineHeight: 21, }, factValueInline: { marginTop: 0, flexShrink: 1, }, risksText: { fontSize: 13.5, color: '#8A5D20', marginTop: 8, lineHeight: 19, }, warningText: { fontSize: 13.5, color: '#B74A35', marginTop: 4, lineHeight: 19, }, takeCuttingButton: { minHeight: 50, borderRadius: 999, backgroundColor: dark, alignItems: 'center', justifyContent: 'center', marginTop: 14, paddingHorizontal: 20, }, takeCuttingButtonText: { color: '#fff', fontSize: 16, fontWeight: '900', }, notRecommendedSection: { marginTop: 10, padding: 14, borderRadius: 18, backgroundColor: '#FFF3E8', }, notRecommendedTitle: { fontSize: 13.5, fontWeight: '800', color: '#B74A35', marginBottom: 6, }, notRecommendedItem: { fontSize: 13, color: '#8E3D2F', lineHeight: 19, marginBottom: 3, }, });