# 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")
);
```
