feat: Initialize GreenLens project with core dependencies and structure
Sets up the project using Vite, React, and TypeScript. Includes initial configuration for Tailwind CSS, Gemini API integration, and local storage management. Defines basic types for plant data and UI elements. The README is updated with local development instructions.
This commit is contained in:
302
components/PlantDetail.tsx
Normal file
302
components/PlantDetail.tsx
Normal file
@@ -0,0 +1,302 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Plant, Language } from '../types';
|
||||
import { Droplets, Sun, Thermometer, ArrowLeft, Calendar, Trash2, Share2, Edit2, AlertCircle, Check, Clock, Bell, BellOff } from 'lucide-react';
|
||||
|
||||
interface PlantDetailProps {
|
||||
plant: Plant;
|
||||
onClose: () => void;
|
||||
onDelete: (id: string) => void;
|
||||
onUpdate: (plant: Plant) => void;
|
||||
t: any;
|
||||
language: Language;
|
||||
}
|
||||
|
||||
export const PlantDetail: React.FC<PlantDetailProps> = ({ plant, onClose, onDelete, onUpdate, t, language }) => {
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
|
||||
// Map internal language codes to locale strings for Date
|
||||
const localeMap: Record<string, string> = {
|
||||
de: 'de-DE',
|
||||
en: 'en-US',
|
||||
es: 'es-ES'
|
||||
};
|
||||
|
||||
const formattedAddedDate = new Date(plant.dateAdded).toLocaleDateString(localeMap[language] || 'de-DE');
|
||||
const formattedWateredDate = new Date(plant.lastWatered).toLocaleDateString(localeMap[language] || 'de-DE');
|
||||
|
||||
// Calculate next watering date
|
||||
const lastWateredObj = new Date(plant.lastWatered);
|
||||
const nextWateringDate = new Date(lastWateredObj);
|
||||
nextWateringDate.setDate(lastWateredObj.getDate() + plant.careInfo.waterIntervalDays);
|
||||
|
||||
const formattedNextWatering = nextWateringDate.toLocaleDateString(localeMap[language] || 'de-DE', { weekday: 'long', day: 'numeric', month: 'numeric' });
|
||||
const nextWateringText = t.nextWatering.replace('{0}', formattedNextWatering);
|
||||
const lastWateredText = t.lastWateredDate.replace('{0}', formattedWateredDate);
|
||||
|
||||
// Check if watered today
|
||||
const isWateredToday = new Date(plant.lastWatered).toDateString() === new Date().toDateString();
|
||||
|
||||
const handleWaterPlant = () => {
|
||||
const now = new Date().toISOString();
|
||||
// Update history: add new date to the beginning, keep last 10 entries max
|
||||
const currentHistory = plant.wateringHistory || [];
|
||||
const newHistory = [now, ...currentHistory].slice(0, 10);
|
||||
|
||||
const updatedPlant = {
|
||||
...plant,
|
||||
lastWatered: now,
|
||||
wateringHistory: newHistory
|
||||
};
|
||||
onUpdate(updatedPlant);
|
||||
};
|
||||
|
||||
const toggleReminder = async () => {
|
||||
const newValue = !plant.notificationsEnabled;
|
||||
|
||||
if (newValue) {
|
||||
// Request permission if enabling
|
||||
if (!('Notification' in window)) {
|
||||
alert("Notifications are not supported by this browser.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Notification.permission === 'granted') {
|
||||
onUpdate({ ...plant, notificationsEnabled: true });
|
||||
} else if (Notification.permission !== 'denied') {
|
||||
const permission = await Notification.requestPermission();
|
||||
if (permission === 'granted') {
|
||||
onUpdate({ ...plant, notificationsEnabled: true });
|
||||
}
|
||||
} else {
|
||||
alert(t.reminderPermissionNeeded);
|
||||
}
|
||||
} else {
|
||||
// Disabling
|
||||
onUpdate({ ...plant, notificationsEnabled: false });
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteClick = () => {
|
||||
setShowDeleteConfirm(true);
|
||||
};
|
||||
|
||||
const handleConfirmDelete = () => {
|
||||
onDelete(plant.id);
|
||||
};
|
||||
|
||||
const handleCancelDelete = () => {
|
||||
setShowDeleteConfirm(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex flex-col h-full bg-stone-50 dark:bg-stone-950 overflow-y-auto no-scrollbar animate-in slide-in-from-right duration-300">
|
||||
|
||||
{/* Header */}
|
||||
<div className="absolute top-0 left-0 right-0 z-10 flex justify-between items-center p-6 text-stone-900 dark:text-white">
|
||||
<button onClick={onClose} className="bg-white/80 dark:bg-black/50 backdrop-blur-md p-2 rounded-full shadow-sm">
|
||||
<ArrowLeft size={20} />
|
||||
</button>
|
||||
<div className="flex space-x-2">
|
||||
<button className="bg-white/80 dark:bg-black/50 backdrop-blur-md p-2 rounded-full shadow-sm">
|
||||
<Share2 size={20} />
|
||||
</button>
|
||||
<button className="bg-white/80 dark:bg-black/50 backdrop-blur-md p-2 rounded-full shadow-sm">
|
||||
<Edit2 size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
{/* Hero Image */}
|
||||
<div className="relative w-full aspect-[4/5] md:aspect-video rounded-b-[2.5rem] overflow-hidden shadow-lg mb-6">
|
||||
<img src={plant.imageUri} alt={plant.name} className="w-full h-full object-cover" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent"></div>
|
||||
|
||||
<div className="absolute bottom-8 left-6 right-6">
|
||||
<h1 className="text-3xl font-serif font-bold text-white leading-tight mb-1 shadow-sm">
|
||||
{plant.name}
|
||||
</h1>
|
||||
<p className="text-stone-200 italic text-sm">
|
||||
{plant.botanicalName}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Container */}
|
||||
<div className="px-6 pb-24">
|
||||
{/* Added Date Info */}
|
||||
<div className="flex justify-between items-center text-xs text-stone-500 dark:text-stone-400 mb-6">
|
||||
<div className="flex items-center space-x-1.5 bg-white dark:bg-stone-900 px-3 py-1.5 rounded-full border border-stone-100 dark:border-stone-800">
|
||||
<Calendar size={12} />
|
||||
<span>{t.addedOn} {formattedAddedDate}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Action: Water */}
|
||||
<div className={`mb-3 p-4 rounded-2xl border flex justify-between items-center transition-colors ${
|
||||
isWateredToday
|
||||
? 'bg-green-50 dark:bg-green-900/10 border-green-100 dark:border-green-800/30'
|
||||
: 'bg-blue-50 dark:bg-blue-900/10 border-blue-100 dark:border-blue-800/30'
|
||||
}`}>
|
||||
<div>
|
||||
<span className="block text-xs text-stone-500 dark:text-stone-400 font-medium mb-0.5">{lastWateredText}</span>
|
||||
<span className={`block text-sm font-bold ${isWateredToday ? 'text-green-700 dark:text-green-300' : 'text-stone-900 dark:text-stone-200'}`}>
|
||||
{nextWateringText}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleWaterPlant}
|
||||
disabled={isWateredToday}
|
||||
className={`px-4 py-2.5 rounded-xl font-bold text-xs flex items-center shadow-lg transition-all ${
|
||||
isWateredToday
|
||||
? 'bg-green-500 text-white cursor-default shadow-green-500/30'
|
||||
: 'bg-blue-500 hover:bg-blue-600 active:scale-95 text-white shadow-blue-500/30'
|
||||
}`}
|
||||
>
|
||||
{isWateredToday ? (
|
||||
<>
|
||||
<Check size={14} className="mr-2" />
|
||||
{t.watered}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Droplets size={14} className="mr-2 fill-current" />
|
||||
{t.waterNow}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Reminder Toggle */}
|
||||
<div className="mb-8 flex items-center justify-between p-3 rounded-xl bg-white dark:bg-stone-900 border border-stone-100 dark:border-stone-800">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={`p-2 rounded-full ${plant.notificationsEnabled ? 'bg-amber-100 text-amber-600 dark:bg-amber-900/30 dark:text-amber-400' : 'bg-stone-100 text-stone-400 dark:bg-stone-800 dark:text-stone-500'}`}>
|
||||
{plant.notificationsEnabled ? <Bell size={18} /> : <BellOff size={18} />}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-bold text-stone-900 dark:text-stone-100">{t.reminder}</span>
|
||||
<span className="text-[10px] text-stone-500">{plant.notificationsEnabled ? t.reminderOn : t.reminderOff}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={toggleReminder}
|
||||
className={`w-11 h-6 rounded-full relative transition-colors duration-300 ${plant.notificationsEnabled ? 'bg-primary-500' : 'bg-stone-300 dark:bg-stone-700'}`}
|
||||
>
|
||||
<div className={`absolute top-1 w-4 h-4 bg-white rounded-full transition-all duration-300 shadow-sm ${plant.notificationsEnabled ? 'left-6' : 'left-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h3 className="font-bold text-stone-900 dark:text-stone-100 mb-2">{t.aboutPlant}</h3>
|
||||
<p className="text-stone-600 dark:text-stone-300 text-sm leading-relaxed mb-8">
|
||||
{plant.description || t.noDescription}
|
||||
</p>
|
||||
|
||||
{/* Care Info */}
|
||||
<h3 className="font-bold text-stone-900 dark:text-stone-100 mb-4">{t.careTips}</h3>
|
||||
<div className="grid grid-cols-3 gap-3 mb-10">
|
||||
<div className="bg-white dark:bg-stone-900 p-3 rounded-2xl border border-stone-100 dark:border-stone-800 flex flex-col items-center text-center shadow-sm">
|
||||
<div className="w-8 h-8 rounded-full bg-blue-50 dark:bg-blue-900/20 text-blue-500 flex items-center justify-center mb-2">
|
||||
<Droplets size={16} className="fill-current" />
|
||||
</div>
|
||||
<span className="text-[10px] text-stone-400 font-medium mb-0.5">{t.water}</span>
|
||||
<span className="text-xs font-bold text-stone-800 dark:text-stone-200">
|
||||
{plant.careInfo.waterIntervalDays} {t.days || 'Tage'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-stone-900 p-3 rounded-2xl border border-stone-100 dark:border-stone-800 flex flex-col items-center text-center shadow-sm">
|
||||
<div className="w-8 h-8 rounded-full bg-amber-50 dark:bg-amber-900/20 text-amber-500 flex items-center justify-center mb-2">
|
||||
<Sun size={16} className="fill-current" />
|
||||
</div>
|
||||
<span className="text-[10px] text-stone-400 font-medium mb-0.5">{t.light}</span>
|
||||
<span className="text-xs font-bold text-stone-800 dark:text-stone-200 truncate w-full">
|
||||
{plant.careInfo.light}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-stone-900 p-3 rounded-2xl border border-stone-100 dark:border-stone-800 flex flex-col items-center text-center shadow-sm">
|
||||
<div className="w-8 h-8 rounded-full bg-rose-50 dark:bg-rose-900/20 text-rose-500 flex items-center justify-center mb-2">
|
||||
<Thermometer size={16} className="fill-current" />
|
||||
</div>
|
||||
<span className="text-[10px] text-stone-400 font-medium mb-0.5">{t.temp}</span>
|
||||
<span className="text-xs font-bold text-stone-800 dark:text-stone-200">
|
||||
{plant.careInfo.temp}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Watering History Section */}
|
||||
<h3 className="font-bold text-stone-900 dark:text-stone-100 mb-4">{t.wateringHistory}</h3>
|
||||
<div className="bg-white dark:bg-stone-900 rounded-2xl border border-stone-100 dark:border-stone-800 overflow-hidden mb-10 shadow-sm">
|
||||
{(!plant.wateringHistory || plant.wateringHistory.length === 0) ? (
|
||||
<div className="p-6 text-center text-stone-400 text-sm">
|
||||
<Clock size={24} className="mx-auto mb-2 opacity-50" />
|
||||
{t.noHistory}
|
||||
</div>
|
||||
) : (
|
||||
<ul className="divide-y divide-stone-100 dark:divide-stone-800">
|
||||
{plant.wateringHistory.slice(0, 5).map((dateStr, index) => (
|
||||
<li key={index} className="px-5 py-3 flex justify-between items-center group hover:bg-stone-50 dark:hover:bg-stone-800/50 transition-colors">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-8 h-8 rounded-full bg-blue-50 dark:bg-blue-900/10 text-blue-500 flex items-center justify-center">
|
||||
<Droplets size={14} className="fill-current" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-stone-700 dark:text-stone-300">
|
||||
{new Date(dateStr).toLocaleDateString(localeMap[language] || 'de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs font-mono text-stone-400 bg-stone-100 dark:bg-stone-800 px-2 py-1 rounded-md">
|
||||
{new Date(dateStr).toLocaleTimeString(localeMap[language] || 'de-DE', { hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Danger Zone */}
|
||||
<div className="border-t border-stone-200 dark:border-stone-800 pt-6 flex justify-center">
|
||||
<button
|
||||
onClick={handleDeleteClick}
|
||||
className="flex items-center space-x-2 text-red-500 hover:text-red-600 px-4 py-2 rounded-xl hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
<span className="text-sm font-medium">{t.delete}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
{showDeleteConfirm && (
|
||||
<div className="absolute inset-0 z-[60] bg-black/40 backdrop-blur-sm flex items-center justify-center p-6 animate-in fade-in duration-200">
|
||||
<div className="bg-white dark:bg-stone-900 w-full max-w-sm rounded-2xl p-6 shadow-2xl scale-100 animate-in zoom-in-95 duration-200">
|
||||
<div className="w-12 h-12 bg-red-100 dark:bg-red-900/30 text-red-500 rounded-full flex items-center justify-center mb-4 mx-auto">
|
||||
<AlertCircle size={24} />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-center text-stone-900 dark:text-white mb-2">{t.deleteConfirmTitle}</h3>
|
||||
<p className="text-stone-500 dark:text-stone-400 text-center text-sm mb-6 leading-relaxed">
|
||||
{t.deleteConfirmMessage}
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
onClick={handleCancelDelete}
|
||||
className="py-3 px-4 rounded-xl bg-stone-100 dark:bg-stone-800 text-stone-700 dark:text-stone-300 font-bold text-sm"
|
||||
>
|
||||
{t.cancel}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleConfirmDelete}
|
||||
className="py-3 px-4 rounded-xl bg-red-500 text-white font-bold text-sm hover:bg-red-600"
|
||||
>
|
||||
{t.confirm}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user