Greenlens/app/plant/[id].tsx

1444 lines
44 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: 'Neues Foto + Health-Check',
running: 'Neues Foto wird analysiert...',
cost: `Kosten: ${HEALTH_CHECK_CREDIT_COST} Credits`,
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.',
issuesTitle: 'Moegliche 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: 'Foto nuevo + Health-check',
running: 'Analizando foto nueva...',
cost: `Costo: ${HEALTH_CHECK_CREDIT_COST} creditos`,
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.',
issuesTitle: 'Posibles causas',
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: 'New Photo + Health Check',
running: 'Analyzing new photo...',
cost: `Cost: ${HEALTH_CHECK_CREDIT_COST} credits`,
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.',
issuesTitle: 'Likely issues',
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 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.cost}</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>
<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,
},
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',
},
});