1. Prerequisites
Before starting, make sure you have:
- Basic knowledge of HTML, CSS, and JavaScript.
- A Firebase project with Firestore enabled.
- A Cloudinary account with an unsigned upload preset.
- Local development environment (e.g., Live Server).
2. Firebase Setup
Steps to configure Firebase Firestore:
- Go to Firebase Console and create a new project.
- 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"
};
- Enable Cloud Firestore in test mode to allow writing data during development.
3. Cloudinary Setup
Configure Cloudinary to store product images:
- Create a Cloudinary account at Cloudinary.
- Go to Settings → Upload → Upload Presets → Add Upload Preset.
- Set Signing Mode to Unsigned.
- Note your Cloud name and Upload Preset name:
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
- Add more fields by updating the HTML form and JS
specificationsobject. - Switch Cloudinary to Firebase Storage if preferred.
- Add form validation for production use.
- Style the form by updating CSS variables or custom classes.
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.