firebase-admin-panel
Version:
A customizable React admin panel for managing Firebase collections.
582 lines (524 loc) • 15.9 kB
Markdown
A 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(
<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" }
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.value,
});
};
const handleSelectChange = (selectedOptions, key) => {
setFormData({
...formData,
[]: 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;
```
```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")
);
```