/** * Garden card components for PixieSprout. * SpaceCard. */ import React from 'react'; import { Image, StyleSheet, Text, TouchableOpacity, View, } from 'react-native'; import { pressWithHaptic } from '../constants/theme'; import { Card } from './BaseUI'; import { getSpaceArtwork } from '../assets/spaceArtwork'; import type { RoomProfile } from '../types/garden'; import type { SpaceSummary } from '../types/appTypes'; import { describeWeatherCode } from '../services/weather/weatherService'; import { formatSpaceType, formatSpaceLight, formatAmbientArtificialLight, formatHumidityProfile, formatSpaceStatusSummary, isOutdoorSpaceType, formatElevation, formatExposureShort, } from '../utils/formatters'; // ---- SpaceCard ---- /** Compact weather tag for a space card (Bekky, 2026-08-23): shows the current * condition + temp, and whether a covering deflects it. */ function formatWeatherChip(weather: import('../types/care').WeatherSnapshot, room: RoomProfile): string { const label = describeWeatherCode(weather.weatherCode); const temp = `${Math.round(weather.temperatureC)}°`; const roof = room.exposure?.roofType; // A solid/glass roof deflects rain and sun; a lattice/vine cover lets some // through. Note it so the user knows the chip isn't the plant's real exposure. let note = ''; if (roof === 'solid' || roof === 'glass') note = ' · covered'; else if (roof === 'permeable') note = ' · partial cover'; return `${label}, ${temp}${note}`; } /** Format a weather snapshot's fetchedAt into a short "as of HH:MM" label so * the user knows the forecast isn't live (Bekky, 2026-09-03 — stability-first: * the data is settled and won't change while you look, but it may be a few * hours old; the label makes that honest). Returns '' if no timestamp. */ function formatWeatherAsOf(weather: import('../types/care').WeatherSnapshot): string { if (!weather.fetchedAt) return ''; const d = new Date(weather.fetchedAt); if (Number.isNaN(d.getTime())) return ''; const hh = String(d.getHours()).padStart(2, '0'); const mm = String(d.getMinutes()).padStart(2, '0'); return `as of ${hh}:${mm}`; } export function SpaceCard({ room, summary, approximateLocationContext, weather, onPress, }: { room: RoomProfile; summary: SpaceSummary; approximateLocationContext?: string | null; weather?: import('../types/care').WeatherSnapshot | null; onPress: () => void; }) { const artwork = getSpaceArtwork(room.spaceType); const statusStyle = summary.statusTone === 'needs' ? s.spaceStatus_needs : summary.statusTone === 'good' ? s.spaceStatus_good : s.spaceStatus_empty; return ( {room.name} {room.name.toLowerCase().includes(formatSpaceType(room.spaceType).toLowerCase()) ? '' : `${formatSpaceType(room.spaceType)} · `}{summary.plantCount} {summary.plantCount === 1 ? 'plant' : 'plants'} Light: {formatSpaceLight(room)} {room.usesAmbientArtificialLight ? Ambient artificial light: {formatAmbientArtificialLight(room)} : null} Humidity: {formatHumidityProfile(room.humidityProfile)} {room.elevation && room.elevation !== 'unsure' ? {formatElevation(room.elevation)} : null} {isOutdoorSpaceType(room.spaceType) && room.exposure?.roofType && room.exposure.roofType !== 'none' && room.exposure.roofType !== 'unsure' ? {formatExposureShort(room.exposure.roofType)} : null} {isOutdoorSpaceType(room.spaceType) && approximateLocationContext ? Location: {approximateLocationContext} : null} {isOutdoorSpaceType(room.spaceType) && weather ? Weather: {formatWeatherChip(weather, room)}{formatWeatherAsOf(weather) ? ` · ${formatWeatherAsOf(weather)}` : ''} : null} {formatSpaceStatusSummary(summary)} ); } // ---- Styles ---- const s = StyleSheet.create({ entityRow: { flexDirection: 'row', alignItems: 'center', gap: 12, marginBottom: 8 }, entityPhoto: { width: 64, height: 64, borderRadius: 14 }, entityCopy: { flex: 1, minWidth: 0 }, entityTitle: { fontSize: 16, lineHeight: 20 }, entityIcon: { width: 64, height: 64, borderRadius: 14, backgroundColor: '#F3F7E6', alignItems: 'center', justifyContent: 'center' }, entityIconText: { color: '#5B7553', fontSize: 22, fontWeight: '900' }, tileTitle: { color: '#2E6B3F', fontSize: 14, fontWeight: '800' }, tileMeta: { color: '#78694B', fontSize: 10, lineHeight: 13, fontWeight: '700', marginTop: 2 }, spaceArtworkThumb: { width: 56, height: 56, borderRadius: 12, backgroundColor: '#F3F7E6' }, spaceTagRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 4, marginTop: 4 }, spaceEnvTag: { color: '#5B7553', fontSize: 10, fontWeight: '600', backgroundColor: '#F3F7E6', paddingHorizontal: 6, paddingVertical: 2, borderRadius: 6 }, spaceSummaryRow: { flexDirection: 'row', justifyContent: 'flex-end', marginTop: 4 }, spaceStatusPill: { fontSize: 11, fontWeight: '800', paddingHorizontal: 10, paddingVertical: 3, borderRadius: 999 }, spaceStatus_needs: { backgroundColor: '#FFF3E0', color: '#E65100' }, spaceStatus_good: { backgroundColor: '#E8F5D3', color: '#2E6B3F' }, spaceStatus_empty: { backgroundColor: '#F5F5F5', color: '#9E9E9E' }, small: { color: '#78694B', fontSize: 11 }, });