EcomLib Product Upload Feature Documentation


1. Prerequisites

Before starting, make sure you have:

2. Firebase Setup

Steps to configure Firebase Firestore:

  1. Go to Firebase Console and create a new project.
  2. Register a web app in your project and copy the Firebase config object:
const firebaseConfig = {
    apiKey: "AIzaSyCUad2MwdykedKqSWG7AOx9iIN5ePYxDg8",
    authDomain: "ecommerce-products-573ce.firebaseapp.com",
    projectId: "ecommerce-products-573ce",
    storageBucket: "ecommerce-products-573ce.firebasestorage.app",
    messagingSenderId: "875506405938",
    appId: "1:875506405938:web:815042d0daf81941ae622b",
    measurementId: "G-LTEMTLNB9E"
    };
  1. Enable Cloud Firestore in test mode to allow writing data during development.

3. Cloudinary Setup

Configure Cloudinary to store product images:

const CLOUDINARY_URL = "https://api.cloudinary.com/v1_1/dxtkxftfg/image/upload";
    const CLOUDINARY_PRESET = "YOUR_UPLOAD_PRESET";
Unsigned presets are required for client-side uploads to work correctly.

4. HTML Form Template

This form includes basic product info, image uploads, specifications, and features. Copy and paste it to start:

<form id="productForm">
    <h2>Add a New Product</h2>

    <label for="productName">Product Name</label>
    <input type="text" id="productName" required>

    <label for="brand">Brand</label>
    <input type="text" id="brand" required>

    <label for="productDescription">Product Description</label>
    <input type="text" id="productDescription" required>

    <label for="gender">Gender</label>
    <select id="gender" required>
        <option value="">Select Gender</option>
        <option value="Men">Men</option>
        <option value="Women">Women</option>
        <option value="Unisex">Unisex</option>
    </select>

    <label for="category">Category</label>
    <select id="category">
        <option value="">Select Category</option>
        <option value="Analog">Analog</option>
        <option value="Digital">Digital</option>
        <option value="Smart">Smart</option>
    </select>

    <label for="productPrice">Price (Ksh)</label>
    <input type="number" id="productPrice" required>

    <label for="mainImage">Main Image</label>
    <input type="file" id="mainImage" accept="image/*" required>

    <label for="supportImages">Supporting Images</label>
    <input type="file" id="supportImages" accept="image/*" multiple>

    <h3>Specifications</h3>
    <label for="dialShape">Dial Shape</label>
    <input type="text" id="dialShape">
    <label for="dialColor">Dial Color</label>
    <input type="text" id="dialColor">

    <h3>Features</h3>
    <label for="features">Features (comma separated)</label>
    <input type="text" id="features">

    <button type="submit">Upload Product</button>
    <div id="statusMsg"></div>
    </form>

5. CSS Styling

Basic styling for the form (optional):

body {
    font-family: Arial, sans-serif;
    background: #f9f9f9;
    padding: 40px;
    }

    form {
    max-width: 700px;
    margin: auto;
    background: #fff;
    padding: 30px;
    border-radius: 10px;
    box-shadow: 0 6px 15px rgba(0,0,0,0.1);
    }

    label { display: block; margin-top: 15px; }
    input, select { width: 100%; padding: 10px; margin-top: 5px; }
    button { margin-top: 20px; padding: 12px; background: #d4af37; border: none; cursor: pointer; font-weight: bold; }
    #statusMsg { margin-top: 15px; font-weight: bold; }

6. JavaScript Upload Logic

This script handles uploading images to Cloudinary and saving product data to Firebase:

<script type="module">
    import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-app.js";
    import { getFirestore, collection, addDoc, serverTimestamp } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-firestore.js";

    const firebaseConfig = { /* Your Firebase config */ };
    const app = initializeApp(firebaseConfig);
    const db = getFirestore(app);

    const CLOUDINARY_URL = "https://api.cloudinary.com/v1_1/YOUR_CLOUD_NAME/image/upload";
    const CLOUDINARY_PRESET = "YOUR_UPLOAD_PRESET";

    async function uploadToCloudinary(file) {
    const formData = new FormData();
    formData.append("file", file);
    formData.append("upload_preset", CLOUDINARY_PRESET);
    const res = await fetch(CLOUDINARY_URL, { method: "POST", body: formData });
    const data = await res.json();
    return data.secure_url;
    }

    document.getElementById("productForm").addEventListener("submit", async (e) => {
    e.preventDefault();
    const status = document.getElementById("statusMsg");
    status.textContent = "Uploading...";

    try {
        const productName = document.getElementById("productName").value.trim();
        const brand = document.getElementById("brand").value.trim();
        const productDescription = document.getElementById("productDescription").value.trim();
        const gender = document.getElementById("gender").value;
        const category = document.getElementById("category").value;
        const price = parseFloat(document.getElementById("productPrice").value);
        const features = document.getElementById("features").value.split(",").map(f => f.trim()).filter(f => f);

        const specifications = {
        dialShape: document.getElementById("dialShape").value.trim(),
        dialColor: document.getElementById("dialColor").value.trim()
        };

        const mainFile = document.getElementById("mainImage").files[0];
        const mainImageURL = await uploadToCloudinary(mainFile);

        const supportFiles = document.getElementById("supportImages").files;
        const supportImageURLs = [];
        for (let file of supportFiles) {
        const url = await uploadToCloudinary(file);
        supportImageURLs.push(url);
        }

        await addDoc(collection(db, "products"), {
        name: productName,
        brand,
        productDescription,
        gender,
        category,
        price,
        mainImage: mainImageURL,
        supportImages: supportImageURLs,
        specifications,
        features,
        createdAt: serverTimestamp()
        });

        status.textContent = "✅ Product successfully uploaded!";
        e.target.reset();
    } catch(err) {
        console.error(err);
        status.textContent = "❌ Error: " + err.message;
    }
    });
    </script>

7. Customization Tips

8. Final Template

Combine the HTML, CSS, and JS above into a single HTML file for immediate use in development. This template is ready to drop into Live Server and start uploading products.

<!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Add Product | Imperial Nova Admin</title>
    <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&display=swap" rel="stylesheet">
    <style>
    :root {
        --primary: #d4af37;
        --primary-dark: #b8941f;
        --primary-light: #f4e4a6;
        --dark: #121212;
        --light: #f9f9f9;
        --white: #ffffff;
        --gray: #e0e0e0;
        --gray-dark: #a0a0a0;
        --success: #28a745;
        --border-radius: 12px;
        --shadow: 0 8px 24px rgba(0,0,0,0.1);
        --transition: all 0.3s ease;
    }
    * { box-sizing: border-box; }
    body {
        font-family: 'Poppins', sans-serif;
        background: var(--light);
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
        padding: 40px 20px;
        min-height: 100vh;
        color: var(--dark);
    }
    form {
        background: var(--white);
        width: 100%;
        max-width: 800px;
        padding: 40px;
        border-radius: var(--border-radius);
        box-shadow: var(--shadow);
        position: relative;
    }
    form::before {
        content: '';
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 5px;
        background: linear-gradient(90deg, var(--primary), var(--primary-light));
    }
    h2 { text-align: center; margin-bottom: 30px; color: var(--dark); font-size: 28px; font-weight: 600; position: relative; padding-bottom: 15px; }
    h2::after { content: ''; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 80px; height: 3px; background: var(--primary); border-radius: 2px; }
    h3 { margin: 30px 0 20px; color: var(--dark); font-size: 20px; font-weight: 600; padding-bottom: 10px; border-bottom: 1px solid var(--gray); }
    label { font-weight: 500; display: block; margin: 15px 0 8px; color: var(--dark); font-size: 14px; }
    input, select, textarea { width: 100%; padding: 14px 16px; border: 1px solid var(--gray); border-radius: 8px; font-family: 'Poppins', sans-serif; font-size: 14px; transition: var(--transition); background-color: var(--white); }
    input:focus, select:focus, textarea:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(212, 175, 55, 0.2); }
    textarea { resize: vertical; min-height: 100px; }
    .btn { margin-top: 30px; padding: 16px; background: var(--primary); color: var(--dark); font-weight: 600; border: none; border-radius: 8px; cursor: pointer; width: 100%; font-size: 16px; transition: var(--transition); letter-spacing: 0.5px; text-transform: uppercase; box-shadow: 0 4px 8px rgba(212, 175, 55, 0.3); }
    .btn:hover { background: var(--primary-dark); transform: translateY(-2px); box-shadow: 0 6px 12px rgba(212, 175, 55, 0.4); }
    .success { color: var(--success); text-align: center; margin-top: 15px; font-weight: 500; padding: 10px; border-radius: 6px; background-color: rgba(40, 167, 69, 0.1); }
    .spec-group { display: flex; flex-wrap: wrap; gap: 20px; }
    .spec-item { flex: 1 1 calc(50% - 20px); min-width: 200px; }
    .file-input-container { position: relative; overflow: hidden; display: inline-block; width: 100%; }
    .file-input-container input[type=file] { position: absolute; left: 0; top: 0; opacity: 0; width: 100%; height: 100%; cursor: pointer; }
    .file-input-label { display: block; padding: 14px 16px; background: var(--white); border: 1px solid var(--gray); border-radius: 8px; text-align: center; cursor: pointer; transition: var(--transition); color: var(--gray-dark); }
    .file-input-label:hover { border-color: var(--primary); color: var(--dark); }
    .file-name { margin-top: 5px; font-size: 12px; color: var(--gray-dark); }
    @media (max-width: 768px) { .spec-item { flex: 1 1 100%; } }
    </style>
    </head>
    <body>

    <form id="productForm">
        <h2>Add a New Watch</h2>

        <!-- Basic Info -->
        <label for="productName">Product Name</label>
        <input type="text" id="productName" required>

        <label for="brand">Brand</label>
        <input type="text" id="brand" required>

        <label for="productDescription">Product Description</label>
        <input type="text" id="productDescription" required>

        <label for="gender">Gender</label>
        <select id="gender" required>
            <option value="">Select Gender</option>
            <option value="Men">Men</option>
            <option value="Women">Women</option>
            <option value="Unisex">Unisex</option>
        </select>

        <label for="saleCategory">Sale Category</label>
        <select id="saleCategory" required>
            <option value="top">Top Collection</option>
            <option value="other">Other</option>
        </select>

        <label for="category">Category</label>
        <select id="category" required>
            <option value="">Select Category</option>
            <option value="Analog">Analog</option>
            <option value="Digital">Digital</option>
            <option value="Smart">Smart</option>
            <option value="Quartz">Quartz</option>
            <option value="Luxury">Luxury</option>
            <option value="Sports">Sports</option>
        </select>

        <label for="productPrice">Price (Ksh)</label>
        <input type="number" id="productPrice" required>

        <label for="mainImage">Main Image</label>
        <div class="file-input-container">
            <input type="file" id="mainImage" accept="image/*" required>
            <div class="file-input-label">Choose Main Image</div>
        </div>
        <div class="file-name" id="mainImageName"></div>

        <label for="supportImages">Supporting Images (up to 4)</label>
        <div class="file-input-container">
            <input type="file" id="supportImages" accept="image/*" multiple>
            <div class="file-input-label">Choose Supporting Images</div>
        </div>
        <div class="file-name" id="supportImagesName"></div>

        <!-- Specifications -->
        <h3>Specifications</h3>
        <div class="spec-group">
            <div class="spec-item">
                <label for="dialShape">Dial Shape</label>
                <input type="text" id="dialShape">
            </div>
            <div class="spec-item">
                <label for="dialSize">Dial Size / Diameter</label>
                <input type="text" id="dialSize">
            </div>
            <!-- ... include all other spec fields similarly ... -->
        </div>

        <label for="features">Features (comma separated)</label>
        <input type="text" id="features" placeholder="Chronograph, Date Display, Luminous Hands, Waterproof, etc.">

        <button type="submit" class="btn">Upload Product</button>
        <div id="statusMsg" class="success"></div>
    </form>

    <script type="module">
    // Firebase imports
    import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-app.js";
    import { getFirestore, collection, addDoc, serverTimestamp } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-firestore.js";

    const firebaseConfig = {
        apiKey: "AIzaSyCUad2MwdykedKqSWG7AOx9iIN5ePYxDg8",
        authDomain: "ecommerce-products-573ce.firebaseapp.com",
        projectId: "ecommerce-products-573ce",
        storageBucket: "ecommerce-products-573ce.firebasestorage.app",
        messagingSenderId: "875506405938",
        appId: "1:875506405938:web:815042d0daf81941ae622b",
        measurementId: "G-LTEMTLNB9E"
    };

    const app = initializeApp(firebaseConfig);
    const db = getFirestore(app);

    const CLOUDINARY_URL = "https://api.cloudinary.com/v1_1/dxtkxftfg/image/upload";
    const CLOUDINARY_PRESET = "ecommerce-products";

    async function uploadToCloudinary(file) {
        const formData = new FormData();
        formData.append("file", file);
        formData.append("upload_preset", CLOUDINARY_PRESET);
        const res = await fetch(CLOUDINARY_URL, { method: "POST", body: formData });
        const data = await res.json();
        return data.secure_url;
    }

    document.getElementById('mainImage').addEventListener('change', e => {
        const fileName = e.target.files[0] ? e.target.files[0].name : 'No file chosen';
        document.getElementById('mainImageName').textContent = fileName;
    });

    document.getElementById('supportImages').addEventListener('change', e => {
        const fileCount = e.target.files.length;
        let fileName = 'No files chosen';
        if(fileCount === 1) fileName = e.target.files[0].name;
        else if(fileCount > 1) fileName = `${fileCount} files selected`;
        document.getElementById('supportImagesName').textContent = fileName;
    });

    document.getElementById("productForm").addEventListener("submit", async e => {
        e.preventDefault();
        const status = document.getElementById("statusMsg");
        status.textContent = "Uploading... Please wait.";

        try {
            const productName = document.getElementById("productName").value.trim();
            const productDescription = document.getElementById("productDescription").value.trim();
            const brand = document.getElementById("brand").value.trim();
            const gender = document.getElementById("gender").value;
            const category = document.getElementById("category").value;
            const saleCategory = document.getElementById("saleCategory").value;
            const price = parseFloat(document.getElementById("productPrice").value);
            const features = document.getElementById("features").value.split(",").map(f=>f.trim()).filter(f=>f);

            const specifications = {
                dialShape: document.getElementById("dialShape").value.trim(),
                dialSize: document.getElementById("dialSize").value.trim(),
                dialColor: document.getElementById("dialColor").value.trim()
                // ... all other specs ...
            };

            const mainFile = document.getElementById("mainImage").files[0];
            const mainImageURL = await uploadToCloudinary(mainFile);

            const supportFiles = document.getElementById("supportImages").files;
            const supportImageURLs = [];
            for (let file of supportFiles) supportImageURLs.push(await uploadToCloudinary(file));

            await addDoc(collection(db, "products"), {
                name: productName,
                productDescription,
                brand,
                gender,
                category,
                saleCategory,
                price,
                mainImage: mainImageURL,
                supportImages: supportImageURLs,
                specifications,
                features,
                createdAt: serverTimestamp()
            });

            status.textContent = "✅ Product successfully uploaded!";
            e.target.reset();
            document.getElementById('mainImageName').textContent = '';
            document.getElementById('supportImagesName').textContent = '';
        } catch(err) {
            console.error(err);
            status.textContent = `❌ Error: ${err.message}`;
        }
    });
    </script>

    </body>
    </html>
    
All code blocks above are fully copyable. Follow the Firebase and Cloudinary setup carefully to ensure uploads work correctly.