import React, { useMemo, useState } from 'react'; import { Modal, View, Text, TouchableOpacity, ScrollView, Image, StyleSheet, } from 'react-native'; import { green, dark, line, brown } from '../constants/theme'; import type { SavedPlantProfile } from '../types/plantScan'; import type { SeedIdentificationResult } from '../services/plantIdentification/types'; /** * Seed Bank — Phase 3 (2026-09-07). * * The saved-seed collection — the seeds the user has intaked (via the seed * scan) organized into a personal seed bank. Each seed expands to show its * OWN seed-saving + germination profile (seed type, comes-true-from-seed, * saving/storage, germination) — the guidance is per-seed, not a generic guide * tab, because people don't know terms like "orthodox" vs "recalcitrant" * (Bekky, 2026-09-07). "Plant" opens the planting flow; "Delete" removes it. * * Rendered as a self-contained modal so it doesn't disturb the fragile Garden * pager. Entry point: the Seed Bank tile pinned to the top of the Plants section. */ export function SeedBankModal({ visible, onClose, savedPlants, onPlantSeed, onDeleteSeed, }: { visible: boolean; onClose: () => void; savedPlants: SavedPlantProfile[]; /** Open the planting flow for a saved seed (Bekky, 2026-09-07). */ onPlantSeed?: (plant: SavedPlantProfile) => void; /** Delete a saved seed from the bank (Bekky, 2026-09-07). */ onDeleteSeed?: (plant: SavedPlantProfile) => void; }) { // Seeds the user has saved via the seed scan (scanMode === 'seed'). // NOTE (Bekky, 2026-09-08): filter on scanMode ONLY (not `&& p.seed`) so // entries saved in earlier builds that lack the rich `seed` object still // show as tiles — a retroactive fix. SeedCard handles a missing seed. // NOTE (Bekky, 2026-09-08): a PLANTED seed (scanMode 'seed' WITH a // growthStage, e.g. 'seedling' via Start Growing) is a Garden citizen, NOT a // bank seed — it must NOT appear here. Only bank-only seeds (no growthStage) // belong in the Seed Bank. This matches the Garden grid's `isSeedBankOnly` // predicate so a seed saved to the bank AND planted shows exactly once (in // the Garden), not twice (Garden + bank). const savedSeeds = useMemo( () => savedPlants.filter((p) => p.scanMode === 'seed' && !p.growthStage), [savedPlants], ); // Expiry-acknowledge modal (Bekky, 2026-09-08): when a recalcitrant seed is // expiring (past its plant-by date), show a pop-up acknowledging it's no // longer viable, with delete on acknowledge. const [expiringSeed, setExpiringSeed] = useState(null); return ( 🌱 Seed Bank {savedSeeds.length === 0 ? 'Seeds you scan land here with their own saving & planting guide.' : `${savedSeeds.length} saved seed${savedSeeds.length === 1 ? '' : 's'} — tap one to see its guide.`} {savedSeeds.length === 0 ? ( 🌱 No saved seeds yet When you scan a seed (packet, fruit, pod, or wild), it lands here with its own seed-saving and germination profile. ) : ( {savedSeeds.map((p) => ( setExpiringSeed(p)} /> ))} )} {/* Expiry-acknowledge modal (Bekky, 2026-09-08): a recalcitrant seed past its plant-by date is no longer viable — acknowledge + delete. */} setExpiringSeed(null)}> ⏳ This seed has expired {expiringSeed?.commonName || 'This seed'} was meant to be planted by{' '} {expiringSeed?.expiresAt ? new Date(expiringSeed.expiresAt).toLocaleDateString() : 'now'}. It's past that date, so it's likely no longer viable. You can remove it from your Seed Bank. { if (expiringSeed && onDeleteSeed) onDeleteSeed(expiringSeed); setExpiringSeed(null); }} > Remove from Seed Bank setExpiringSeed(null)} > Keep it ); } function SeedCard({ plant, seed, onPlantSeed, onDeleteSeed, onExpired, }: { plant: SavedPlantProfile; seed?: SeedIdentificationResult; onPlantSeed?: (plant: SavedPlantProfile) => void; onDeleteSeed?: (plant: SavedPlantProfile) => void; /** Fired when this seed is past its plant-by date (Bekky, 2026-09-08). */ onExpired?: (plant: SavedPlantProfile) => void; }) { const [expanded, setExpanded] = useState(false); const name = plant.commonName || seed?.commonName || 'Unknown seed'; const imageUri = plant.photoUri || plant.identification?.imageUri || plant.identification?.imageUris?.[0] || ''; const seedTypeLabel = seed?.seedType === 'recalcitrant' ? 'Recalcitrant — must be planted fresh. Dies if dried or refrigerated.' : seed?.seedType === 'orthodox' ? 'Orthodox — can be dried and stored for later planting.' : 'Seed type unknown.'; const comesTrueLabel = seed?.comesTrueFromSeed === true ? 'Yes — comes true from seed.' : seed?.comesTrueFromSeed === false ? 'No — does NOT come true from seed.' : 'Unknown whether it comes true from seed.'; // Expired? (Bekky, 2026-09-08): a recalcitrant seed past its plant-by date. const isExpired = !!plant.expiresAt && new Date(plant.expiresAt).getTime() < Date.now(); // Fire the expiry-acknowledge once when the seed is past its date. React.useEffect(() => { if (isExpired && onExpired) onExpired(plant); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( setExpanded((e) => !e)}> {imageUri ? ( ) : ( 🌱 )} {name} {seed?.scientificName ? {seed.scientificName} : null} {isExpired ? ⏳ Expired : null} {expanded ? '^' : 'v'} {expanded ? ( 🌱 Seed-saving & germination guide {!seed ? ( This seed was saved before the full seed profile was available — no detailed guide was captured. ) : ( <> {seed.comesTrueNote ? : null} {seed.canSaveSeed !== undefined ? : null} {seed.savingMethod ? : null} {seed.storageMethod ? : null} {seed.storageDuration ? : null} {plant.expiresAt ? ( ⏳ Must be planted by {new Date(plant.expiresAt).toLocaleDateString()} — this seed doesn't store well and will lose viability after that. ) : null} {seed.stratification && seed.stratification.needed ? ( ) : null} {seed.scarification && seed.scarification.needed ? ( ) : null} {seed.plantingDepth ? : null} {seed.daysToGerminate ? : null} {seed.germinationSteps && seed.germinationSteps.length ? ( Germination steps {seed.germinationSteps.map((step, i) => ( - {step} ))} ) : null} )} {onPlantSeed ? ( onPlantSeed(plant)}> Plant ) : null} {onDeleteSeed ? ( onDeleteSeed(plant)}> Delete ) : null} ) : null} ); } function Row({ label, value }: { label: string; value: string }) { return ( {label} {value} ); } const styles = StyleSheet.create({ overlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.45)', alignItems: 'center', justifyContent: 'center', padding: 24 }, card: { width: '100%', maxWidth: 420, height: '85%', borderRadius: 24, backgroundColor: '#F8F4E8', padding: 18, overflow: 'hidden' }, cardEmpty: { height: 'auto', maxWidth: 360 }, headerRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4 }, title: { color: dark, fontSize: 20, fontWeight: '900' }, closeButton: { minWidth: 34, height: 34, borderRadius: 17, backgroundColor: '#E8E0C8', alignItems: 'center', justifyContent: 'center' }, closeText: { color: dark, fontSize: 15, fontWeight: '900' }, subtitle: { color: '#7A704D', fontSize: 12, lineHeight: 17, fontWeight: '600', marginBottom: 10 }, body: { flex: 1 }, bodyContent: { paddingBottom: 8 }, emptyWrap: { alignItems: 'center', paddingVertical: 20, paddingHorizontal: 12 }, emptyEmoji: { fontSize: 40, marginBottom: 8 }, emptyTitle: { color: dark, fontSize: 16, fontWeight: '900', marginBottom: 6 }, emptyText: { color: '#7A704D', fontSize: 13, lineHeight: 19, fontWeight: '600', textAlign: 'center' }, seedCard: { borderRadius: 18, borderWidth: 1, borderColor: line, backgroundColor: '#FFFDF7', marginBottom: 10, overflow: 'hidden' }, seedCardHeader: { flexDirection: 'row', alignItems: 'center', padding: 12, gap: 12 }, seedCardThumb: { width: 56, height: 56, borderRadius: 12, backgroundColor: '#E8F5D3', flexShrink: 0 }, seedCardThumbPlaceholder: { alignItems: 'center', justifyContent: 'center' }, seedCardThumbPlaceholderText: { fontSize: 26 }, seedCardTitleWrap: { flex: 1, minWidth: 0 }, seedCardName: { color: dark, fontSize: 15, fontWeight: '900' }, seedCardSci: { color: '#7A704D', fontSize: 12, fontWeight: '600', fontStyle: 'italic', marginTop: 1 }, seedCardChevron: { color: green, fontSize: 15, fontWeight: '900', marginLeft: 8 }, seedCardBody: { borderTopWidth: 1, borderTopColor: line, padding: 14, paddingTop: 10 }, seedCardSectionTitle: { color: dark, fontSize: 13, fontWeight: '900', marginBottom: 8 }, seedCardActions: { flexDirection: 'row', gap: 8, marginTop: 10 }, plantButton: { flex: 1, minHeight: 40, borderRadius: 20, backgroundColor: green, alignItems: 'center', justifyContent: 'center' }, plantButtonText: { color: '#fff', fontSize: 13, fontWeight: '900' }, deleteButton: { flex: 1, minHeight: 40, borderRadius: 20, borderWidth: 1.5, borderColor: '#B85C4A', backgroundColor: '#FCE9E6', alignItems: 'center', justifyContent: 'center' }, deleteButtonText: { color: '#8E2F1E', fontSize: 13, fontWeight: '900' }, row: { flexDirection: 'row', gap: 8, marginBottom: 5 }, rowLabel: { color: '#7A704D', fontSize: 12, fontWeight: '800', width: 120 }, rowValue: { flex: 1, color: '#2F302A', fontSize: 12, lineHeight: 17, fontWeight: '600' }, expiryBox: { borderRadius: 10, borderWidth: 1, borderColor: '#E7C9A8', backgroundColor: '#FDF0E7', paddingHorizontal: 10, paddingVertical: 8, marginTop: 4, marginBottom: 6 }, expiryText: { color: '#9A5B2A', fontSize: 12, lineHeight: 17, fontWeight: '600' }, expiredBadge: { color: '#B85C4A', fontSize: 11, fontWeight: '900', marginTop: 2 }, expiryModalCard: { width: '100%', maxWidth: 360, borderRadius: 20, backgroundColor: '#F8F4E8', padding: 20 }, expiryModalTitle: { color: dark, fontSize: 17, fontWeight: '900', marginBottom: 8 }, expiryModalText: { color: '#7A704D', fontSize: 13, lineHeight: 19, fontWeight: '600', marginBottom: 16 }, expiryModalActions: { flexDirection: 'row', gap: 8 }, expiryModalDelete: { flex: 1, minHeight: 42, borderRadius: 21, backgroundColor: '#B85C4A', alignItems: 'center', justifyContent: 'center' }, expiryModalDeleteText: { color: '#fff', fontSize: 13, fontWeight: '900' }, expiryModalKeep: { flex: 1, minHeight: 42, borderRadius: 21, borderWidth: 1.5, borderColor: line, backgroundColor: '#FFFDF7', alignItems: 'center', justifyContent: 'center' }, expiryModalKeepText: { color: '#2F302A', fontSize: 13, fontWeight: '900' }, stepsWrap: { marginTop: 2, marginBottom: 4 }, stepText: { color: '#2F302A', fontSize: 12, lineHeight: 17, fontWeight: '600', marginLeft: 8 }, });