UNPKG

firebase-admin-panel

Version:

A customizable React admin panel for managing Firebase collections.

582 lines (524 loc) 15.9 kB
# Firebase Admin Panel Package A customizable React admin panel for managing Firebase collections. This package allows you to use default components or provide your own custom components. ## Installation To install the package, run: ```bash npm install firebase-admin-panel ``` ## Usage **Note: Make sure to setup the database and storage in your firestore and update any necessary rules** # Basic Usage with Default Components In your index.js file, pass the firebaseConfig and collectionStructure to the AdminPage component. ```js import React from "react"; import ReactDOM from "react-dom"; import { AdminPage } from "firebase-admin-panel"; const firebaseConfig = { apiKey: "YOUR_API_KEY", authDomain: "YOUR_AUTH_DOMAIN", projectId: "YOUR_PROJECT_ID", storageBucket: "YOUR_STORAGE_BUCKET", messagingSenderId: "YOUR_MESSAGING_SENDER_ID", appId: "YOUR_APP_ID", }; const collectionStructure = { bookings: { endDate: "date", id: "string", name: "string", phone: "number", roomId: { type: "docRef", collection: "rooms" }, startDate: "date", }, rooms: { number: "number", type: "string", }, }; ReactDOM.render( <React.StrictMode> <AdminPage firebaseConfig={firebaseConfig} collectionStructure={collectionStructure} /> </React.StrictMode>, document.getElementById("root") ); ``` make sure to update your indedx.html to add the following line ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="theme-color" content="#000000" /> <meta name="description" content="Web site created using create-react-app" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <title>React App</title> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> <!-- add this line --> <div id="modal-root"></div> </body> </html> ``` **for the collection structure the available types are:** - number - string - date - array - { type: "docRef", collection: "collectionName" } - { type: "docRef array", collection: "collectionName" } # Usage with Custom Components You can provide your own custom components for CollectionList, DocumentTable, Modal, and DocumentForm. Below are barebones templates for each custom component. ```js import React from "react"; const CustomCollectionList = ({ onSelectCollection, collections, className = "", itemClassName = "", }) => { return ( <div className={`p-4 bg-purple-800 text-white rounded-lg ${className}`}> <h2 className="text-lg font-semibold mb-4">My Custom Collections</h2> {collections.length > 0 ? ( <ul> {collections.map((collection) => ( <li key={collection} onClick={() => onSelectCollection(collection)} className={`cursor-pointer p-2 hover:bg-purple-700 rounded-md ${itemClassName}`} > {collection} </li> ))} </ul> ) : ( <p>No collections found.</p> )} </div> ); }; export default CustomCollectionList; ``` ```js import React from "react"; import { useDocuments, deleteDocument, Timestamp } from "firebase-admin-panel"; const CustomDocumentTable = ({ collectionName, onEdit, onAdd, tableClassName = "", rowClassName = "", cellClassName = "", buttonClassName = "", schema, }) => { const { documents } = useDocuments(collectionName); const handleDelete = async (id) => { await deleteDocument(collectionName, id); }; const getColumns = () => { if (!schema) return []; return Object.keys(schema); }; const columns = getColumns(); const formatValue = (key, value) => { if (Array.isArray(value)) { return value.join(", "); } else if (schema[key] === "image") { return <img src={value} alt={key} className="h-[30px] object-cover" />; } else if (value instanceof Timestamp) { return value.toDate().toISOString().split("T")[0]; } else { return value; } }; return ( <div className="table-wrapper w-full border-collapse"> <h2 className="text-xl font-bold mb-4">{collectionName}</h2> <button onClick={() => onAdd(schema)} className={`bg-green-500 text-white py-2 px-4 rounded mb-4 ${buttonClassName}`} > Add New Document </button> <table className={`w-full table-auto ${tableClassName}`}> <thead> <tr> {columns.map((column) => ( <th key={column} className={`border px-4 py-2 text-left ${cellClassName}`} > {column} </th> ))} <th className={`px-4 py-2 w-32 text-center ${cellClassName}`}> Actions </th> </tr> </thead> <tbody> {documents.map((doc) => ( <tr key={doc.id} className={`hover:bg-gray-100 ${rowClassName}`}> {columns.map((column) => ( <td key={column} className={`border px-4 py-2 ${cellClassName}`} > {formatValue(column, doc[column])} </td> ))} <td className={`border px-4 py-2 w-60 text-center ${cellClassName}`} > <div className="flex items-center justify-center"> <button onClick={() => onEdit(doc)} className={`bg-blue-500 text-white py-1 px-2 rounded w-[50%] ${buttonClassName}`} > Edit </button> <button onClick={() => handleDelete(doc.id)} className={`bg-red-500 text-white py-1 px-2 rounded w-[50%] ${buttonClassName}`} > Delete </button> </div> </td> </tr> ))} </tbody> </table> </div> ); }; export default CustomDocumentTable; ``` ```js import React from "react"; import ReactDOM from "react-dom"; const CustomModal = ({ children, onClose }) => { return ReactDOM.createPortal( <div className="fixed inset-0 flex items-start justify-center bg-red-500 bg-opacity-50 overflow-y-scroll"> <div className="bg-white p-4 rounded-lg shadow-lg w-[30vw]"> {children} <button onClick={onClose} className="mt-4 bg-red-700 text-white py-2 px-4 rounded" > Close </button> </div> </div>, document.getElementById("modal-root") ); }; export default CustomModal; ``` ```js import React, { useState, useEffect } from "react"; import CreatableSelect from "react-select/creatable"; import { addDocument, updateDocument, getCollectionDocuments, } from "firebase-admin-panel"; import { uploadImage } from "firebase-admin-panel"; import { Timestamp } from "firebase/firestore"; const CustomDocumentForm = ({ collectionName, docToEdit, onSave, schema, formClassName = "", inputClassName = "", labelClassName = "", buttonClassName = "", }) => { const [formData, setFormData] = useState({}); const [file, setFile] = useState(null); const [docRefOptions, setDocRefOptions] = useState({}); const [errors, setErrors] = useState({}); useEffect(() => { if (docToEdit) { const initialData = { ...docToEdit }; Object.keys(initialData).forEach((key) => { if (initialData[key] instanceof Timestamp) { initialData[key] = initialData[key] .toDate() .toISOString() .split("T")[0]; } else if (Array.isArray(initialData[key])) { initialData[key] = initialData[key].map((value) => ({ label: value, value: value, })); } }); setFormData(initialData); } else { const initialData = {}; Object.keys(schema).forEach((key) => { initialData[key] = schema[key].type === "array" || schema[key].type === "docRef array" ? [] : ""; }); setFormData(initialData); } }, [docToEdit, schema]); useEffect(() => { const fetchDocRefOptions = async () => { const options = {}; for (const key in schema) { if ( schema[key].type === "docRef" || schema[key].type === "docRef array" ) { const docs = await getCollectionDocuments(schema[key].collection); options[key] = docs.map((doc) => ({ label: doc.id, value: doc.id })); } } setDocRefOptions(options); }; fetchDocRefOptions(); }, [schema]); const handleChange = (e) => { setFormData({ ...formData, [e.target.name]: e.target.value, }); }; const handleSelectChange = (selectedOptions, key) => { setFormData({ ...formData, [key]: selectedOptions, }); }; const handleFileChange = (e) => { setFile(e.target.files[0]); }; const validateForm = () => { const newErrors = {}; Object.keys(schema).forEach((key) => { if ( (!formData[key] || formData[key].length === 0) && schema[key] !== "image" ) { newErrors[key] = "This field is required"; } else if (schema[key] === "image" && !file && !docToEdit?.[key]) { newErrors[key] = "This field is required"; } }); if (Object.keys(newErrors).length > 0) { setErrors(newErrors); return false; } return true; }; function getKeyByValue(object, value) { let result = []; for (let prop in object) { if (object.hasOwnProperty(prop)) { if (object[prop] === value) result.push(prop); } } return result; } const handleSubmit = async (e) => { e.preventDefault(); if (!validateForm()) { return; } let tempDataToSave = { ...formData }; let dataToSave = tempDataToSave; let arrayTypes = getKeyByValue(schema, "array"); arrayTypes.forEach((arrayType) => { if ( tempDataToSave && arrayType && tempDataToSave[arrayType] && tempDataToSave[arrayType].length > 0 ) { tempDataToSave[arrayType]?.forEach((e, index) => { dataToSave[arrayType][index] = e.value; }); } }); if (file) { const imageUrl = await uploadImage( file, `${collectionName}/${file.name}` ); dataToSave[getKeyByValue(schema, "image")] = imageUrl; } Object.keys(dataToSave).forEach((key) => { if (schema[key] && schema[key] === "date" && dataToSave[key]) { dataToSave[key] = Timestamp.fromDate(new Date(dataToSave[key])); } else if ( (schema[key] && schema[key] === "array") || (schema[key] && schema[key].type === "docRef array") ) { dataToSave[key] = dataToSave[key].map((option) => { if (schema[key].type === "docRef array") return option.value; return option; }); } else if (schema[key] && schema[key] === "number") { dataToSave[key] = parseFloat(dataToSave[key]); } }); if (docToEdit) { await updateDocument(collectionName, docToEdit.id, dataToSave); } else { await addDocument(collectionName, dataToSave); } onSave(); }; const getInputType = (type) => { switch (type) { case "string": return "text"; case "number": return "number"; case "date": return "date"; case "image": return "file"; case "docRef": case "docRef array": return "select"; default: return "text"; } }; return ( <form onSubmit={handleSubmit} className={`mt-4 p-4 border border-gray-300 bg-white ${formClassName}`} > {Object.keys(schema).map((key) => ( <div key={key} className="mb-4"> <label className={`block font-bold ${labelClassName}`}>{key}</label> {schema[key] === "image" ? ( <input type="file" name={key} onChange={handleFileChange} className={`w-full p-2 my-2 border border-gray-300 rounded ${inputClassName}`} /> ) : schema[key] === "array" || schema[key].type === "docRef array" ? ( <CreatableSelect isMulti name={key} value={formData[key]} onChange={(selectedOptions) => handleSelectChange(selectedOptions, key) } className="basic-multi-select" classNamePrefix="select" formatCreateLabel={(inputValue) => `Add "${inputValue}"`} options={ schema[key].type === "docRef array" ? docRefOptions[key] : [] } /> ) : schema[key].type === "docRef" ? ( <select name={key} value={formData[key]} onChange={handleChange} className={`w-full p-2 my-2 border border-gray-300 rounded ${inputClassName}`} > {docRefOptions[key] && docRefOptions[key].map((option) => ( <option key={option.value} value={option.value}> {option.label} </option> ))} </select> ) : ( <input type={getInputType(schema[key])} name={key} value={formData[key]} onChange={handleChange} className={`w-full p-2 my-2 border border-gray-300 rounded ${inputClassName}`} /> )} {errors[key] && <p className="text-red-500">{errors[key]}</p>} </div> ))} <button type="submit" className={`bg-blue-500 text-white py-2 px-4 rounded text-white border-none cursor-pointer mr-2 ${buttonClassName}`} > {docToEdit ? "Update" : "Add"} Document </button> <button type="button" onClick={onSave} className={`bg-red-500 text-white py-2 px-4 rounded ml-2 text-white border-none cursor-pointer mr-2 ${buttonClassName}`} > Cancel </button> </form> ); }; export default CustomDocumentForm; ``` # index.js for example ```js import React from "react"; import ReactDOM from "react-dom"; import { AdminPage } from "firebase-admin-panel"; import CustomCollectionList from "./components/CustomCollectionList"; import CustomDocumentTable from "./components/CustomDocumentTable"; import CustomModal from "./components/CustomModal"; import CustomDocumentForm from "./components/CustomDocumentForm"; const firebaseConfig = { apiKey: "YOUR_API_KEY", authDomain: "YOUR_AUTH_DOMAIN", projectId: "YOUR_PROJECT_ID", storageBucket: "YOUR_STORAGE_BUCKET", messagingSenderId: "YOUR_MESSAGING_SENDER_ID", appId: "YOUR_APP_ID", }; const collectionStructure = { bookings: { endDate: "date", id: "string", name: "string", phone: "number", roomId: { type: "docRef", collection: "rooms" }, startDate: "date", }, rooms: { number: "number", type: "string", }, }; ReactDOM.render( <React.StrictMode> <AdminPage firebaseConfig={firebaseConfig} collectionStructure={collectionStructure} CustomCollectionList={CustomCollectionList} CustomDocumentTable={CustomDocumentTable} CustomModal={CustomModal} CustomDocumentForm={CustomDocumentForm} /> </React.StrictMode>, document.getElementById("root") ); ```