search bar for plants
import React, { useState, useEffect, createContext, useContext } from 'react';
import { initializeApp } from 'firebase/app';
import { getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken } from 'firebase/auth';
import { getFirestore, collection, addDoc, onSnapshot, query, orderBy } from 'firebase/firestore';
// --- Firebase Context and Provider ---
const FirebaseContext = createContext(null);
const FirebaseProvider = ({ children }) => {
const [db, setDb] = useState(null);
const [auth, setAuth] = useState(null);
const [userId, setUserId] = useState(null);
const [firebaseReady, setFirebaseReady] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const initializeFirebase = async () => {
try {
// Mandatory global variables provided by the Canvas environment
const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id';
const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : {};
const initialAuthToken = typeof __initial_auth_token !== 'undefined' ? __initial_auth_token : null;
if (!Object.keys(firebaseConfig).length) {
throw new Error("Firebase configuration not provided. Cannot initialize Firebase.");
}
const app = initializeApp(firebaseConfig);
const firestore = getFirestore(app);
const firebaseAuth = getAuth(app);
setDb(firestore);
setAuth(firebaseAuth);
// Authenticate user
if (initialAuthToken) {
await signInWithCustomToken(firebaseAuth, initialAuthToken);
} else {
await signInAnonymously(firebaseAuth);
}
// Listen for auth state changes to get the user ID
onAuthStateChanged(firebaseAuth, (user) => {
if (user) {
setUserId(user.uid);
} else {
// Fallback for userId if auth fails or not available, use a random ID
setUserId(crypto.randomUUID());
}
setFirebaseReady(true);
});
} catch (err) {
console.error("Failed to initialize Firebase or authenticate:", err);
setError("Failed to initialize the application. Please try again later.");
setFirebaseReady(true); // Still set ready to true so UI can show error
}
};
initializeFirebase();
}, []); // Run only once on component mount
if (error) {
return (
<div className="flex items-center justify-center min-h-screen bg-red-100 text-red-800">
<p>Error initializing application: {error}</p>
</div>
);
}
if (!firebaseReady) {
return (
<div className="flex items-center justify-center min-h-screen bg-gray-50">
<div className="flex flex-col items-center">
<svg className="animate-spin h-10 w-10 text-green-600 mb-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<p className="text-lg text-gray-700">Loading application...</p>
</div>
</div>
);
}
return (
<FirebaseContext.Provider value={{ db, auth, userId }}>
{children}
</FirebaseContext.Provider>
);
};
// --- Plant Card Component ---
const PlantCard = ({ plant }) => {
// Placeholder image if plant.imageUrl is not provided or invalid
const handleError = (e) => {
e.target.onerror = null; // Prevents infinite loop if placeholder also fails
e.target.src = 'https://placehold.co/400x300/a3e635/166534?text=No+Image'; // Green placeholder
};
return (
<div className="bg-white rounded-xl shadow-lg overflow-hidden border border-gray-200 transform hover:scale-105 transition-all duration-300">
<img
src={plant.imageUrl || 'https://placehold.co/400x300/a3e635/166534?text=No+Image'}
alt={plant.name}
className="w-full h-48 object-cover object-center"
onError={handleError}
/>
<div className="p-6">
<h3 className="text-2xl font-bold text-green-800 mb-2">{plant.name}</h3>
<p className="text-gray-700 text-sm mb-3 line-clamp-3">{plant.description}</p>
<div className="mb-4">
<p className="text-gray-900 font-semibold mb-1">Uses:</p>
<ul className="list-disc list-inside text-gray-700 text-sm">
{plant.uses.split(',').map((use, index) => (
<li key={index} className="mb-1">{use.trim()}</li>
))}
</ul>
</div>
<span className="inline-block bg-green-100 text-green-800 text-xs font-medium px-3 py-1 rounded-full">
{plant.category}
</span>
</div>
</div>
);
};
// --- Plant Form Component ---
const PlantForm = ({ onSubmit }) => {
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [uses, setUses] = useState('');
const [imageUrl, setImageUrl] = useState('');
const [category, setCategory] = useState('');
const [message, setMessage] = useState('');
const [messageType, setMessageType] = useState(''); // 'success' or 'error'
const categories = ['Herb', 'Spice', 'Fruit', 'Vegetable', 'Flower', 'Tree', 'Other'];
const handleSubmit = (e) => {
e.preventDefault();
if (!name || !description || !uses || !category) {
setMessage('Please fill in all required fields.');
setMessageType('error');
return;
}
const newPlant = {
name,
description,
uses,
imageUrl,
category,
createdAt: new Date() // Timestamp for sorting
};
onSubmit(newPlant);
setName('');
setDescription('');
setUses('');
setImageUrl('');
setCategory('');
setMessage('Plant added successfully!');
setMessageType('success');
setTimeout(() => setMessage(''), 3000); // Clear message after 3 seconds
};
return (
<div className="bg-white p-8 rounded-2xl shadow-xl border border-gray-200 w-full">
<h2 className="text-3xl font-bold text-green-700 mb-6 text-center">Add New Plant</h2>
{message && (
<div className={`p-3 mb-4 rounded-lg text-sm ${messageType === 'success' ? 'bg-green-100 text-green-700 border border-green-300' : 'bg-red-100 text-red-700 border border-red-300'}`}>
{message}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-5">
<div>
<label htmlFor="name" className="block text-md font-medium text-gray-700 mb-2">Plant Name <span className="text-red-500">*</span></label>
<input
type="text"
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g., Turmeric"
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-green-500 focus:border-green-500 transition-all duration-200"
required
/>
</div>
<div>
<label htmlFor="description" className="block text-md font-medium text-gray-700 mb-2">Description <span className="text-red-500">*</span></label>
<textarea
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
rows="3"
placeholder="Brief description of the plant..."
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-green-500 focus:border-green-500 transition-all duration-200 resize-y"
required
></textarea>
</div>
<div>
<label htmlFor="uses" className="block text-md font-medium text-gray-700 mb-2">Uses (comma-separated) <span className="text-red-500">*</span></label>
<input
type="text"
id="uses"
value={uses}
onChange={(e) => setUses(e.target.value)}
placeholder="e.g., Anti-inflammatory, Digestive aid, Skin health"
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-green-500 focus:border-green-500 transition-all duration-200"
required
/>
</div>
<div>
<label htmlFor="imageUrl" className="block text-md font-medium text-gray-700 mb-2">Image URL (Optional)</label>
<input
type="url"
id="imageUrl"
value={imageUrl}
onChange={(e) => setImageUrl(e.target.value)}
placeholder="e.g., https://example.com/turmeric.jpg"
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-green-500 focus:border-green-500 transition-all duration-200"
/>
</div>
<div>
<label htmlFor="category" className="block text-md font-medium text-gray-700 mb-2">Category <span className="text-red-500">*</span></label>
<select
id="category"
value={category}
onChange={(e) => setCategory(e.target.value)}
className="w-full p-3 border border-gray-300 rounded-lg bg-white focus:ring-green-500 focus:border-green-500 transition-all duration-200"
required
>
<option value="">Select a category</option>
{categories.map((cat) => (
<option key={cat} value={cat}>{cat}</option>
))}
</select>
</div>
<button
type="submit"
className="w-full bg-green-600 hover:bg-green-700 text-white font-semibold py-3 px-6 rounded-lg shadow-md transition transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-opacity-75"
>
Add Plant
</button>
</form>
</div>
);
};
// --- Plant List Component ---
const PlantList = () => {
const { db, userId } = useContext(FirebaseContext);
const [plants, setPlants] = useState([]);
const [loadingPlants, setLoadingPlants] = useState(true);
const [plantsError, setPlantsError] = useState(null);
const [searchTerm, setSearchTerm] = useState(''); // New state for search term
useEffect(() => {
if (!db || !userId) return; // Wait until Firebase is ready
// Path for public data storage
// __app_id is a global variable provided by the Canvas environment
const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id';
const plantsCollectionRef = collection(db, `artifacts/${appId}/public/data/plants`);
const q = query(plantsCollectionRef, orderBy('createdAt', 'desc')); // Order by creation time
const unsubscribe = onSnapshot(q,
(snapshot) => {
const plantsData = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
}));
setPlants(plantsData);
setLoadingPlants(false);
},
(error) => {
console.error("Error fetching plants:", error);
setPlantsError("Failed to load plants. Please try refreshing.");
setLoadingPlants(false);
}
);
// Cleanup subscription on component unmount
return () => unsubscribe();
}, [db, userId]);
// Filter plants based on search term
const filteredPlants = plants.filter(plant =>
plant.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
plant.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
plant.uses.toLowerCase().includes(searchTerm.toLowerCase())
);
// Group filtered plants by category
const plantsByCategory = filteredPlants.reduce((acc, plant) => {
const category = plant.category || 'Uncategorized';
if (!acc[category]) {
acc[category] = [];
}
acc[category].push(plant);
return acc;
}, {});
return (
<div className="w-full">
<h2 className="text-3xl font-bold text-green-700 mb-6 text-center">Available Plants</h2>
{/* Search Bar */}
<div className="mb-8 p-4 bg-white rounded-xl shadow-md border border-gray-200">
<label htmlFor="search-plant" className="sr-only">Search Plants</label>
<input
type="text"
id="search-plant"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search plants by name, description, or uses..."
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-green-500 focus:border-green-500 transition-all duration-200 text-lg"
/>
</div>
{loadingPlants && (
<div className="flex items-center justify-center p-8">
<svg className="animate-spin h-8 w-8 text-green-600 mr-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<p className="text-gray-700">Loading plants...</p>
</div>
)}
{plantsError && (
<div className="p-4 bg-red-100 text-red-700 rounded-lg border border-red-300 mb-6">
<p>{plantsError}</p>
</div>
)}
{!loadingPlants && filteredPlants.length === 0 && (
<p className="text-center text-gray-600 p-8">No matching plants found. Try a different search term or add a new plant!</p>
)}
{Object.keys(plantsByCategory).sort().map((category) => (
<div key={category} className="mb-10">
<h3 className="text-2xl font-semibold text-green-800 border-b-2 border-green-300 pb-2 mb-6 capitalize">
{category}s
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{plantsByCategory[category].map((plant) => (
<PlantCard key={plant.id} plant={plant} />
))}
</div>
</div>
))}
</div>
);
};
// --- Main Application Component ---
const RootApplication = () => {
const { db, userId } = useContext(FirebaseContext); // Correctly consumes context now
const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id';
const handleAddPlant = async (newPlant) => {
if (!db) {
console.error("Firestore DB not initialized.");
return;
}
try {
// Store public data under /artifacts/{appId}/public/data/plants
const docRef = await addDoc(collection(db, `artifacts/${appId}/public/data/plants`), newPlant);
console.log("Document written with ID: ", docRef.id);
} catch (e) {
console.error("Error adding document: ", e);
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-green-50 to-teal-100 font-inter text-gray-800 flex flex-col">
<style>
{`
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
body { font-family: 'Inter', sans-serif; }
`}
</style>
{/* Header */}
<header className="bg-gradient-to-r from-green-700 to-green-900 text-white py-6 shadow-lg rounded-b-xl">
<div className="container mx-auto px-4 text-center">
<h1 className="text-4xl font-extrabold mb-2 drop-shadow-md">
Ayurvedic Plant Data Manager
</h1>
<p className="text-lg opacity-90">
Collect and categorize information about medicinal plants.
</p>
{userId && (
<p className="text-xs mt-2 opacity-70">Your User ID: {userId}</p>
)}
</div>
</header>
{/* Main Content */}
<main className="container mx-auto px-4 py-10 flex-grow">
<section className="mb-12">
<PlantForm onSubmit={handleAddPlant} />
</section>
<hr className="border-t-2 border-green-300 my-10" />
<section>
<PlantList />
</section>
</main>
{/* Footer */}
<footer className="bg-gray-800 text-white text-center py-6 mt-auto rounded-t-xl">
<p>© 2025 Ayurvedic Plant Data Manager. All rights reserved.</p>
</footer>
</div>
);
};
// --- App Wrapper (default export) ---
// This component ensures FirebaseProvider wraps the entire application
export default function App() {
return (
<FirebaseProvider>
<RootApplication />
</FirebaseProvider>
);
}
Comments
Post a Comment