A customizable React admin panel for managing Firebase collections.
npm install firebase-admin-panelA customizable React admin panel for managing Firebase collections. This package allows you to use default components or provide your own custom components.
To install the package, run:
``bash`
npm install firebase-admin-panel
Note: Make sure to setup the database and storage in your firestore and update any necessary rules
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(
collectionStructure={collectionStructure}
/>
document.getElementById("root")
);
`
make sure to update your indedx.html to add the following line
`html
name="description"
content="Web site created using create-react-app"
/>
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 (
p-4 bg-purple-800 text-white rounded-lg ${className}}>
My Custom Collections
{collections.length > 0 ? (
{collections.map((collection) => (
key={collection}
onClick={() => onSelectCollection(collection)}
className={
cursor-pointer p-2 hover:bg-purple-700 rounded-md ${itemClassName}}
>
{collection}
))}
) : (
No collections found.
)}
);
};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
;
} else if (value instanceof Timestamp) {
return value.toDate().toISOString().split("T")[0];
} else {
return value;
}
};
return (
{collectionName}
onClick={() => onAdd(schema)}
className={bg-green-500 text-white py-2 px-4 rounded mb-4 ${buttonClassName}}
>
Add New Document
w-full table-auto ${tableClassName}}>
{columns.map((column) => (
key={column}
className={border px-4 py-2 text-left ${cellClassName}}
>
{column}
))}
px-4 py-2 w-32 text-center ${cellClassName}}>
Actions
{documents.map((doc) => (
hover:bg-gray-100 ${rowClassName}}>
{columns.map((column) => (
key={column}
className={border px-4 py-2 ${cellClassName}}
>
{formatValue(column, doc[column])}
))}
className={border px-4 py-2 w-60 text-center ${cellClassName}}
>
onClick={() => onEdit(doc)}
className={bg-blue-500 text-white py-1 px-2 rounded w-[50%] ${buttonClassName}}
>
Edit
onClick={() => handleDelete(doc.id)}
className={bg-red-500 text-white py-1 px-2 rounded w-[50%] ${buttonClassName}}
>
Delete
))}
);
};export default CustomDocumentTable;
``js
import React from "react";
import ReactDOM from "react-dom";const CustomModal = ({ children, onClose }) => {
return ReactDOM.createPortal(
{children}
onClick={onClose}
className="mt-4 bg-red-700 text-white py-2 px-4 rounded"
>
Close
,
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 (
onSubmit={handleSubmit}
className={mt-4 p-4 border border-gray-300 bg-white ${formClassName}}
>
{Object.keys(schema).map((key) => (
{schema[key] === "image" ? (
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" ? (
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" ? (
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) => (
))}
) : (
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] && {errors[key]}
}
))}
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
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
);
};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(
firebaseConfig={firebaseConfig}
collectionStructure={collectionStructure}
CustomCollectionList={CustomCollectionList}
CustomDocumentTable={CustomDocumentTable}
CustomModal={CustomModal}
CustomDocumentForm={CustomDocumentForm}
/>
,
document.getElementById("root")
);
``