Files
Greenlens/app/plant/[id].tsx
2026-05-08 13:00:30 +02:00

1475 lines
49 KiB
TypeScript

import React, { useMemo, useState } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
Modal,
Alert,
Share,
} from 'react-native';
import { useLocalSearchParams, useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import * as Haptics from 'expo-haptics';
import * as ImagePicker from 'expo-image-picker';
import { useApp } from '../../context/AppContext';
import { useColors } from '../../constants/Colors';
import { requestPermissions, scheduleWateringReminder, cancelReminder } from '../../services/notificationService';
import { SafeImage } from '../../components/SafeImage';
const parseColorToRgb = (input: string) => {
const color = input.trim();
if (color.startsWith('#')) {
const hex = color.slice(1);
const normalized = hex.length === 3
? hex.split('').map((char) => `${char}${char}`).join('')
: hex;
if (normalized.length === 6) {
return {
r: Number.parseInt(normalized.slice(0, 2), 16),
g: Number.parseInt(normalized.slice(2, 4), 16),
b: Number.parseInt(normalized.slice(4, 6), 16),
};
}
}
const rgbMatch = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i);
if (rgbMatch) {
return {
r: Number.parseInt(rgbMatch[1], 10),
g: Number.parseInt(rgbMatch[2], 10),
b: Number.parseInt(rgbMatch[3], 10),
};
}
return { r: 127, g: 127, b: 127 };
};
const getReadableTextColor = (background: string, dark: string, light: string) => {
const { r, g, b } = parseColorToRgb(background);
const luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
return luminance > 0.58 ? dark : light;
};
const HEALTH_CHECK_CREDIT_COST = 2;
const getHealthCopy = (language: 'de' | 'en' | 'es') => {
if (language === 'de') {
return {
title: 'Health Check',
action: 'Health-Scan starten',
running: 'Neues Foto wird analysiert...',
cost: `Kosten: ${HEALTH_CHECK_CREDIT_COST} Credits`,
intro: 'Fotografiere die ganze Pflanze plus auffaellige Blaetter. Danach bekommst du Diagnose, Dringlichkeit und einen konkreten Pflegeplan.',
creditsLabel: 'Credits',
managePlan: 'Plan verwalten',
noCreditsTitle: 'Nicht genug Credits',
noCreditsMessage: `Du brauchst ${HEALTH_CHECK_CREDIT_COST} Credits fuer den Health-Check.`,
insufficientInline: 'Nicht genug Credits fuer den Health-Check.',
timeoutInline: 'Health-Check Timeout. Bitte erneut versuchen.',
providerInline: 'Health-Check ist gerade nicht verfuegbar.',
analysisTitle: 'Analyse',
analysisFallback: 'Die Pflanze wirkt insgesamt beurteilbar, aber die gespeicherte Analyse enthaelt noch keine ausformulierte Zusammenfassung. Orientiere dich deshalb an Score, Ursachen und Sofortmassnahmen. Pruefe zuerst die auffaelligsten Blaetter, danach Substratfeuchte und Standort. Wenn die Blaetter innerhalb von 48 Stunden weiter haengen, gelb werden oder Flecken ausbreiten, solltest du ein neues Foto bei hellem indirektem Licht aufnehmen. Ein neuer Health-Scan kann dann genauer zwischen Wasserstress, Lichtstress, Schaedlingen und normaler Blattalterung unterscheiden.',
issuesTitle: 'Wahrscheinlichste Ursachen',
actionsTitle: 'Sofortmassnahmen',
planTitle: '7-Tage-Plan',
scoreLabel: 'Gesundheits-Score',
healthy: 'Stabil',
watch: 'Beobachten',
critical: 'Kritisch',
lastCheck: 'Zuletzt geprueft',
};
}
if (language === 'es') {
return {
title: 'Health Check',
action: 'Iniciar health-scan',
running: 'Analizando foto nueva...',
cost: `Costo: ${HEALTH_CHECK_CREDIT_COST} creditos`,
intro: 'Fotografia la planta completa y las hojas llamativas. Luego recibes diagnostico, urgencia y un plan de cuidado concreto.',
creditsLabel: 'Creditos',
managePlan: 'Gestionar plan',
noCreditsTitle: 'Creditos insuficientes',
noCreditsMessage: `Necesitas ${HEALTH_CHECK_CREDIT_COST} creditos para el health-check.`,
insufficientInline: 'No hay creditos suficientes para el health-check.',
timeoutInline: 'Health-check agotado por tiempo. Intenta de nuevo.',
providerInline: 'Health-check no disponible ahora.',
analysisTitle: 'Analisis',
analysisFallback: 'La planta se puede evaluar en general, pero este chequeo guardado todavia no contiene un resumen completo. Usa el puntaje, las causas y las acciones inmediatas como guia principal. Revisa primero las hojas mas llamativas, despues la humedad del sustrato y la ubicacion. Si las hojas empeoran en 48 horas, amarillean o las manchas se expanden, toma una foto nueva con luz indirecta clara. Un nuevo health-scan podra diferenciar mejor entre exceso o falta de agua, luz, plagas y envejecimiento normal.',
issuesTitle: 'Causas mas probables',
actionsTitle: 'Acciones inmediatas',
planTitle: 'Plan de 7 dias',
scoreLabel: 'Puntaje de salud',
healthy: 'Estable',
watch: 'Observar',
critical: 'Critico',
lastCheck: 'Ultima revision',
};
}
return {
title: 'Health Check',
action: 'Start health scan',
running: 'Analyzing new photo...',
cost: `Cost: ${HEALTH_CHECK_CREDIT_COST} credits`,
intro: 'Photograph the full plant plus any suspicious leaves. You will get a diagnosis, urgency level, and a concrete care plan.',
creditsLabel: 'Credits',
managePlan: 'Manage plan',
noCreditsTitle: 'Not enough credits',
noCreditsMessage: `You need ${HEALTH_CHECK_CREDIT_COST} credits for the health check.`,
insufficientInline: 'Not enough credits for the health check.',
timeoutInline: 'Health check timed out. Please try again.',
providerInline: 'Health check is unavailable right now.',
analysisTitle: 'Analysis',
analysisFallback: 'The plant is still assessable, but this saved check does not include a full written summary yet. Use the score, likely causes, and immediate actions as the primary guide. Start by inspecting the most unusual leaves, then check soil moisture and placement. If leaves droop further, yellowing spreads, or spots expand within 48 hours, take a new photo in bright indirect light. A fresh health scan can separate watering stress, light stress, pests, and normal leaf aging more accurately.',
issuesTitle: 'Most likely causes',
actionsTitle: 'Actions now',
planTitle: '7-day plan',
scoreLabel: 'Health score',
healthy: 'Stable',
watch: 'Watch',
critical: 'Critical',
lastCheck: 'Last checked',
};
};
export default function PlantDetailScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
const {
plants,
isDarkMode,
colorPalette,
language,
t,
deletePlant,
updatePlant,
billingSummary,
} = useApp();
const colors = useColors(isDarkMode, colorPalette);
const router = useRouter();
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [fullscreenImage, setFullscreenImage] = useState<string | null>(null);
const [healthStatus, setHealthStatus] = useState<'idle' | 'insufficient_credits'>('idle');
const [paywallVisible, setPaywallVisible] = useState(false);
const plant = plants.find((item) => item.id === id);
if (!plant) {
return (
<View
style={[
styles.container,
{
backgroundColor: colors.pageBase,
justifyContent: 'center',
alignItems: 'center',
},
]}
>
<Text style={{ color: colors.text }}>Plant not found</Text>
</View>
);
}
const localeMap: Record<string, string> = { de: 'de-DE', en: 'en-US', es: 'es-ES' };
const locale = localeMap[language] || 'de-DE';
const healthCopy = getHealthCopy(language);
const availableCredits = billingSummary?.credits.available ?? 0;
const formattedWateredDate = new Date(plant.lastWatered).toLocaleDateString(locale);
const lastWateredObj = new Date(plant.lastWatered);
const nextWateringDate = new Date(lastWateredObj);
nextWateringDate.setDate(lastWateredObj.getDate() + plant.careInfo.waterIntervalDays);
const formattedNextWatering = nextWateringDate.toLocaleDateString(locale, {
weekday: 'short',
day: 'numeric',
month: 'short',
});
const lastWateredText = t.lastWateredDate.replace('{0}', formattedWateredDate);
const isWateredToday = new Date(plant.lastWatered).toDateString() === new Date().toDateString();
const daysSinceWatered = Math.max(
0,
Math.floor((Date.now() - lastWateredObj.getTime()) / (1000 * 60 * 60 * 24))
);
const nextWaterLabel = language === 'de'
? 'Nächstes Gießen'
: language === 'es'
? 'Proximo riego'
: 'Next water';
const wateredAgoText = language === 'de'
? `Zuletzt vor ${daysSinceWatered} ${daysSinceWatered === 1 ? 'Tag' : 'Tagen'}`
: language === 'es'
? `Ultimo riego hace ${daysSinceWatered} ${daysSinceWatered === 1 ? 'dia' : 'dias'}`
: `Last watered ${daysSinceWatered} ${daysSinceWatered === 1 ? 'day' : 'days'} ago`;
const textOnPage = getReadableTextColor(colors.pageBase, colors.primaryDark, colors.textOnImage);
const textOnSurface = getReadableTextColor(colors.surface, colors.primaryDark, colors.textOnImage);
const textOnHeroButton = getReadableTextColor(colors.heroButton, colors.primaryDark, colors.textOnImage);
const textOnPrimaryAction = getReadableTextColor(colors.primaryDark, '#ffffff', '#111111');
const textOnAiBadge = getReadableTextColor(colors.primarySoft, colors.primaryDark, colors.textOnImage);
const latestHealthCheck = useMemo(() => {
if (!plant.healthChecks || plant.healthChecks.length === 0) return null;
return plant.healthChecks[0];
}, [plant.healthChecks]);
const healthStatusInlineText = (() => {
if (healthStatus === 'insufficient_credits') return healthCopy.insufficientInline;
if (healthStatus === 'idle' && availableCredits < HEALTH_CHECK_CREDIT_COST) return healthCopy.insufficientInline;
return '';
})();
const healthStateColor = (() => {
if (healthStatus === 'insufficient_credits') return colors.warning;
return colors.textMuted;
})();
const latestStatusText = latestHealthCheck
? (
latestHealthCheck.status === 'healthy'
? healthCopy.healthy
: latestHealthCheck.status === 'watch'
? healthCopy.watch
: healthCopy.critical
)
: '';
const latestStatusBg = latestHealthCheck
? (
latestHealthCheck.status === 'healthy'
? colors.successSoft
: latestHealthCheck.status === 'watch'
? colors.warningSoft
: colors.dangerSoft
)
: colors.surfaceMuted;
const latestStatusColor = latestHealthCheck
? (
latestHealthCheck.status === 'healthy'
? colors.success
: latestHealthCheck.status === 'watch'
? colors.warning
: colors.danger
)
: colors.textMuted;
const latestAnalysisSummary = latestHealthCheck
? latestHealthCheck.analysisSummary || healthCopy.analysisFallback
: '';
const timelineEntries = useMemo(() => {
const history = plant.wateringHistory && plant.wateringHistory.length > 0
? plant.wateringHistory
: [plant.lastWatered];
return history.slice(0, 5);
}, [plant.lastWatered, plant.wateringHistory]);
const hasEnoughHealthCredits = availableCredits >= HEALTH_CHECK_CREDIT_COST;
const canRunHealthCheck = true;
const handleWater = async () => {
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
const now = new Date().toISOString();
const currentHistory = plant.wateringHistory || [];
const newHistory = [now, ...currentHistory].slice(0, 10);
updatePlant({ ...plant, lastWatered: now, wateringHistory: newHistory });
};
const handleShare = async () => {
try {
await Share.share({
message: `Check out my plant: ${plant.name} (${plant.botanicalName}) - identified with GreenLens!`,
});
} catch (error: any) {
Alert.alert('Error', error.message);
}
};
const handleDelete = () => {
deletePlant(plant.id);
router.back();
};
const handleAddPhoto = async () => {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ['images'],
quality: 0.8,
});
if (!result.canceled && result.assets[0]) {
const currentGallery = plant.gallery || [];
updatePlant({ ...plant, gallery: [...currentGallery, result.assets[0].uri] });
}
};
const handleHealthCheck = async () => {
if (availableCredits < HEALTH_CHECK_CREDIT_COST) {
setPaywallVisible(true);
return;
}
if (healthStatus !== 'idle') {
setHealthStatus('idle');
}
router.push({
pathname: '/scanner',
params: { mode: 'health', plantId: plant.id },
});
};
return (
<View style={[styles.container, { backgroundColor: colors.pageBase }]}>
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={styles.scrollContent}>
<View style={styles.hero}>
<SafeImage uri={plant.imageUri} style={styles.heroImage} />
<View style={[styles.heroShade, { backgroundColor: colors.overlay }]} />
<View style={[styles.heroBaseFade, { backgroundColor: colors.pageBase }]} />
<View style={styles.topActions}>
<TouchableOpacity
style={[
styles.floatingAction,
{
backgroundColor: colors.heroButton,
borderColor: colors.heroButtonBorder,
},
]}
onPress={() => router.back()}
>
<Ionicons name="arrow-back" size={21} color={textOnHeroButton} />
</TouchableOpacity>
<TouchableOpacity
style={[
styles.floatingAction,
{
backgroundColor: colors.heroButton,
borderColor: colors.heroButtonBorder,
},
]}
onPress={handleShare}
>
<Ionicons name="ellipsis-horizontal" size={21} color={textOnHeroButton} />
</TouchableOpacity>
</View>
<View style={styles.heroTitleWrap}>
<Text style={[styles.heroName, { color: textOnPage }]}>{plant.name}</Text>
<Text style={[styles.heroBotanical, { color: colors.primaryDark }]}>{plant.botanicalName}</Text>
</View>
</View>
<View style={styles.content}>
<View style={[styles.waterCard, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<View style={styles.waterInfo}>
<View style={styles.waterHeadline}>
<Ionicons name="water-outline" size={15} color={colors.info} />
<Text style={[styles.waterLabel, { color: colors.info }]}>{nextWaterLabel}</Text>
</View>
<Text style={[styles.waterValue, { color: textOnSurface }]} numberOfLines={1}>
{formattedNextWatering}
</Text>
<Text style={[styles.waterMeta, { color: colors.textMuted }]}>
{isWateredToday ? lastWateredText : wateredAgoText}
</Text>
</View>
<TouchableOpacity
style={[
styles.waterButton,
{
backgroundColor: isWateredToday ? colors.success : colors.primaryDark,
opacity: isWateredToday ? 0.9 : 1,
},
]}
disabled={isWateredToday}
onPress={handleWater}
>
<Ionicons name={isWateredToday ? 'checkmark' : 'water'} size={14} color={textOnPrimaryAction} />
<Text style={[styles.waterButtonText, { color: textOnPrimaryAction }]}>
{isWateredToday ? t.watered : t.waterNow}
</Text>
</TouchableOpacity>
</View>
<View style={styles.careGrid}>
{[
{
icon: 'water' as const,
label: t.water,
value: `${plant.careInfo.waterIntervalDays} ${t.days}`,
iconColor: colors.info,
iconBg: colors.infoSoft,
},
{
icon: 'sunny' as const,
label: t.light,
value: plant.careInfo.light,
iconColor: colors.warning,
iconBg: colors.warningSoft,
},
{
icon: 'thermometer' as const,
label: t.temp,
value: plant.careInfo.temp,
iconColor: colors.success,
iconBg: colors.successSoft,
},
].map((item) => (
<View key={item.label} style={[styles.careCard, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<View style={[styles.careIconWrap, { backgroundColor: item.iconBg }]}>
<Ionicons name={item.icon} size={18} color={item.iconColor} />
</View>
<Text style={[styles.careLabel, { color: colors.textMuted }]}>{item.label}</Text>
<Text style={[styles.careValue, { color: textOnSurface }]}>
{item.value}
</Text>
</View>
))}
</View>
<View style={[styles.reminderCard, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<View style={styles.reminderInfoRow}>
<View
style={[
styles.reminderIconWrap,
{
backgroundColor: plant.notificationsEnabled ? colors.warningSoft : colors.surfaceMuted,
},
]}
>
<Ionicons
name={plant.notificationsEnabled ? 'notifications' : 'notifications-off'}
size={18}
color={plant.notificationsEnabled ? colors.warning : colors.textMuted}
/>
</View>
<View>
<Text style={[styles.reminderTitle, { color: textOnSurface }]}>Smart Reminders</Text>
<Text style={[styles.reminderSubtitle, { color: colors.textMuted }]}>
{t.reminderDesc}
</Text>
</View>
</View>
<TouchableOpacity
style={[
styles.toggleTrack,
{
backgroundColor: plant.notificationsEnabled ? colors.primaryDark : colors.borderStrong,
},
]}
onPress={async () => {
if (!plant.notificationsEnabled) {
const granted = await requestPermissions();
if (!granted) {
Alert.alert(t.reminder, t.reminderPermissionNeeded);
return;
}
await scheduleWateringReminder(plant);
} else {
await cancelReminder(plant.id);
}
updatePlant({ ...plant, notificationsEnabled: !plant.notificationsEnabled });
}}
>
<View
style={[
styles.toggleThumb,
{
left: plant.notificationsEnabled ? 29 : 3,
},
]}
/>
</TouchableOpacity>
</View>
<View>
<View style={styles.sectionHeaderRow}>
<Text style={[styles.sectionTitle, { color: textOnPage }]}>{t.wateringHistory}</Text>
<Ionicons name="time-outline" size={16} color={colors.textMuted} />
</View>
<View style={[styles.timelineCard, { backgroundColor: colors.surface, borderColor: colors.border }]}>
{timelineEntries.length === 0 ? (
<View style={styles.historyEmpty}>
<Ionicons name="time-outline" size={24} color={colors.textMuted} />
<Text style={[styles.historyEmptyText, { color: colors.textMuted }]}>{t.noHistory}</Text>
</View>
) : (
timelineEntries.map((dateStr, index) => (
<View key={`${dateStr}-${index}`} style={styles.timelineRow}>
<View style={styles.timelineMarkerColumn}>
<View
style={[
styles.timelineDot,
{
backgroundColor: index === 0 ? colors.success : colors.surface,
borderColor: index === 0 ? colors.success : colors.borderStrong,
},
]}
/>
{index < timelineEntries.length - 1 && (
<View style={[styles.timelineLine, { backgroundColor: colors.border }]} />
)}
</View>
<View style={styles.timelineContent}>
<View style={styles.timelineTopRow}>
<Text style={[styles.timelineDate, { color: textOnSurface }]}>
{new Date(dateStr).toLocaleDateString(locale, {
day: '2-digit',
month: 'short',
year: 'numeric',
})}
</Text>
<Text style={[styles.timelineType, { color: colors.textMuted }]}>
{index === 0 ? t.watered : t.waterNow}
</Text>
</View>
<Text style={[styles.timelineTime, { color: colors.textMuted }]}>
{new Date(dateStr).toLocaleTimeString(locale, {
hour: '2-digit',
minute: '2-digit',
})}
</Text>
</View>
</View>
))
)}
</View>
</View>
<View>
<View style={styles.galleryHeader}>
<Text style={[styles.sectionTitle, { color: textOnPage }]}>{t.galleryTitle}</Text>
<TouchableOpacity
style={[styles.addPhotoBtn, { backgroundColor: colors.surface, borderColor: colors.border }]}
onPress={handleAddPhoto}
>
<Ionicons name="add" size={16} color={textOnSurface} />
<Text style={[styles.addPhotoBtnText, { color: textOnSurface }]}>{t.addPhoto}</Text>
</TouchableOpacity>
</View>
{(!plant.gallery || plant.gallery.length === 0) ? (
<View style={[styles.galleryEmpty, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<Ionicons name="images-outline" size={24} color={colors.textMuted} />
<Text style={{ color: colors.textMuted, fontSize: 13 }}>{t.noPhotos}</Text>
</View>
) : (
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={styles.galleryScroll}
contentContainerStyle={styles.galleryContent}
>
{plant.gallery.map((uri, index) => (
<TouchableOpacity key={`${uri}-${index}`} onPress={() => setFullscreenImage(uri)}>
<SafeImage uri={uri} style={styles.galleryThumb} />
</TouchableOpacity>
))}
</ScrollView>
)}
</View>
<View style={styles.summarySection}>
<View style={styles.summaryHeader}>
<Text style={[styles.sectionTitle, { color: textOnPage }]}>{t.aboutPlant}</Text>
<View style={[styles.aiBadge, { backgroundColor: colors.primarySoft }]}>
<Text style={[styles.aiBadgeText, { color: textOnAiBadge }]}>AI</Text>
</View>
</View>
<View style={[styles.summaryCard, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<Text style={[styles.summaryText, { color: colors.textSecondary }]}>
{plant.description || t.noDescription}
</Text>
</View>
<View style={[styles.healthActionCard, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<View style={styles.healthActionRow}>
<View style={styles.healthActionInfo}>
<Text style={[styles.healthActionTitle, { color: textOnSurface }]}>{healthCopy.title}</Text>
<Text style={[styles.healthActionMeta, { color: colors.textMuted }]}>{healthCopy.intro}</Text>
</View>
<TouchableOpacity
style={[
styles.healthActionBtn,
{
backgroundColor: hasEnoughHealthCredits ? colors.primary : colors.surfaceMuted,
borderColor: hasEnoughHealthCredits ? colors.primaryDark : colors.borderStrong,
},
]}
disabled={!canRunHealthCheck}
onPress={handleHealthCheck}
activeOpacity={0.85}
>
<Ionicons name="sparkles" size={14} color={hasEnoughHealthCredits ? colors.onPrimary : colors.textMuted} />
<Text style={[styles.healthActionBtnText, { color: hasEnoughHealthCredits ? colors.onPrimary : colors.textMuted }]}>
{healthCopy.action}
</Text>
</TouchableOpacity>
</View>
<View style={styles.healthMetaRow}>
<Text style={[styles.healthMetaText, { color: colors.textSecondary }]}>
{healthCopy.creditsLabel}: {availableCredits}
</Text>
<Text style={[styles.healthMetaText, { color: colors.textMuted }]}>
{healthCopy.cost}
</Text>
</View>
{healthStatusInlineText ? (
<View style={styles.healthInlineWrap}>
<Text style={[styles.healthInlineText, { color: healthStateColor }]}>
{healthStatusInlineText}
</Text>
{healthStatus === 'insufficient_credits' || (healthStatus === 'idle' && !hasEnoughHealthCredits) ? (
<TouchableOpacity
style={[styles.healthPlanButton, { borderColor: colors.borderStrong, backgroundColor: colors.surfaceStrong }]}
onPress={() => router.push('/(tabs)/profile')}
>
<Text style={[styles.healthPlanButtonText, { color: textOnSurface }]}>{healthCopy.managePlan}</Text>
</TouchableOpacity>
) : null}
</View>
) : null}
</View>
{latestHealthCheck ? (
<View style={[styles.healthResultCard, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<View style={styles.healthResultHead}>
<View>
<Text style={[styles.healthResultTitle, { color: textOnSurface }]}>{healthCopy.scoreLabel}</Text>
<Text style={[styles.healthResultScore, { color: textOnSurface }]}>
{latestHealthCheck.overallHealthScore}/100
</Text>
</View>
<View style={[styles.healthStatusBadge, { backgroundColor: latestStatusBg }]}>
<Text style={[styles.healthStatusBadgeText, { color: latestStatusColor }]}>{latestStatusText}</Text>
</View>
</View>
<Text style={[styles.healthTimestamp, { color: colors.textMuted }]}>
{healthCopy.lastCheck}: {new Date(latestHealthCheck.generatedAt).toLocaleString(locale)}
</Text>
{latestAnalysisSummary ? (
<View style={[styles.healthAnalysisBox, { backgroundColor: colors.surfaceMuted, borderColor: colors.border }]}>
<Text style={[styles.healthListTitle, { color: textOnSurface }]}>{healthCopy.analysisTitle}</Text>
<Text style={[styles.healthAnalysisText, { color: colors.textSecondary }]}>
{latestAnalysisSummary}
</Text>
</View>
) : null}
<View style={styles.healthListBlock}>
<Text style={[styles.healthListTitle, { color: textOnSurface }]}>{healthCopy.issuesTitle}</Text>
{latestHealthCheck.likelyIssues.map((issue, index) => (
<View key={`${issue.title}-${index}`} style={styles.healthIssueWrap}>
<View style={styles.healthIssueHead}>
<Text style={[styles.healthIssueTitle, { color: textOnSurface }]}>{issue.title}</Text>
<Text style={[styles.healthIssueConfidence, { color: colors.textMuted }]}>
{Math.round(issue.confidence * 100)}%
</Text>
</View>
<Text style={[styles.healthIssueDetails, { color: colors.textSecondary }]}>
{issue.details}
</Text>
</View>
))}
</View>
<View style={styles.healthListBlock}>
<Text style={[styles.healthListTitle, { color: textOnSurface }]}>{healthCopy.actionsTitle}</Text>
{latestHealthCheck.actionsNow.map((item, index) => (
<View key={`${item}-${index}`} style={styles.healthBulletRow}>
<View style={[styles.healthBulletDot, { backgroundColor: colors.warning }]} />
<Text style={[styles.healthBulletText, { color: colors.textSecondary }]}>{item}</Text>
</View>
))}
</View>
<View style={styles.healthListBlock}>
<Text style={[styles.healthListTitle, { color: textOnSurface }]}>{healthCopy.planTitle}</Text>
{latestHealthCheck.plan7Days.map((item, index) => (
<View key={`${item}-${index}`} style={styles.healthBulletRow}>
<View style={[styles.healthBulletDot, { backgroundColor: colors.info }]} />
<Text style={[styles.healthBulletText, { color: colors.textSecondary }]}>{item}</Text>
</View>
))}
</View>
</View>
) : null}
</View>
<TouchableOpacity style={styles.deleteAction} onPress={() => setShowDeleteConfirm(true)}>
<Ionicons name="trash-outline" size={16} color={colors.danger} />
<Text style={[styles.deleteActionText, { color: colors.danger }]}>{t.delete}</Text>
</TouchableOpacity>
</View>
</ScrollView>
<Modal visible={showDeleteConfirm} transparent animationType="fade">
<View style={[styles.modalOverlay, { backgroundColor: colors.overlayStrong }]}>
<View style={[styles.modalCard, { backgroundColor: colors.surface }]}>
<View style={[styles.modalIcon, { backgroundColor: colors.dangerSoft }]}>
<Ionicons name="alert-circle" size={24} color={colors.danger} />
</View>
<Text style={[styles.modalTitle, { color: colors.text }]}>{t.deleteConfirmTitle}</Text>
<Text style={[styles.modalMessage, { color: colors.textSecondary }]}>{t.deleteConfirmMessage}</Text>
<View style={styles.modalActions}>
<TouchableOpacity
style={[styles.modalButton, { backgroundColor: colors.surfaceMuted }]}
onPress={() => setShowDeleteConfirm(false)}
>
<Text style={[styles.modalButtonText, { color: colors.textSecondary }]}>{t.cancel}</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.modalButton, { backgroundColor: colors.danger }]}
onPress={handleDelete}
>
<Text style={[styles.modalButtonText, { color: colors.iconOnImage }]}>{t.confirm}</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
<Modal visible={!!fullscreenImage} transparent animationType="fade">
<View style={styles.fullscreenOverlay}>
{fullscreenImage && <SafeImage uri={fullscreenImage} style={styles.fullscreenImage} />}
<TouchableOpacity style={styles.fullscreenClose} onPress={() => setFullscreenImage(null)}>
<Ionicons name="close" size={28} color="#fff" />
</TouchableOpacity>
</View>
</Modal>
{/* Health Check Paywall Modal */}
<Modal visible={paywallVisible} transparent animationType="slide" onRequestClose={() => setPaywallVisible(false)}>
<View style={styles.paywallOverlay}>
<View style={[styles.paywallContent, { backgroundColor: colors.cardBg, borderColor: colors.border }]}>
<View style={styles.paywallHeader}>
<View style={[styles.paywallIconWrap, { backgroundColor: colors.primarySoft }]}>
<Ionicons name="sparkles" size={32} color={colors.primary} />
</View>
<TouchableOpacity onPress={() => setPaywallVisible(false)} style={styles.paywallClose}>
<Ionicons name="close" size={24} color={colors.textMuted} />
</TouchableOpacity>
</View>
<Text style={[styles.paywallTitle, { color: colors.text }]}>KI-Pflanzendoktor freischalten</Text>
<Text style={[styles.paywallSubtitle, { color: colors.textSecondary }]}>
Nutze fortschrittliche KI, um Probleme zu erkennen, bevor sie deine Pflanze gefährden.
</Text>
<View style={styles.paywallFeatures}>
{[
{ icon: 'search', title: 'Präzise Diagnose', desc: 'Erkennt Schädlinge, Krankheiten und Nährstoffmangel.' },
{ icon: 'medkit', title: 'Rettungspläne', desc: 'Schritt-für-Schritt Anleitungen zur Genesung.' },
{ icon: 'infinite', title: 'Mehr Scans', desc: 'Erhalte 120 Credits jeden Monat mit GreenLens Pro.' },
].map((feat, i) => (
<View key={i} style={styles.paywallFeatureRow}>
<View style={[styles.paywallFeatureIcon, { backgroundColor: colors.surfaceMuted }]}>
<Ionicons name={feat.icon as any} size={18} color={colors.primary} />
</View>
<View style={{ flex: 1 }}>
<Text style={[styles.paywallFeatureTitle, { color: colors.text }]}>{feat.title}</Text>
<Text style={[styles.paywallFeatureDesc, { color: colors.textMuted }]}>{feat.desc}</Text>
</View>
</View>
))}
</View>
<TouchableOpacity
style={[styles.paywallPrimaryBtn, { backgroundColor: colors.primary }]}
onPress={() => {
setPaywallVisible(false);
router.push('/profile/billing');
}}
>
<Text style={[styles.paywallPrimaryBtnText, { color: colors.onPrimary }]}>Jetzt upgraden</Text>
<Ionicons name="arrow-forward" size={18} color={colors.onPrimary} />
</TouchableOpacity>
<TouchableOpacity
style={styles.paywallSecondaryBtn}
onPress={() => setPaywallVisible(false)}
>
<Text style={[styles.paywallSecondaryBtnText, { color: colors.textMuted }]}>Vielleicht später</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
scrollContent: { paddingBottom: 120 },
hero: {
height: 450,
position: 'relative',
},
heroImage: {
width: '100%',
height: '100%',
resizeMode: 'cover',
},
heroShade: {
...StyleSheet.absoluteFillObject,
opacity: 0.18,
},
heroBaseFade: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
height: 172,
opacity: 0.95,
borderTopLeftRadius: 28,
borderTopRightRadius: 28,
},
topActions: {
position: 'absolute',
top: 58,
left: 22,
right: 22,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
floatingAction: {
width: 44,
height: 44,
borderRadius: 14,
borderWidth: 1,
justifyContent: 'center',
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.15,
shadowRadius: 10,
elevation: 5,
},
heroTitleWrap: {
position: 'absolute',
left: 24,
right: 24,
bottom: 74,
},
heroName: {
fontSize: 36,
lineHeight: 40,
fontWeight: '700',
marginBottom: 6,
},
heroBotanical: {
fontSize: 16,
fontStyle: 'italic',
fontWeight: '600',
opacity: 0.72,
},
content: {
marginTop: -46,
paddingHorizontal: 16,
gap: 20,
},
waterCard: {
borderRadius: 28,
borderWidth: 1,
paddingHorizontal: 16,
paddingVertical: 14,
flexDirection: 'row',
alignItems: 'center',
gap: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.08,
shadowRadius: 16,
elevation: 4,
},
waterInfo: {
flex: 1,
gap: 2,
},
waterHeadline: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
},
waterLabel: {
fontSize: 11,
textTransform: 'uppercase',
letterSpacing: 0.8,
fontWeight: '700',
},
waterValue: {
fontSize: 15,
lineHeight: 19,
fontWeight: '700',
},
waterMeta: {
fontSize: 12,
fontWeight: '500',
},
waterButton: {
borderRadius: 16,
paddingHorizontal: 20,
paddingVertical: 13,
flexDirection: 'row',
alignItems: 'center',
gap: 6,
},
waterButtonText: {
fontSize: 16,
fontWeight: '700',
},
careGrid: {
flexDirection: 'row',
gap: 10,
},
careCard: {
flex: 1,
borderRadius: 22,
borderWidth: 1,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 8,
paddingVertical: 16,
gap: 6,
},
careIconWrap: {
width: 34,
height: 34,
borderRadius: 17,
alignItems: 'center',
justifyContent: 'center',
},
careLabel: {
fontSize: 10,
fontWeight: '700',
textTransform: 'uppercase',
letterSpacing: 0.5,
},
careValue: {
width: '100%',
fontSize: 12,
lineHeight: 15,
fontWeight: '700',
textAlign: 'center',
flexShrink: 1,
},
reminderCard: {
borderRadius: 24,
borderWidth: 1,
paddingHorizontal: 14,
paddingVertical: 12,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
gap: 12,
},
reminderInfoRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
flex: 1,
},
reminderIconWrap: {
width: 40,
height: 40,
borderRadius: 13,
justifyContent: 'center',
alignItems: 'center',
},
reminderTitle: {
fontSize: 14,
fontWeight: '700',
},
reminderSubtitle: {
fontSize: 10,
marginTop: 1,
},
toggleTrack: {
width: 54,
height: 30,
borderRadius: 999,
justifyContent: 'center',
position: 'relative',
},
toggleThumb: {
position: 'absolute',
width: 22,
height: 22,
borderRadius: 11,
backgroundColor: '#ffffff',
},
summarySection: {
gap: 8,
},
summaryHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
aiBadge: {
borderRadius: 999,
paddingHorizontal: 10,
paddingVertical: 4,
},
aiBadgeText: {
fontSize: 10,
fontWeight: '700',
letterSpacing: 0.6,
},
summaryCard: {
borderRadius: 22,
borderWidth: 1,
padding: 16,
},
summaryText: {
fontSize: 14,
lineHeight: 22,
},
healthActionCard: {
borderRadius: 22,
borderWidth: 1,
padding: 14,
gap: 10,
},
healthActionRow: {
flexDirection: 'row',
gap: 10,
alignItems: 'center',
},
healthActionInfo: {
flex: 1,
},
healthActionTitle: {
fontSize: 14,
fontWeight: '700',
},
healthActionMeta: {
fontSize: 11,
marginTop: 2,
},
healthActionBtn: {
borderRadius: 14,
borderWidth: 1,
paddingHorizontal: 12,
paddingVertical: 10,
flexDirection: 'row',
alignItems: 'center',
gap: 6,
},
healthActionBtnText: {
fontSize: 12,
fontWeight: '700',
},
healthMetaRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
healthMetaText: {
fontSize: 12,
fontWeight: '500',
},
healthInlineWrap: {
gap: 8,
},
healthInlineText: {
fontSize: 12,
fontWeight: '600',
},
healthPlanButton: {
alignSelf: 'flex-start',
borderWidth: 1,
borderRadius: 12,
paddingHorizontal: 10,
paddingVertical: 7,
},
healthPlanButtonText: {
fontSize: 12,
fontWeight: '700',
},
healthResultCard: {
borderRadius: 22,
borderWidth: 1,
padding: 14,
gap: 12,
},
healthResultHead: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
healthResultTitle: {
fontSize: 11,
textTransform: 'uppercase',
letterSpacing: 0.5,
fontWeight: '700',
},
healthResultScore: {
fontSize: 26,
lineHeight: 30,
fontWeight: '800',
marginTop: 2,
},
healthStatusBadge: {
borderRadius: 999,
paddingHorizontal: 10,
paddingVertical: 5,
},
healthStatusBadgeText: {
fontSize: 11,
fontWeight: '700',
},
healthTimestamp: {
fontSize: 11,
},
healthListBlock: {
gap: 8,
},
healthAnalysisBox: {
borderRadius: 16,
borderWidth: 1,
padding: 12,
gap: 6,
},
healthAnalysisText: {
fontSize: 12,
lineHeight: 19,
},
healthListTitle: {
fontSize: 13,
fontWeight: '700',
},
healthIssueWrap: {
gap: 3,
},
healthIssueHead: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
gap: 8,
},
healthIssueTitle: {
fontSize: 13,
fontWeight: '600',
flex: 1,
},
healthIssueConfidence: {
fontSize: 11,
fontWeight: '700',
},
healthIssueDetails: {
fontSize: 12,
lineHeight: 18,
},
healthBulletRow: {
flexDirection: 'row',
alignItems: 'flex-start',
gap: 8,
},
healthBulletDot: {
width: 7,
height: 7,
borderRadius: 4,
marginTop: 5,
},
healthBulletText: {
flex: 1,
fontSize: 12,
lineHeight: 18,
},
sectionHeaderRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 10,
marginTop: 4,
},
sectionTitle: {
fontSize: 18,
fontWeight: '700',
},
timelineCard: {
borderRadius: 22,
borderWidth: 1,
paddingHorizontal: 14,
paddingTop: 16,
paddingBottom: 10,
gap: 4,
},
historyEmpty: {
width: '100%',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
minHeight: 96,
paddingVertical: 20,
},
historyEmptyText: {
fontSize: 13,
textAlign: 'center',
},
timelineRow: {
flexDirection: 'row',
alignItems: 'flex-start',
gap: 10,
},
timelineMarkerColumn: {
width: 20,
alignItems: 'center',
},
timelineDot: {
width: 11,
height: 11,
borderRadius: 6,
borderWidth: 2,
},
timelineLine: {
width: 2,
flex: 1,
marginTop: 3,
minHeight: 26,
borderRadius: 2,
},
timelineContent: {
flex: 1,
paddingBottom: 14,
},
timelineTopRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
gap: 10,
},
timelineDate: {
fontSize: 14,
fontWeight: '700',
},
timelineType: {
fontSize: 10,
textTransform: 'uppercase',
letterSpacing: 0.45,
fontWeight: '600',
},
timelineTime: {
fontSize: 12,
marginTop: 3,
},
galleryHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
addPhotoBtn: {
flexDirection: 'row',
alignItems: 'center',
gap: 5,
borderWidth: 1,
borderRadius: 18,
paddingHorizontal: 10,
paddingVertical: 6,
},
addPhotoBtnText: {
fontSize: 13,
fontWeight: '600',
},
galleryEmpty: {
borderRadius: 18,
borderWidth: 1,
paddingVertical: 22,
alignItems: 'center',
justifyContent: 'center',
gap: 8,
},
galleryScroll: {
marginBottom: 2,
},
galleryContent: {
gap: 12,
paddingRight: 20,
},
galleryThumb: {
width: 96,
height: 96,
borderRadius: 18,
},
deleteAction: {
alignSelf: 'center',
marginTop: 4,
marginBottom: 12,
flexDirection: 'row',
alignItems: 'center',
gap: 6,
paddingHorizontal: 8,
paddingVertical: 6,
},
deleteActionText: {
fontSize: 13,
fontWeight: '600',
},
modalOverlay: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 24,
},
modalCard: {
width: '100%',
maxWidth: 340,
borderRadius: 24,
padding: 22,
},
modalIcon: {
width: 48,
height: 48,
borderRadius: 24,
justifyContent: 'center',
alignItems: 'center',
alignSelf: 'center',
marginBottom: 14,
},
modalTitle: {
fontSize: 18,
fontWeight: '700',
textAlign: 'center',
marginBottom: 8,
},
modalMessage: {
fontSize: 13,
lineHeight: 20,
textAlign: 'center',
marginBottom: 20,
},
modalActions: {
flexDirection: 'row',
gap: 10,
},
modalButton: {
flex: 1,
paddingVertical: 14,
borderRadius: 14,
alignItems: 'center',
},
modalButtonText: {
fontSize: 13,
fontWeight: '700',
},
fullscreenOverlay: {
flex: 1,
backgroundColor: '#000000ee',
justifyContent: 'center',
alignItems: 'center',
},
fullscreenImage: {
width: '90%',
height: '72%',
resizeMode: 'contain',
},
fullscreenClose: {
position: 'absolute',
top: 56,
right: 20,
backgroundColor: '#00000088',
borderRadius: 20,
padding: 8,
},
paywallOverlay: {
flex: 1,
backgroundColor: '#000000aa',
justifyContent: 'flex-end',
},
paywallContent: {
borderTopLeftRadius: 32,
borderTopRightRadius: 32,
padding: 24,
paddingBottom: 48,
borderTopWidth: 1,
},
paywallHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: 20,
},
paywallIconWrap: {
width: 64,
height: 64,
borderRadius: 20,
justifyContent: 'center',
alignItems: 'center',
},
paywallClose: {
padding: 4,
},
paywallTitle: {
fontSize: 24,
fontWeight: '800',
marginBottom: 8,
letterSpacing: 0.2,
},
paywallSubtitle: {
fontSize: 15,
lineHeight: 22,
marginBottom: 28,
},
paywallFeatures: {
gap: 18,
marginBottom: 32,
},
paywallFeatureRow: {
flexDirection: 'row',
gap: 14,
alignItems: 'center',
},
paywallFeatureIcon: {
width: 38,
height: 38,
borderRadius: 12,
justifyContent: 'center',
alignItems: 'center',
},
paywallFeatureTitle: {
fontSize: 15,
fontWeight: '700',
marginBottom: 2,
},
paywallFeatureDesc: {
fontSize: 13,
lineHeight: 18,
},
paywallPrimaryBtn: {
borderRadius: 18,
paddingVertical: 16,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
shadowOpacity: 0.2,
shadowRadius: 10,
shadowOffset: { width: 0, height: 4 },
elevation: 4,
},
paywallPrimaryBtnText: {
fontSize: 17,
fontWeight: '700',
},
paywallSecondaryBtn: {
marginTop: 12,
paddingVertical: 12,
alignItems: 'center',
},
paywallSecondaryBtnText: {
fontSize: 14,
fontWeight: '600',
},
});