Greenlens/app/(tabs)/profile.tsx

527 lines
15 KiB
TypeScript

import React, { useState, useEffect, useMemo } from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
ScrollView,
Image,
Alert,
TextInput,
Keyboard,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Ionicons } from '@expo/vector-icons';
import * as ImagePicker from 'expo-image-picker';
import { useRouter } from 'expo-router';
import { useApp } from '../../context/AppContext';
import { useColors } from '../../constants/Colors';
import { ThemeBackdrop } from '../../components/ThemeBackdrop';
import { Language } from '../../types';
const DAY_MS = 24 * 60 * 60 * 1000;
const getDaysUntilWatering = (lastWatered: string, intervalDays: number): number => {
const lastWateredTs = new Date(lastWatered).getTime();
if (Number.isNaN(lastWateredTs)) return 0;
const dueTs = lastWateredTs + (intervalDays * DAY_MS);
const remainingMs = dueTs - Date.now();
if (remainingMs <= 0) return 0;
return Math.ceil(remainingMs / DAY_MS);
};
const getProfileCopy = (language: Language) => {
if (language === 'de') {
return {
overviewLabel: 'Überblick',
statPlants: 'Pflanzen',
statDueToday: 'Heute fällig',
statReminders: 'Erinnerungen an',
account: 'Account',
changePhoto: 'Foto ändern',
removePhoto: 'Foto entfernen',
nameLabel: 'Name',
namePlaceholder: 'Dein Name',
saveName: 'Name speichern',
photoErrorTitle: 'Fehler',
photoErrorMessage: 'Profilfoto konnte nicht geladen werden.',
nameErrorTitle: 'Name fehlt',
nameErrorMessage: 'Bitte gib einen Namen ein.',
menuSettings: 'Einstellungen',
menuBilling: 'Abo & Credits',
menuData: 'Daten & Datenschutz',
logout: 'Abmelden',
logoutConfirmTitle: 'Abmelden?',
logoutConfirmMessage: 'Möchtest du dich wirklich abmelden?',
logoutConfirmBtn: 'Abmelden',
};
}
if (language === 'es') {
return {
overviewLabel: 'Resumen',
statPlants: 'Plantas',
statDueToday: 'Vencen hoy',
statReminders: 'Recordatorios',
account: 'Cuenta',
changePhoto: 'Cambiar foto',
removePhoto: 'Eliminar foto',
nameLabel: 'Nombre',
namePlaceholder: 'Tu nombre',
saveName: 'Guardar nombre',
photoErrorTitle: 'Error',
photoErrorMessage: 'No se pudo cargar la foto.',
nameErrorTitle: 'Falta nombre',
nameErrorMessage: 'Por favor ingresa un nombre.',
menuSettings: 'Ajustes',
menuBilling: 'Suscripción y Créditos',
menuData: 'Datos y Privacidad',
logout: 'Cerrar sesión',
logoutConfirmTitle: '¿Cerrar sesión?',
logoutConfirmMessage: '¿Realmente quieres cerrar sesión?',
logoutConfirmBtn: 'Cerrar sesión',
};
}
return {
overviewLabel: 'Overview',
statPlants: 'Plants',
statDueToday: 'Due today',
statReminders: 'Reminders on',
account: 'Account',
changePhoto: 'Change photo',
removePhoto: 'Remove photo',
nameLabel: 'Name',
namePlaceholder: 'Your name',
saveName: 'Save name',
photoErrorTitle: 'Error',
photoErrorMessage: 'Could not load profile photo.',
nameErrorTitle: 'Name missing',
nameErrorMessage: 'Please enter a name.',
menuSettings: 'Preferences',
menuBilling: 'Billing & Credits',
menuData: 'Data & Privacy',
logout: 'Sign Out',
logoutConfirmTitle: 'Sign out?',
logoutConfirmMessage: 'Do you really want to sign out?',
logoutConfirmBtn: 'Sign Out',
};
};
export default function ProfileScreen() {
const {
plants,
language,
t,
isDarkMode,
colorPalette,
profileImageUri,
setProfileImage,
profileName,
setProfileName,
signOut,
} = useApp();
const router = useRouter();
const colors = useColors(isDarkMode, colorPalette);
const [isUpdatingImage, setIsUpdatingImage] = useState(false);
const [isSavingName, setIsSavingName] = useState(false);
const [draftName, setDraftName] = useState(profileName);
const copy = useMemo(() => getProfileCopy(language), [language]);
useEffect(() => {
setDraftName(profileName);
}, [profileName]);
const normalizedDraftName = draftName.trim();
const canSaveName = normalizedDraftName.length > 0 && normalizedDraftName !== profileName;
const dueTodayCount = useMemo(
() => plants.filter(plant => getDaysUntilWatering(plant.lastWatered, plant.careInfo.waterIntervalDays) === 0).length,
[plants]
);
const remindersEnabledCount = useMemo(
() => plants.filter(plant => Boolean(plant.notificationsEnabled)).length,
[plants]
);
const handlePickProfileImage = async () => {
if (isUpdatingImage) return;
setIsUpdatingImage(true);
try {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ['images'],
quality: 0.85,
base64: true,
});
if (!result.canceled && result.assets[0]) {
const asset = result.assets[0];
const imageUri = asset.base64
? `data:image/jpeg;base64,${asset.base64}`
: asset.uri;
await setProfileImage(imageUri);
}
} catch (error) {
console.error('Failed to pick profile image', error);
Alert.alert(copy.photoErrorTitle, copy.photoErrorMessage);
} finally {
setIsUpdatingImage(false);
}
};
const handleRemovePhoto = async () => {
await setProfileImage(null);
};
const handleSaveName = async () => {
if (!normalizedDraftName) {
Alert.alert(copy.nameErrorTitle, copy.nameErrorMessage);
return;
}
if (!canSaveName) {
Keyboard.dismiss();
return;
}
setIsSavingName(true);
try {
await setProfileName(normalizedDraftName);
Keyboard.dismiss();
} finally {
setIsSavingName(false);
}
};
const menuItems = [
{ label: copy.menuSettings, icon: 'settings-outline', route: '/profile/preferences' as any },
{ label: copy.menuBilling, icon: 'card-outline', route: '/profile/billing' as any },
{ label: copy.menuData, icon: 'shield-checkmark-outline', route: '/profile/data' as any },
];
return (
<SafeAreaView edges={['top', 'left', 'right']} style={[styles.container, { backgroundColor: colors.background }]}>
<ThemeBackdrop colors={colors} />
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
<Text style={[styles.title, { color: colors.text }]}>{t.tabProfile}</Text>
<View style={[styles.card, { backgroundColor: colors.cardBg, borderColor: colors.cardBorder }]}>
<Text style={[styles.cardTitle, { color: colors.text }]}>{copy.overviewLabel}</Text>
<View style={styles.statsRow}>
{[
{ label: copy.statPlants, value: plants.length.toString() },
{ label: copy.statDueToday, value: dueTodayCount.toString() },
{ label: copy.statReminders, value: remindersEnabledCount.toString() },
].map((item) => (
<View
key={item.label}
style={[styles.statCard, { backgroundColor: colors.surfaceStrong, borderColor: colors.borderStrong }]}
>
<Text style={[styles.statValue, { color: colors.text }]}>{item.value}</Text>
<Text style={[styles.statLabel, { color: colors.textSecondary }]}>{item.label}</Text>
</View>
))}
</View>
</View>
<View style={[styles.card, styles.accountCard, { backgroundColor: colors.cardBg, borderColor: colors.cardBorder }]}>
<Text style={[styles.cardTitle, { color: colors.text }]}>{copy.account}</Text>
<View style={styles.accountRow}>
<TouchableOpacity
style={[styles.avatarFrame, { backgroundColor: colors.primaryDark }]}
onPress={handlePickProfileImage}
activeOpacity={0.9}
>
{profileImageUri ? (
<Image source={{ uri: profileImageUri }} style={styles.avatarImage} />
) : (
<Ionicons name="person" size={34} color={colors.iconOnImage} />
)}
</TouchableOpacity>
<View style={styles.accountMeta}>
<Text style={[styles.currentName, { color: colors.text }]}>{profileName}</Text>
<Text style={[styles.plantsCount, { color: colors.textSecondary }]}>
{plants.length} {copy.statPlants}
</Text>
</View>
</View>
<View style={styles.photoButtons}>
<TouchableOpacity
style={[styles.photoActionBtn, { backgroundColor: colors.primary }]}
onPress={handlePickProfileImage}
activeOpacity={0.85}
disabled={isUpdatingImage}
>
<Text style={[styles.photoActionText, { color: colors.onPrimary }]}>
{isUpdatingImage ? '...' : copy.changePhoto}
</Text>
</TouchableOpacity>
{profileImageUri ? (
<TouchableOpacity
style={[styles.photoActionBtn, styles.photoSecondaryBtn, { borderColor: colors.borderStrong }]}
onPress={handleRemovePhoto}
activeOpacity={0.85}
>
<Text style={[styles.photoSecondaryText, { color: colors.textSecondary }]}>
{copy.removePhoto}
</Text>
</TouchableOpacity>
) : null}
</View>
<Text style={[styles.fieldLabel, { color: colors.textSecondary }]}>{copy.nameLabel}</Text>
<View style={styles.nameRow}>
<TextInput
style={[
styles.nameInput,
{
color: colors.text,
backgroundColor: colors.inputBg,
borderColor: colors.inputBorder,
},
]}
value={draftName}
placeholder={copy.namePlaceholder}
placeholderTextColor={colors.textMuted}
onChangeText={setDraftName}
maxLength={40}
autoCapitalize="words"
returnKeyType="done"
onSubmitEditing={handleSaveName}
/>
<TouchableOpacity
style={[
styles.saveNameBtn,
{
backgroundColor: canSaveName ? colors.primary : colors.surfaceStrong,
borderColor: canSaveName ? colors.primaryDark : colors.borderStrong,
},
]}
onPress={handleSaveName}
disabled={!canSaveName || isSavingName}
activeOpacity={0.85}
>
<Text style={[styles.saveNameText, { color: canSaveName ? colors.onPrimary : colors.textMuted }]}>
{isSavingName ? '...' : copy.saveName}
</Text>
</TouchableOpacity>
</View>
</View>
<View style={[styles.card, { backgroundColor: colors.cardBg, borderColor: colors.cardBorder, padding: 0, overflow: 'hidden' }]}>
{menuItems.map((item, idx) => (
<TouchableOpacity
key={item.route}
style={[
styles.menuItem,
idx !== menuItems.length - 1 && { borderBottomWidth: 1, borderBottomColor: colors.borderStrong }
]}
onPress={() => router.push(item.route)}
activeOpacity={0.7}
>
<View style={styles.menuItemLeft}>
<Ionicons name={item.icon as any} size={20} color={colors.text} />
<Text style={[styles.menuItemText, { color: colors.text }]}>{item.label}</Text>
</View>
<Ionicons name="chevron-forward" size={20} color={colors.textMuted} />
</TouchableOpacity>
))}
</View>
{/* Logout */}
<TouchableOpacity
style={[styles.logoutBtn, { borderColor: colors.dangerSoft, backgroundColor: colors.dangerSoft }]}
activeOpacity={0.78}
onPress={() => {
Alert.alert(copy.logoutConfirmTitle, copy.logoutConfirmMessage, [
{ text: t.cancel, style: 'cancel' },
{
text: copy.logoutConfirmBtn,
style: 'destructive',
onPress: async () => {
await signOut();
router.replace('/auth/login');
},
},
]);
}}
>
<Ionicons name="log-out-outline" size={18} color={colors.danger} />
<Text style={[styles.logoutText, { color: colors.danger }]}>{copy.logout}</Text>
</TouchableOpacity>
<View style={{ height: 40 }} />
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingHorizontal: 20,
},
scrollContent: {
paddingBottom: 20,
},
title: {
marginTop: 14,
marginBottom: 14,
fontSize: 28,
fontWeight: '700',
},
card: {
borderWidth: 1,
borderRadius: 18,
padding: 14,
marginBottom: 12,
gap: 10,
},
accountCard: {
marginBottom: 14,
},
logoutBtn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
borderRadius: 14,
borderWidth: 1,
paddingVertical: 14,
marginBottom: 12,
},
logoutText: {
fontSize: 15,
fontWeight: '600',
},
cardTitle: {
fontSize: 15,
fontWeight: '700',
},
statsRow: {
flexDirection: 'row',
gap: 8,
},
statCard: {
flex: 1,
borderWidth: 1,
borderRadius: 12,
paddingVertical: 12,
paddingHorizontal: 8,
alignItems: 'center',
justifyContent: 'center',
gap: 2,
},
statValue: {
fontSize: 22,
lineHeight: 24,
fontWeight: '700',
},
statLabel: {
fontSize: 11,
fontWeight: '600',
textAlign: 'center',
},
accountRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
},
accountMeta: {
flex: 1,
gap: 3,
},
currentName: {
fontSize: 18,
fontWeight: '700',
},
plantsCount: {
fontSize: 13,
fontWeight: '600',
},
avatarFrame: {
width: 74,
height: 74,
borderRadius: 22,
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
},
avatarImage: {
width: '100%',
height: '100%',
},
photoButtons: {
flexDirection: 'row',
gap: 8,
},
photoActionBtn: {
borderRadius: 10,
paddingVertical: 10,
paddingHorizontal: 12,
},
photoSecondaryBtn: {
borderWidth: 1,
},
photoActionText: {
fontSize: 12,
fontWeight: '700',
},
photoSecondaryText: {
fontSize: 12,
fontWeight: '700',
},
fieldLabel: {
marginTop: 2,
fontSize: 12,
fontWeight: '600',
},
nameRow: {
flexDirection: 'row',
gap: 8,
alignItems: 'center',
},
nameInput: {
flex: 1,
borderWidth: 1,
borderRadius: 12,
paddingHorizontal: 12,
paddingVertical: 10,
fontSize: 14,
fontWeight: '500',
},
saveNameBtn: {
borderWidth: 1,
borderRadius: 12,
paddingHorizontal: 12,
paddingVertical: 10,
},
saveNameText: {
fontSize: 12,
fontWeight: '700',
},
menuItem: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 16,
paddingHorizontal: 14,
},
menuItemLeft: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
},
menuItemText: {
fontSize: 16,
fontWeight: '500',
}
});