1444 lines
44 KiB
TypeScript
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',
|
|
},
|
|
});
|