Skip to content

Commit 7f2ab36

Browse files
authored
Merge pull request #6 from BeyteFlow/feature/productDetail
connect with firebase
2 parents 4751d0e + 2138beb commit 7f2ab36

9 files changed

Lines changed: 1150 additions & 99 deletions

File tree

package-lock.json

Lines changed: 990 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"@tailwindcss/vite": "^4.1.18",
1414
"@tanstack/react-query": "^5.90.20",
1515
"clsx": "^2.1.1",
16+
"firebase": "^12.8.0",
1617
"lucide-react": "^0.563.0",
1718
"react": "^19.2.4",
1819
"react-dom": "^19.2.0",

src/lib/.gitkeep renamed to src/features/ProductDetail/hooks/useProductDetail.ts

File renamed without changes.

src/features/ProductDetail/types/index.ts

Whitespace-only changes.
Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,65 @@
1-
import React, {useMemo} from "react";
1+
import React, { useMemo, useEffect } from "react";
22
import { useProductStore } from "../../../../store/product.store";
3+
import { collection, onSnapshot, query } from "firebase/firestore";
4+
import { db } from "../../../../lib/firebase";
35
import ProductToolbar from "../ProductToolbar/ProductToolbar";
46
import ProductGrid from "../ProductGrid/ProductGrid";
5-
import Pagination from "../Pagination/Pagination";
67
import ProductFilters from "../ProductFilters/ProductFilters";
7-
import{ MOCK_PRODUCTS} from "../../services/MockProduct"
8+
import type{ Product } from "../../../../store/product.store";
9+
810
const ProductsPage: React.FC = () => {
9-
const { searchQuery, sortBy, selectedCategory } = useProductStore();
11+
const { products, setProducts, searchQuery, sortBy, selectedCategory } = useProductStore();
12+
13+
useEffect(() => {
14+
// Listen to Firebase "products" collection
15+
const q = query(collection(db, "products"));
16+
const unsubscribe = onSnapshot(q, (snapshot) => {
17+
const liveData = snapshot.docs.map(doc => ({
18+
id: doc.id,
19+
...doc.data()
20+
})) as Product[];
21+
setProducts(liveData);
22+
});
23+
return () => unsubscribe();
24+
}, [setProducts]);
1025

1126
const filteredProducts = useMemo(() => {
12-
let result = [...MOCK_PRODUCTS];
27+
let result = [...products];
1328
if (searchQuery) result = result.filter(p => p.name.toLowerCase().includes(searchQuery.toLowerCase()));
1429
if (selectedCategory !== 'All') result = result.filter(p => p.category === selectedCategory);
30+
1531
if (sortBy === 'price-asc') result.sort((a, b) => a.price - b.price);
1632
if (sortBy === 'price-desc') result.sort((a, b) => b.price - a.price);
1733
return result;
18-
}, [searchQuery, sortBy, selectedCategory]);
34+
}, [products, searchQuery, sortBy, selectedCategory]);
1935

2036
return (
21-
<div className="min-h-screen bg-[#F6F4F1] selection:bg-[#111111] selection:text-white px-6 py-12 md:py-24">
37+
<div className="min-h-screen bg-[#F6F4F1] px-6 py-12 md:py-24">
2238
<div className="max-w-7xl mx-auto">
23-
24-
{/* Simple Page Heading */}
2539
<div className="mb-20 text-center md:text-left">
26-
<h1 className="text-6xl md:text-8xl font-black text-[#111111] tracking-tight leading-none">
27-
LUXE<span className="text-[#8F8F8F]">CASE</span>
40+
<h1 className="text-6xl md:text-8xl font-black text-[#111111] tracking-tight">
41+
ZAYQ<span className="text-[#8F8F8F]">CASE</span>
2842
</h1>
2943
<p className="mt-4 text-[#8F8F8F] font-bold uppercase tracking-[0.4em] text-xs">Essential Protection Series</p>
3044
</div>
3145

3246
<ProductToolbar />
3347

34-
<div className="flex gap-20">
35-
<ProductFilters />
48+
<div className="flex flex-col md:flex-row gap-10 lg:gap-20">
49+
<div className="w-full md:w-64">
50+
<ProductFilters />
51+
</div>
3652
<div className="flex-1">
37-
<ProductGrid products={filteredProducts} />
38-
<Pagination />
53+
{products.length === 0 ? (
54+
<div className="flex justify-center py-20 animate-pulse text-gray-400">Loading live collection...</div>
55+
) : (
56+
<ProductGrid products={filteredProducts} />
57+
)}
3958
</div>
4059
</div>
41-
4260
</div>
4361
</div>
4462
);
4563
};
64+
4665
export default ProductsPage;
Lines changed: 57 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,70 @@
11
import React from "react";
2-
import { Apple } from "lucide-react"
32
import type { Product } from "../../../../store/product.store"
3+
44
const ProductCard: React.FC<{ product: Product }> = ({ product }) => {
5+
const isComingSoon = product.status === 'coming-soon';
6+
const isOutOfStock = product.status === 'out-of-stock';
7+
8+
const handleBuyNow = () => {
9+
if (isComingSoon || isOutOfStock) return;
10+
const message = encodeURIComponent(`Hi ZAYQ! I'm interested in the ${product.name} (₹${product.price}). Is it in stock?`);
11+
window.open(`https://wa.me/91XXXXXXXXXX?text=${message}`, '_blank'); // Replace with your WhatsApp number
12+
};
13+
514
return (
6-
<div className="flex flex-col group cursor-pointer">
7-
<div className="relative aspect-4/5 rounded-3xl bg-[#E6E3DF] flex items-center justify-center overflow-hidden transition-all duration-700 ease-in-out group-hover:bg-[#EFECE8]">
8-
<div
9-
className="relative w-32 h-64 sm:w-40 sm:h-80 rounded-[2.8rem] shadow-2xl border-[6px] border-[#1C1C1C] flex flex-col items-center p-2 transition-transform duration-500 ease-out group-hover:scale-105"
10-
style={{
11-
backgroundColor: product.colorHex,
12-
backgroundImage: product.type === 'clear' ? 'linear-gradient(135deg, rgba(255,255,255,0.6) 0%, rgba(255,255,255,0.1) 100%)' : 'none',
13-
}}
14-
>
15-
<div className="absolute top-5 left-5 w-14 h-14 bg-black/10 rounded-xl p-1.5 grid grid-cols-2 gap-1">
16-
<div className="w-5 h-5 rounded-full bg-black/30"></div>
17-
<div className="w-5 h-5 rounded-full bg-black/30"></div>
18-
<div className="w-5 h-5 rounded-full bg-black/30 col-span-2 mx-auto"></div>
15+
<div className="flex flex-col group">
16+
{/* Image Wrapper */}
17+
<div className="relative aspect-4/5 rounded-2xl bg-[#E6E3DF] overflow-hidden transition-all duration-500 group-hover:bg-[#EFECE8]">
18+
19+
{/* Status Badge */}
20+
{(isComingSoon || isOutOfStock) && (
21+
<div className="absolute top-4 left-4 z-20 px-3 py-1 bg-white/90 backdrop-blur-md rounded-full shadow-sm">
22+
<p className="text-[9px] font-black uppercase tracking-widest text-black">
23+
{isComingSoon ? 'Coming Soon' : 'Sold Out'}
24+
</p>
1925
</div>
20-
{product.type === 'clear' && <div className="mt-32 opacity-10"><Apple size={40} /></div>}
26+
)}
27+
28+
{/* Actual Product Image */}
29+
<div className={`w-full h-full transition-all duration-700 ease-in-out ${!isComingSoon && 'group-hover:scale-110'} ${isComingSoon ? 'blur-xl opacity-50' : 'opacity-100'}`}>
30+
{product.imageUrl ? (
31+
<img
32+
src={product.imageUrl}
33+
alt={product.name}
34+
className="w-full h-full object-cover"
35+
/>
36+
) : (
37+
<div className="w-full h-full flex items-center justify-center text-gray-400 text-xs">
38+
No Image Available
39+
</div>
40+
)}
2141
</div>
22-
<div className="absolute bottom-8 w-32 h-4 bg-black/5 blur-xl rounded-full opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
42+
43+
{/* Hover Overlay for In-Stock items */}
44+
{!isComingSoon && !isOutOfStock && (
45+
<div className="absolute inset-0 bg-black/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
46+
)}
2347
</div>
24-
<div className="mt-6 flex flex-col items-center text-center">
25-
<h3 className="text-lg font-bold text-[#111111]">{product.name}</h3>
26-
<p className="text-[#8F8F8F] text-sm font-medium">{product.category}</p>
27-
<p className="text-[#111111] font-semibold mt-1">{product.price.toLocaleString()}</p>
28-
<button className="mt-5 w-full max-w-40 py-3 bg-[#111111] text-white text-[10px] font-bold uppercase tracking-[0.2em] rounded-full transition-all duration-300 hover:bg-[#2A2A2A] hover:scale-[1.02]">
29-
Add to Cart
48+
49+
{/* Details Section */}
50+
<div className="mt-5 flex flex-col items-center text-center">
51+
<h3 className="text-base font-bold text-[#111111] tracking-tight">{product.name}</h3>
52+
<p className="text-[#8F8F8F] text-xs font-medium uppercase tracking-wider mt-1">{product.category}</p>
53+
<p className="text-[#111111] font-semibold mt-2">{product.price.toLocaleString()}</p>
54+
55+
<button
56+
onClick={handleBuyNow}
57+
disabled={isComingSoon || isOutOfStock}
58+
className={`mt-4 w-full py-3 text-[10px] font-bold uppercase tracking-[0.2em] rounded-xl transition-all duration-300
59+
${isComingSoon || isOutOfStock
60+
? 'bg-gray-200 text-gray-400 cursor-not-allowed'
61+
: 'bg-[#111111] text-white hover:bg-black active:scale-95'}`}
62+
>
63+
{isComingSoon ? 'Coming Soon' : isOutOfStock ? 'Sold Out' : 'Order via WhatsApp'}
3064
</button>
3165
</div>
3266
</div>
3367
);
3468
};
69+
3570
export default ProductCard;

src/features/products/services/MockProduct.tsx

Lines changed: 0 additions & 11 deletions
This file was deleted.

src/lib/firebase.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Import the functions you need from the SDKs you need
2+
import { initializeApp } from "firebase/app";
3+
import { getFirestore } from "firebase/firestore";
4+
// TODO: Add SDKs for Firebase products that you want to use
5+
// https://firebase.google.com/docs/web/setup#available-libraries
6+
7+
// Your web app's Firebase configuration
8+
const firebaseConfig = {
9+
apiKey: "AIzaSyBeWI2D-Dk9X55w6is0eBoYZRSNPLyXHY4",
10+
authDomain: "zayq-1b3ee.firebaseapp.com",
11+
projectId: "zayq-1b3ee",
12+
storageBucket: "zayq-1b3ee.firebasestorage.app",
13+
messagingSenderId: "982781196414",
14+
appId: "1:982781196414:web:69841f4c02c35d8cc5c02f"
15+
};
16+
17+
// Initialize Firebase
18+
const app = initializeApp(firebaseConfig);
19+
export const db = getFirestore(app);
20+
21+
export default app;

src/store/product.store.ts

Lines changed: 46 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,64 @@
11
import { create } from 'zustand'
22

3-
// =====================
4-
// Types
5-
// =====================
6-
73
export interface Product {
8-
id: number
9-
name: string
10-
price: number
11-
category: string
12-
colorHex: string
13-
type: 'matte' | 'clear' | 'leather'
4+
id: string; // Changed to string for Firebase compatibility
5+
name: string;
6+
price: number;
7+
category: string;
8+
imageUrl: string; // Added for your product images
9+
status: 'in-stock' | 'out-of-stock' | 'coming-soon'; // PRD Section 9
10+
type: 'matte' | 'clear' | 'leather';
11+
isFeatured?: boolean;
1412
}
1513

1614
type SortBy = 'featured' | 'price-asc' | 'price-desc'
1715

1816
interface ProductState {
19-
// data
20-
products: Product[]
21-
22-
// ui state
23-
searchQuery: string
24-
sortBy: SortBy
25-
selectedCategory: string
26-
currentPage: number
17+
products: Product[];
18+
searchQuery: string;
19+
sortBy: SortBy;
20+
selectedCategory: string;
21+
currentPage: number;
2722

28-
// actions
29-
setSearchQuery: (q: string) => void
30-
setSortBy: (s: SortBy) => void
31-
setSelectedCategory: (c: string) => void
32-
setCurrentPage: (p: number) => void
33-
setProducts: (products: Product[]) => void
23+
// Actions
24+
setProducts: (products: Product[]) => void;
25+
setSearchQuery: (q: string) => void;
26+
setSortBy: (s: SortBy) => void;
27+
setSelectedCategory: (c: string) => void;
28+
setCurrentPage: (p: number) => void;
29+
30+
// Getter for filtered products
31+
getFilteredProducts: () => Product[];
3432
}
3533

36-
// =====================
37-
// Store
38-
// =====================
39-
40-
export const useProductStore = create<ProductState>((set) => ({
34+
export const useProductStore = create<ProductState>((set, get) => ({
4135
products: [],
42-
4336
searchQuery: '',
4437
sortBy: 'featured',
4538
selectedCategory: 'All',
4639
currentPage: 1,
4740

48-
setSearchQuery: (q) =>
49-
set({ searchQuery: q, currentPage: 1 }),
50-
51-
setSortBy: (s) =>
52-
set({ sortBy: s }),
53-
54-
setSelectedCategory: (c) =>
55-
set({ selectedCategory: c, currentPage: 1 }),
56-
57-
setCurrentPage: (p) =>
58-
set({ currentPage: p }),
41+
setProducts: (products) => set({ products }),
42+
setSearchQuery: (q) => set({ searchQuery: q, currentPage: 1 }),
43+
setSortBy: (s) => set({ sortBy: s }),
44+
setSelectedCategory: (c) => set({ selectedCategory: c, currentPage: 1 }),
45+
setCurrentPage: (p) => set({ currentPage: p }),
5946

60-
setProducts: (products) =>
61-
set({ products }),
62-
}))
47+
// Logic to filter and sort products in one place
48+
getFilteredProducts: () => {
49+
const { products, searchQuery, selectedCategory, sortBy } = get();
50+
51+
return products
52+
.filter((p) => {
53+
const matchesSearch = p.name.toLowerCase().includes(searchQuery.toLowerCase());
54+
const matchesCategory = selectedCategory === 'All' || p.category === selectedCategory;
55+
return matchesSearch && matchesCategory;
56+
})
57+
.sort((a, b) => {
58+
if (sortBy === 'price-asc') return a.price - b.price;
59+
if (sortBy === 'price-desc') return b.price - a.price;
60+
if (sortBy === 'featured') return a.isFeatured === b.isFeatured ? 0 : a.isFeatured ? -1 : 1;
61+
return 0;
62+
});
63+
},
64+
}))

0 commit comments

Comments
 (0)