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. Shop Product 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>Product Details</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');
body {
font-family: 'Poppins', sans-serif;
background: #fafafa;
margin: 0;
padding: 0;
color: #333;
line-height: 1.6;
}
.product-details-container {
width: 90%;
margin: 20px auto;
display: flex;
gap: 60px;
padding: 40px;
border-radius: 0;
position: relative;
}
.product-images {
flex: 1;
position: relative;
}
.main-image {
width: 100%;
height: 500px;
border-radius: 0;
object-fit: cover;
background: #f8f8f8;
transition: opacity 0.3s ease;
}
.thumbnails {
display: flex;
gap: 12px;
margin-top: 20px;
flex-wrap: wrap;
}
.thumbnail {
width: 70px;
height: 70px;
border-radius: 0;
cursor: pointer;
object-fit: cover;
border: 1px solid #e0e0e0;
transition: all 0.3s ease;
opacity: 0.7;
}
.thumbnail:hover,
.thumbnail.active {
border-color: #b8860b;
opacity: 1;
transform: scale(1.05);
}
.product-info {
flex: 1;
padding-top: 20px;
}
.product-info h1 {
font-size: 32px;
font-weight: 300;
letter-spacing: 1px;
margin-bottom: 10px;
color: #1a1a1a;
}
.price {
color: #b8860b;
font-size: 28px;
font-weight: 400;
margin-top: 0;
margin-bottom: 25px;
letter-spacing: 0.5px;
}
.product-info p {
font-size: 15px;
color: #666;
margin-bottom: 25px;
}
.product-info h3 {
font-size: 18px;
font-weight: 500;
color: #1a1a1a;
margin-bottom: 15px;
letter-spacing: 0.5px;
padding-bottom: 8px;
}
.product-info ul {
padding-left: 20px;
margin-bottom: 30px;
}
.product-info li {
margin-bottom: 8px;
color: #666;
font-size: 15px;
}
.product-info li:before {
content: "•";
color: #b8860b;
font-weight: bold;
display: inline-block;
width: 1em;
margin-left: -1em;
}
#productSpecifications p {
margin-bottom: 10px;
font-size: 15px;
}
#productSpecifications strong {
color: #1a1a1a;
font-weight: 500;
min-width: 120px;
display: inline-block;
}
.add-to-cart-btn {
padding: 15px 20px;
background: #1a1a1a;
color: white;
border-radius: 0;
border: none;
cursor: pointer;
margin-top: 30px;
font-size: 14px;
font-weight: 500;
letter-spacing: 1px;
text-transform: uppercase;
transition: all 0.3s ease;
width: 250px;
}
.add-to-cart-btn:hover {
background: #b8860b;
transform: translateY(-2px);
}
.product-info h3:after {
content: '';
display: block;
width: 40px;
height: 1px;
background: #b8860b;
margin-top: 8px;
}
#productName:empty:before {
content: "Loading...";
color: #999;
}
/* Related Products */
.container-product-details {
width: 90%;
max-width: 1200px;
margin: 0 auto;
padding: 15px;
}
.products-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 2rem;
}
.product-card {
background-color: #ffffff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.product-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
.product-card img {
width: 100%;
height: 200px;
object-fit: cover;
}
.product-card-content {
padding: 1.5rem;
}
.product-card h3 {
font-size: 1.2rem;
margin-bottom: 0.5rem;
}
.product-card-price {
color:#d4af37;
font-weight: 600;
margin-bottom: 1rem;
}
.product-card .btn {
width: 100%;
}
.btn-secondary {
display: inline-block;
padding: 15px 20px;
width: 250px;
border: none;
border-radius: 4px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
cursor: pointer;
transition: all 0.3s ease;
background-color: #fff;
color: #121212;
border: 2px solid #b8860b;
}
@media (max-width:768px) {
.product-details-container {
flex-direction: column;
width: 98%;
}
}
</style>
</head>
<body>
<div class="product-details-container">
<!-- Images -->
<div class="product-images">
<img id="mainImage" class="main-image" src="" alt="Product Image">
<div class="thumbnails" id="thumbnails">
<!-- Thumbnails injected here -->
</div>
</div>
<!-- Info -->
<div class="product-info">
<h1 id="productName">Loading...</h1>
<p class="price">Ksh <span id="productPrice" style="color: #b8860b;font-size: 1.6em;">0</span></p>
<h3>About this Product</h3>
<p id="productDescription"></p>
<h3>Specifications</h3>
<div id="productSpecifications"></div>
<button class="btn btn-secondary" id="buyNow">
<i class="fab fa-whatsapp"></i> Order Now
</button>
<button class="add-to-cart-btn">ADD TO CART</button>
<br>
</div>
</div>
<!-- Related Products -->
<div class="products-grid" id="relatedProducts" style="padding:20px"></div>
<script type="module">
import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-app.js";
import { getFirestore, doc, getDoc, collection, getDocs } 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 productId = new URLSearchParams(window.location.search).get("id");
function shuffleArray(array) {
return array.sort(() => Math.random() - 0.5);
}
async function loadProduct() {
if (!productId) {
document.body.innerHTML = "<h2>⚠ Product ID missing in URL.</h2>";
return;
}
const docRef = doc(db, "products", productId);
const docSnap = await getDoc(docRef);
if (!docSnap.exists()) {
document.body.innerHTML = "<h2>⚠ Product not found.</h2>";
return;
}
const product = docSnap.data();
document.getElementById("productName").textContent = product.name ?? "Unnamed Product";
document.getElementById("productPrice").textContent = product.price ?? "0";
document.getElementById("productDescription").textContent = product.productDescription || "No description available.";
const mainImage = document.getElementById("mainImage");
mainImage.src = product.mainImage || "";
const thumbnailsDiv = document.getElementById("thumbnails");
thumbnailsDiv.innerHTML = "";
if (product.supportImages?.length > 0) {
product.supportImages.forEach(imgUrl => {
const img = document.createElement("img");
img.src = imgUrl;
img.classList.add("thumbnail");
img.onclick = () => (mainImage.src = imgUrl);
thumbnailsDiv.appendChild(img);
});
}
const specsDiv = document.getElementById("productSpecifications");
specsDiv.innerHTML = "";
specsDiv.style.display = "flex";
specsDiv.style.flexWrap = "wrap";
specsDiv.style.gap = "15px";
if (product.specifications && typeof product.specifications === "object") {
Object.entries(product.specifications).forEach(([key, value]) => {
const specItem = document.createElement("div");
specItem.style.flex = "1 1 200px";
specItem.style.background = "#f4f4f4";
specItem.style.padding = "10px 12px";
specItem.style.borderRadius = "8px";
specItem.style.boxShadow = "0 2px 6px rgba(0,0,0,0.1)";
specItem.innerHTML = `<strong style="text-transform: capitalize;">${key}:</strong> ${value}`;
specsDiv.appendChild(specItem);
});
}
loadRelatedProducts();
}
async function loadRelatedProducts() {
const relatedContainer = document.getElementById("relatedProducts");
if (!relatedContainer) return;
const querySnapshot = await getDocs(collection(db, "products"));
let products = [];
querySnapshot.forEach(doc => {
if (doc.id !== productId) {
products.push({ id: doc.id, ...doc.data() });
}
});
const selected = shuffleArray(products).slice(0, 4);
relatedContainer.innerHTML = "";
selected.forEach(product => {
relatedContainer.innerHTML += `
<div class="product-card">
<img src="${product.mainImage}" alt="${product.name}">
<div class="product-card-content">
<h3>${product.name}</h3>
<p class="product-card-price">Ksh ${product.price}</p>
<button class="btn btn-secondary" onclick="window.location.href='product.html?id=${product.id}'">
View Details
</button>
</div>
</div>
`;
});
}
loadProduct();
</script>
</body>
</html>
All code blocks above are fully copyable. Follow the Firebase and Cloudinary setup carefully to ensure uploads work correctly.