import React, { useState, useEffect, createContext, useContext } from ‘react’;
import { initializeApp } from ‘firebase/app’;
import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from ‘firebase/auth’;
import { getFirestore, collection, query, onSnapshot, addDoc, serverTimestamp, orderBy } from ‘firebase/firestore’;
// Context for Firebase services
const FirebaseContext = createContext(null);
// Firebase Provider component
const FirebaseProvider = ({ children }) => {
const [firebaseApp, setFirebaseApp] = useState(null);
const [db, setDb] = useState(null);
const [auth, setAuth] = useState(null);
const [userId, setUserId] = useState(null);
const [isAuthReady, setIsAuthReady] = useState(false);
useEffect(() => {
try {
const firebaseConfig = typeof __firebase_config !== ‘undefined’ ? JSON.parse(__firebase_config) : {};
const app = initializeApp(firebaseConfig);
const firestoreDb = getFirestore(app);
const firebaseAuth = getAuth(app);
setFirebaseApp(app);
setDb(firestoreDb);
setAuth(firebaseAuth);
console.log(“Firebase initialized.”);
// Sign in anonymously or with custom token
const signInUser = async () => {
const initialAuthToken = typeof __initial_auth_token !== ‘undefined’ ? __initial_auth_token : null;
try {
if (initialAuthToken) {
await signInWithCustomToken(firebaseAuth, initialAuthToken);
console.log(“Signed in with custom token.”);
} else {
await signInAnonymously(firebaseAuth);
console.log(“Signed in anonymously.”);
}
} catch (error) {
console.error(“Firebase authentication error during sign-in:”, error);
}
};
signInUser();
// Set up auth state change listener
const unsubscribe = onAuthStateChanged(firebaseAuth, (user) => {
if (user) {
setUserId(user.uid);
console.log(“Authentication state changed: User is logged in with UID:”, user.uid);
} else {
// If no user, generate a random ID for unauthenticated users
setUserId(crypto.randomUUID());
console.log(“Authentication state changed: No user logged in, generated anonymous ID:”, userId); // userId here might be from previous render
}
setIsAuthReady(true);
console.log(“isAuthReady set to true.”);
});
return () => unsubscribe();
} catch (error) {
console.error(“Failed to initialize Firebase:”, error);
}
}, []); // Run only once on component mount
return (
{children}
);
};
// Custom hook to use Firebase services
const useFirebase = () => useContext(FirebaseContext);
// Main App component
const App = () => {
const { db, userId, isAuthReady } = useFirebase();
const [businesses, setBusinesses] = useState([]);
const [searchTerm, setSearchTerm] = useState(”);
const [activeCategory, setActiveCategory] = useState(‘All’); // The category used for filtering (All or a specific subcategory)
const [selectedMainCategoryForDisplay, setSelectedMainCategoryForDisplay] = useState(null); // Controls which main category’s subcategories are displayed
const [showAddForm, setShowAddForm] = useState(false);
const [message, setMessage] = useState(”);
const appId = typeof __app_id !== ‘undefined’ ? __app_id : ‘default-app-id’;
// Define the hierarchical structure of categories
const mainCategoryGroups = {
‘Retail & Wholesale’: [‘Supermarkets’, ‘General shops’, ‘Wholesale distributors’, ‘Agrovet stores’],
‘Health & Medical’: [‘Hospitals & clinics’, ‘Pharmacies & chemists’, ‘Dental clinics’, ‘Diagnostic labs’, ‘Herbal & nutrition centers’, ‘Physiotherapy & occupational therapy clinics’],
‘Hospitality & Tourism’: [‘Hotels & lodges’, ‘Restaurants & cafés’, ‘Bars & nightclubs’, ‘Tour operators & travel agencies’],
‘Agriculture & Agribusiness’: [‘Farming (crop/livestock)’, ‘Agro-processing’, ‘Agricultural equipment suppliers’, ‘Exporters of produce’],
‘Education & Training’: [‘Schools (primary, secondary)’, ‘Colleges & universities’, ‘Vocational training centers’, ‘Driving schools’],
‘Transport & Logistics’: [‘Car hire & taxi services’, ‘Freight & courier companies’, ‘Clearing & forwarding agents’, ‘Vehicle import/export businesses’],
‘Construction & Real Estate’: [‘Building contractors’, ‘Hardware stores’, ‘Real estate agencies’, ‘Architects & surveyors’],
‘Finance & Legal’: [‘Banks & SACCO’, ‘Microfinance institutions’, ‘Insurance agencies’, ‘Law firms & legal consultants’, ‘Accountants & auditors’],
‘ICT & Technology’: [‘Cyber cafés’, ‘Software & app developers’, ‘IT support services’, ‘Electronics & phone shops’],
‘Creative & Media’: [‘Photography & Videography’, ‘Graphic design & Branding’, ‘Printing & publishing’, ‘Advertising agencies’],
‘Beauty & Wellness’: [‘Salons & barbershops’, ‘Spas & massage parlors’, ‘Cosmetic shops’, ‘Fitness centers & gyms’],
‘Manufacturing & Industry’: [‘Food & beverage processing’, ‘Furniture making’, ‘Textile & garment production’, ‘EPZ (Export Processing Zone) companies’],
‘Professional Services’: [‘Consultants (business, HR, marketing)’, ‘Employment agencies’, ‘Company secretaries’, ‘City/town planners’],
‘Environment & Conservation’: [‘Wildlife conservation’, ‘Environmental NGOs’, ‘Waste management services’],
‘Government Agencies & Regulatory Bodies’: [‘Transport Authority’, ‘Revenue Authority’, ‘Insurance’, ‘Bureaus Standard’, ‘Ethics and Anticorruption’, ‘Environment’, ‘Communication Authority’, ‘Banking’, ‘Retirement Authority’, ‘Airport Authority’, ‘Ports Authority’, ‘Invest Authority’, ‘Betting Control’, ‘Trade Network’, ‘Kenya Power’, ‘Government Portal’, ‘Business Registration’]
};
// Fetch businesses from Firestore
useEffect(() => {
console.log(“Attempting to fetch businesses. db:”, !!db, “isAuthReady:”, isAuthReady, “userId:”, userId);
if (!db || !isAuthReady) {
console.log(“Firestore or Auth not ready, skipping fetch.”);
return;
}
// Collection path for public data
const collectionPath = `artifacts/${appId}/public/data/businesses`;
console.log(“Fetching from collection path:”, collectionPath);
const businessesColRef = collection(db, collectionPath);
const q = query(businessesColRef);
const unsubscribe = onSnapshot(q, (snapshot) => {
const fetchedBusinesses = snapshot.docs.map(doc => ({
id: doc.id,
…doc.data()
}));
setBusinesses(fetchedBusinesses);
console.log(“Businesses fetched:”, fetchedBusinesses.length, “items.”);
}, (error) => {
console.error(“Error fetching businesses:”, error);
setMessage(`Error fetching business listings: ${error.message}. Please check Firebase rules.`);
});
return () => unsubscribe();
}, [db, appId, isAuthReady, userId]); // Add userId to dependencies to react to auth changes
// Handle click on main category (to toggle subcategory display)
const handleMainCategoryToggle = (mainCatName) => {
// If clicking the currently selected main category, collapse it and reset filter to ‘All’
if (selectedMainCategoryForDisplay === mainCatName) {
setSelectedMainCategoryForDisplay(null);
setActiveCategory(‘All’); // Reset filter to All when collapsing
} else {
// If clicking a different main category, expand it and reset filter to ‘All’
setSelectedMainCategoryForDisplay(mainCatName);
setActiveCategory(‘All’); // Reset filter to All when changing main category display
}
setSearchTerm(”); // Clear search on any category navigation action
};
// Handle click on subcategory (to filter businesses)
const handleSubCategoryFilter = (subCatName) => {
setActiveCategory(subCatName); // Set the specific subcategory as the filter
setSearchTerm(”); // Clear search
};
const handleSearchChange = (event) => {
setSearchTerm(event.target.value);
};
const handleAddBusiness = async (newBusiness) => {
if (!db || !userId) {
setMessage(“Database not ready or user not authenticated.”);
console.warn(“Attempted to add business, but DB or User ID not ready.”);
return;
}
try {
console.log(“Attempting to add new business:”, newBusiness);
const docRef = await addDoc(collection(db, `artifacts/${appId}/public/data/businesses`), {
…newBusiness,
userId: userId, // Store the user ID of who added the business
createdAt: serverTimestamp() // Add a timestamp
});
console.log(“Business added successfully with ID:”, docRef.id);
setMessage(‘Business added successfully!’);
setShowAddForm(false); // Hide form after submission
setTimeout(() => setMessage(”), 3000); // Clear message after 3 seconds
} catch (error) {
console.error(“Error adding business:”, error);
setMessage(`Failed to add business: ${error.message}. Please check your Firebase rules.`); // More specific error
}
};
// Filter businesses based on search term and activeCategory (which is always a specific subcategory or ‘All’)
const filteredBusinesses = businesses
.filter(business => {
const lowerCaseSearchTerm = searchTerm.toLowerCase();
// If search term is empty, just apply category filter
if (lowerCaseSearchTerm === ”) {
return activeCategory === ‘All’ || business.category === activeCategory;
}
// Check if any of the specified fields include the search term (case-insensitive)
const matchesSearch =
business.name.toLowerCase().includes(lowerCaseSearchTerm) ||
(business.representative && business.representative.toLowerCase().includes(lowerCaseSearchTerm)) ||
(business.mobile && business.mobile.toLowerCase().includes(lowerCaseSearchTerm)) ||
(business.telephone && business.telephone.toLowerCase().includes(lowerCaseSearchTerm)) ||
business.location.toLowerCase().includes(lowerCaseSearchTerm) ||
business.category.toLowerCase().includes(lowerCaseSearchTerm);
const matchesCategory = activeCategory === ‘All’ ||
business.category === activeCategory; // activeCategory is always a specific subcategory or ‘All’
return matchesSearch && matchesCategory;
})
.sort((a, b) => a.name.localeCompare(b.name)); // Client-side sorting
console.log(“Filtered businesses for display:”, filteredBusinesses.length, “items.”);
return (
{/* Header */}
{/* User ID Display */}
{userId && isAuthReady && (
)}
{/* Message Display for main app */}
{message && (
{message}
)}
{/* Main Category Navigation */}
{/* Subcategory Navigation (conditionally displayed) */}
{selectedMainCategoryForDisplay && mainCategoryGroups[selectedMainCategoryForDisplay] && (
Subcategories:
{mainCategoryGroups[selectedMainCategoryForDisplay].map(subCatName => (
))}
)}
{/* Add Business Button */}
{/* Add Business Form */}
{showAddForm && (
setShowAddForm(false)}
/>
)}
{/* Business Listings */}
{/* Footer */}
);
};
// SearchBar Component (Wrapped with React.memo)
const SearchBar = React.memo(({ searchTerm, onSearchChange }) => {
return (
);
});
// BusinessList Component (No changes needed)
const BusinessList = ({ businesses }) => {
if (businesses.length === 0) {
return (
No businesses found matching your criteria. Try adjusting your search or category.
);
}
return (
{businesses.map(business => (
))}
);
};
// BusinessCard Component (No changes needed)
const BusinessCard = ({ business }) => {
return (
{business.category}
{business.name}
{business.representative && (
Representative: {business.representative}
)}
{business.mobile && (
Mobile: {business.mobile}
)}
{business.telephone && (
Telephone: {business.telephone}
)}
{business.email && (
Email: {business.email}
)}
Location: {business.location}
Added by: {business.userId ? `${business.userId.substring(0, 8)}…` : ‘N/A’}
);
};
// AddBusinessForm Component (Updated for hierarchical category selection and wrapped with React.memo)
const AddBusinessForm = React.memo(({ onAddBusiness, mainCategoryGroups, onCancel }) => {
const [name, setName] = useState(”);
const [representative, setRepresentative] = useState(”);
const [mobile, setMobile] = useState(”);
const [telephone, setTelephone] = useState(”);
const [email, setEmail] = useState(”);
const [location, setLocation] = useState(”);
const [selectedMainCategoryInForm, setSelectedMainCategoryInForm] = useState(”); // State for main category dropdown
const [selectedSubCategoryInForm, setSelectedSubCategoryInForm] = useState(”); // State for subcategory dropdown
const [formMessage, setFormMessage] = useState(”); // New state for form-specific messages
// Initialize main category dropdown with the first available main category
useEffect(() => {
const mainCatKeys = Object.keys(mainCategoryGroups);
if (mainCatKeys.length > 0) {
setSelectedMainCategoryInForm(mainCatKeys[0]);
}
}, [mainCategoryGroups]);
// Update subcategory dropdown when main category selection changes
useEffect(() => {
if (selectedMainCategoryInForm && mainCategoryGroups[selectedMainCategoryInForm] && mainCategoryGroups[selectedMainCategoryInForm].length > 0) {
setSelectedSubCategoryInForm(mainCategoryGroups[selectedMainCategoryInForm][0]);
} else {
setSelectedSubCategoryInForm(”);
}
}, [selectedMainCategoryInForm, mainCategoryGroups]);
const handleSubmit = (e) => {
e.preventDefault();
setFormMessage(”); // Clear previous form messages
// Validate that essential fields are filled, including the selected subcategory
if (!name || !location || !selectedSubCategoryInForm) {
setFormMessage(“Business Name, Location, and Subcategory are required!”);
return;
}
onAddBusiness({
name,
representative,
mobile,
telephone,
email,
location,
category: selectedSubCategoryInForm // Save the selected subcategory as the business’s category
});
// Clear form fields after submission
setName(”);
setRepresentative(”);
setMobile(”);
setTelephone(”);
setEmail(”);
setLocation(”);
// Reset category dropdowns to initial state
setSelectedMainCategoryInForm(Object.keys(mainCategoryGroups)[0] || ”);
setSelectedSubCategoryInForm(”);
};
// Get the list of subcategories for the currently selected main category in the form
const currentSubcategories = selectedMainCategoryInForm ? mainCategoryGroups[selectedMainCategoryInForm] : [];
return (
Add New Business Listing
{/* Form-specific message display */}
{formMessage && (
{formMessage}
)}
);
});
// Wrap the App component with FirebaseProvider
export default function ProvidedApp() {
return (
);
}