Upload folder using huggingface_hub
Browse files- .env.example +9 -0
- .gitattributes +4 -0
- .gitignore +8 -0
- README.md +20 -0
- assets/.aistudio/.gitignore +1 -0
- firebase-applet-config.json +10 -0
- firebase-blueprint.json +126 -0
- firestore.rules +123 -0
- index.html +112 -0
- metadata.json +6 -0
- package-lock.json +0 -0
- package.json +36 -0
- src/App.tsx +798 -0
- src/assets/images/comfort_tire_1781509452553.jpg +3 -0
- src/assets/images/sport_tire_1781509490802.jpg +3 -0
- src/assets/images/suv_tire_1781509472580.jpg +3 -0
- src/assets/images/winter_tire_1781509513584.jpg +3 -0
- src/components/AdminLoginModal.tsx +129 -0
- src/components/FAQSection.tsx +262 -0
- src/components/FloatingActions.tsx +96 -0
- src/components/Hero.tsx +275 -0
- src/components/InventoryManager.tsx +1311 -0
- src/components/LocationContact.tsx +243 -0
- src/components/Logo.tsx +179 -0
- src/components/Navbar.tsx +211 -0
- src/components/NewsletterSignup.tsx +128 -0
- src/components/ProductDetailsModal.tsx +175 -0
- src/components/Promotions.tsx +318 -0
- src/components/ReviewsSection.tsx +251 -0
- src/components/TireCard.tsx +139 -0
- src/components/VehicleFitmentWidget.tsx +296 -0
- src/data.ts +393 -0
- src/index.css +92 -0
- src/lib/firebase.ts +86 -0
- src/lib/firebaseService.ts +827 -0
- src/lib/tireService.ts +372 -0
- src/main.tsx +10 -0
- storage.rules +19 -0
- tsconfig.json +26 -0
- vite.config.ts +22 -0
.env.example
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# GEMINI_API_KEY: Required for Gemini AI API calls.
|
| 2 |
+
# AI Studio automatically injects this at runtime from user secrets.
|
| 3 |
+
# Users configure this via the Secrets panel in the AI Studio UI.
|
| 4 |
+
GEMINI_API_KEY="MY_GEMINI_API_KEY"
|
| 5 |
+
|
| 6 |
+
# APP_URL: The URL where this applet is hosted.
|
| 7 |
+
# AI Studio automatically injects this at runtime with the Cloud Run service URL.
|
| 8 |
+
# Used for self-referential links, OAuth callbacks, and API endpoints.
|
| 9 |
+
APP_URL="MY_APP_URL"
|
.gitattributes
CHANGED
|
@@ -33,3 +33,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
src/assets/images/comfort_tire_1781509452553.jpg filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
src/assets/images/sport_tire_1781509490802.jpg filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
src/assets/images/suv_tire_1781509472580.jpg filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
src/assets/images/winter_tire_1781509513584.jpg filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules/
|
| 2 |
+
build/
|
| 3 |
+
dist/
|
| 4 |
+
coverage/
|
| 5 |
+
.DS_Store
|
| 6 |
+
*.log
|
| 7 |
+
.env*
|
| 8 |
+
!.env.example
|
README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<div align="center">
|
| 2 |
+
<img width="1200" height="475" alt="GHBanner" src="https://ai.google.dev/static/site-assets/images/share-ais-513315318.png" />
|
| 3 |
+
</div>
|
| 4 |
+
|
| 5 |
+
# Run and deploy your AI Studio app
|
| 6 |
+
|
| 7 |
+
This contains everything you need to run your app locally.
|
| 8 |
+
|
| 9 |
+
View your app in AI Studio: https://ai.studio/apps/b18648e1-f7e5-49af-8c1a-a41fa7ba6ce2
|
| 10 |
+
|
| 11 |
+
## Run Locally
|
| 12 |
+
|
| 13 |
+
**Prerequisites:** Node.js
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
1. Install dependencies:
|
| 17 |
+
`npm install`
|
| 18 |
+
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
|
| 19 |
+
3. Run the app:
|
| 20 |
+
`npm run dev`
|
assets/.aistudio/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
*
|
firebase-applet-config.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"projectId": "gen-lang-client-0073278154",
|
| 3 |
+
"appId": "1:439857597926:web:83652c75dc57d7059e9a27",
|
| 4 |
+
"apiKey": "AIzaSyBpSkNWbpSUtiOwhbo30Xhx_K8OU9ClT2U",
|
| 5 |
+
"authDomain": "gen-lang-client-0073278154.firebaseapp.com",
|
| 6 |
+
"firestoreDatabaseId": "ai-studio-b18648e1-f7e5-49af-8c1a-a41fa7ba6ce2",
|
| 7 |
+
"storageBucket": "gen-lang-client-0073278154.firebasestorage.app",
|
| 8 |
+
"messagingSenderId": "439857597926",
|
| 9 |
+
"measurementId": ""
|
| 10 |
+
}
|
firebase-blueprint.json
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"entities": {
|
| 3 |
+
"Product": {
|
| 4 |
+
"title": "Product",
|
| 5 |
+
"description": "Premium tire configuration parameters and live branch inventory metrics",
|
| 6 |
+
"type": "object",
|
| 7 |
+
"properties": {
|
| 8 |
+
"id": {
|
| 9 |
+
"type": "string",
|
| 10 |
+
"description": "Auto-generated document identifier"
|
| 11 |
+
},
|
| 12 |
+
"name": {
|
| 13 |
+
"type": "string",
|
| 14 |
+
"description": "Full tire model name"
|
| 15 |
+
},
|
| 16 |
+
"brand": {
|
| 17 |
+
"type": "string",
|
| 18 |
+
"description": "Tyre manufacturer / brand name"
|
| 19 |
+
},
|
| 20 |
+
"size": {
|
| 21 |
+
"type": "string",
|
| 22 |
+
"description": "Tyre size specification code"
|
| 23 |
+
},
|
| 24 |
+
"price": {
|
| 25 |
+
"type": "number",
|
| 26 |
+
"description": "Dealer wholesale price in PKR"
|
| 27 |
+
},
|
| 28 |
+
"category": {
|
| 29 |
+
"type": "string",
|
| 30 |
+
"enum": ["hot-selling", "new-brands", "famous"],
|
| 31 |
+
"description": "Segment classification category"
|
| 32 |
+
},
|
| 33 |
+
"stock": {
|
| 34 |
+
"type": "number",
|
| 35 |
+
"description": "Live stock quantity level currently available at Faisal Town Lahore showroom"
|
| 36 |
+
},
|
| 37 |
+
"feature": {
|
| 38 |
+
"type": "string",
|
| 39 |
+
"description": "Primary highlight tire characteristic"
|
| 40 |
+
},
|
| 41 |
+
"imageUrl": {
|
| 42 |
+
"type": "string",
|
| 43 |
+
"description": "Absolute URL path to tire product showcase image file"
|
| 44 |
+
},
|
| 45 |
+
"createdAt": {
|
| 46 |
+
"type": "string",
|
| 47 |
+
"format": "date-time",
|
| 48 |
+
"description": "Timestamp indicating document creation time"
|
| 49 |
+
}
|
| 50 |
+
},
|
| 51 |
+
"required": ["name", "brand", "size", "price", "category", "stock"]
|
| 52 |
+
},
|
| 53 |
+
"VisitorLog": {
|
| 54 |
+
"title": "VisitorLog",
|
| 55 |
+
"description": "Log of user visitor analytics for tracking store traffic and engagement",
|
| 56 |
+
"type": "object",
|
| 57 |
+
"properties": {
|
| 58 |
+
"id": {
|
| 59 |
+
"type": "string",
|
| 60 |
+
"description": "Auto-generated document identifier"
|
| 61 |
+
},
|
| 62 |
+
"sessionId": {
|
| 63 |
+
"type": "string",
|
| 64 |
+
"description": "Unique session identifier for the visitor"
|
| 65 |
+
},
|
| 66 |
+
"userAgent": {
|
| 67 |
+
"type": "string",
|
| 68 |
+
"description": "Visitor browser metadata"
|
| 69 |
+
},
|
| 70 |
+
"platform": {
|
| 71 |
+
"type": "string",
|
| 72 |
+
"description": "OS platform of the visitor"
|
| 73 |
+
},
|
| 74 |
+
"language": {
|
| 75 |
+
"type": "string",
|
| 76 |
+
"description": "Browser default language"
|
| 77 |
+
},
|
| 78 |
+
"pagePath": {
|
| 79 |
+
"type": "string",
|
| 80 |
+
"description": "Specific route or section visited"
|
| 81 |
+
},
|
| 82 |
+
"timestamp": {
|
| 83 |
+
"type": "string",
|
| 84 |
+
"format": "date-time",
|
| 85 |
+
"description": "Creation timestamp"
|
| 86 |
+
}
|
| 87 |
+
},
|
| 88 |
+
"required": ["sessionId", "timestamp"]
|
| 89 |
+
},
|
| 90 |
+
"NewsletterSubscriber": {
|
| 91 |
+
"title": "NewsletterSubscriber",
|
| 92 |
+
"description": "Newsletter subscriber email record for sending seasonal maintenance tips",
|
| 93 |
+
"type": "object",
|
| 94 |
+
"properties": {
|
| 95 |
+
"id": {
|
| 96 |
+
"type": "string",
|
| 97 |
+
"description": "Auto-generated document identifier"
|
| 98 |
+
},
|
| 99 |
+
"email": {
|
| 100 |
+
"type": "string",
|
| 101 |
+
"description": "Email address of the newsletter subscriber"
|
| 102 |
+
},
|
| 103 |
+
"createdAt": {
|
| 104 |
+
"type": "string",
|
| 105 |
+
"format": "date-time",
|
| 106 |
+
"description": "Subscription timestamp"
|
| 107 |
+
}
|
| 108 |
+
},
|
| 109 |
+
"required": ["email", "createdAt"]
|
| 110 |
+
}
|
| 111 |
+
},
|
| 112 |
+
"firestore": {
|
| 113 |
+
"products/{productId}": {
|
| 114 |
+
"schema": "Product",
|
| 115 |
+
"description": "Unified main retail/wholesale tire product collection"
|
| 116 |
+
},
|
| 117 |
+
"visitors/{visitorId}": {
|
| 118 |
+
"schema": "VisitorLog",
|
| 119 |
+
"description": "Collection storing website visitation events"
|
| 120 |
+
},
|
| 121 |
+
"subscribers/{subscriberId}": {
|
| 122 |
+
"schema": "NewsletterSubscriber",
|
| 123 |
+
"description": "Collection of customer newsletter recipient subscriber emails"
|
| 124 |
+
}
|
| 125 |
+
}
|
| 126 |
+
}
|
firestore.rules
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
rules_version = '2';
|
| 2 |
+
service cloud.firestore {
|
| 3 |
+
match /databases/{database}/documents {
|
| 4 |
+
|
| 5 |
+
// Global Safety net default-deny catch-all
|
| 6 |
+
match /{document=**} {
|
| 7 |
+
allow read, write: if false;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
// Hardened helper functions for static verification (denial of wallet guard)
|
| 11 |
+
function isValidId(id) {
|
| 12 |
+
return id is string && id.size() <= 128 && id.matches('^[a-zA-Z0-9_-]+$');
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
function incoming() {
|
| 16 |
+
return request.resource.data;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
function existing() {
|
| 20 |
+
return resource.data;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
// High fidelity schema assertion helper for Tire Products
|
| 24 |
+
function isValidTireProduct(data) {
|
| 25 |
+
return data.keys().hasAll(['id', 'name', 'brand', 'size', 'feature', 'price', 'badge', 'segment', 'image', 'description'])
|
| 26 |
+
&& data.keys().size() == 10
|
| 27 |
+
&& data.id is string && data.id.size() <= 128
|
| 28 |
+
&& data.name is string && data.name.size() <= 128
|
| 29 |
+
&& data.brand is string && data.brand.size() <= 128
|
| 30 |
+
&& data.size is string && data.size.size() <= 64
|
| 31 |
+
&& data.feature is string && data.feature.size() <= 256
|
| 32 |
+
&& (data.price is int || data.price is float) && data.price >= 0
|
| 33 |
+
&& data.badge is string && data.badge.size() <= 128
|
| 34 |
+
&& data.segment is string && (data.segment == 'hot' || data.segment == 'new' || data.segment == 'famous')
|
| 35 |
+
&& data.image is string && data.image.size() <= 1024
|
| 36 |
+
&& data.description is string && data.description.size() <= 4096;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
// Modern High fidelity schema helper for the requested "products" collection
|
| 40 |
+
function isValidProduct(data) {
|
| 41 |
+
return data.name is string && data.name.size() <= 128
|
| 42 |
+
&& data.brand is string && data.brand.size() <= 128
|
| 43 |
+
&& data.size is string && data.size.size() <= 64
|
| 44 |
+
&& (data.price is int || data.price is float) && data.price >= 0
|
| 45 |
+
&& data.category is string && (data.category == 'hot-selling' || data.category == 'new-brands' || data.category == 'famous')
|
| 46 |
+
&& (data.stock is int || data.stock is float) && data.stock >= 0
|
| 47 |
+
&& data.feature is string && data.feature.size() <= 256
|
| 48 |
+
&& data.imageUrl is string && data.imageUrl.size() <= 1024;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
// High fidelity schema assertion helper for User Testimonials
|
| 52 |
+
function isValidUserReview(data) {
|
| 53 |
+
return data.keys().hasAll(['name', 'rating', 'text', 'date', 'initials'])
|
| 54 |
+
&& data.keys().size() == 5
|
| 55 |
+
&& data.name is string && data.name.size() <= 128
|
| 56 |
+
&& data.rating is int && data.rating >= 1 && data.rating <= 5
|
| 57 |
+
&& data.text is string && data.text.size() <= 4096
|
| 58 |
+
&& data.date is string && data.date.size() <= 128
|
| 59 |
+
&& data.initials is string && data.initials.size() <= 16;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
// High fidelity schema assertion helper for Visitor Analytics
|
| 63 |
+
function isValidVisitorLog(data) {
|
| 64 |
+
return data.keys().hasAll(['sessionId', 'userAgent', 'platform', 'language', 'pagePath', 'timestamp'])
|
| 65 |
+
&& data.keys().size() == 6
|
| 66 |
+
&& data.sessionId is string && data.sessionId.size() <= 256
|
| 67 |
+
&& data.userAgent is string && data.userAgent.size() <= 1024
|
| 68 |
+
&& data.platform is string && data.platform.size() <= 128
|
| 69 |
+
&& data.language is string && data.language.size() <= 64
|
| 70 |
+
&& data.pagePath is string && data.pagePath.size() <= 256
|
| 71 |
+
&& data.timestamp is timestamp && data.timestamp == request.time;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
// High fidelity schema assertion helper for Newsletter Subscribers
|
| 75 |
+
function isValidSubscriber(data) {
|
| 76 |
+
return data.keys().hasAll(['email', 'createdAt'])
|
| 77 |
+
&& data.keys().size() == 2
|
| 78 |
+
&& data.email is string && data.email.size() <= 256 && data.email.matches('^[^@]+@[^@]+\\.[^@]+$')
|
| 79 |
+
&& data.createdAt is timestamp && data.createdAt == request.time;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
// Hardened constraints for the /products/ collection (Public read, authenticated write or seed prefix)
|
| 83 |
+
match /products/{productId} {
|
| 84 |
+
allow read: if true;
|
| 85 |
+
allow create: if (request.auth != null || productId.startsWith('seed-')) && isValidId(productId) && isValidProduct(incoming());
|
| 86 |
+
allow update: if request.auth != null && isValidId(productId) && isValidProduct(incoming());
|
| 87 |
+
allow delete: if request.auth != null && isValidId(productId);
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
// Allow public read on the system connectivity check path
|
| 91 |
+
match /test/connection {
|
| 92 |
+
allow read: if true;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// Hardened constraints for the /tyres/ collection
|
| 96 |
+
match /tyres/{tyreId} {
|
| 97 |
+
allow read: if true;
|
| 98 |
+
allow create, update: if isValidId(tyreId) && isValidTireProduct(incoming());
|
| 99 |
+
allow delete: if isValidId(tyreId);
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
// Hardened constraints for the /reviews/ collection
|
| 103 |
+
match /reviews/{reviewId} {
|
| 104 |
+
allow read: if true;
|
| 105 |
+
allow create: if isValidId(reviewId) && isValidUserReview(incoming());
|
| 106 |
+
allow update, delete: if false; // Reviews are write-once append-only
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
// Hardened constraints for the /visitors/ collection
|
| 110 |
+
match /visitors/{visitorId} {
|
| 111 |
+
allow read: if request.auth != null; // Admin-only read for visitor stats
|
| 112 |
+
allow create: if isValidId(visitorId) && isValidVisitorLog(incoming());
|
| 113 |
+
allow update, delete: if false; // Visitor logs are immutable
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
// Hardened constraints for the /subscribers/ collection (Public sign up, Admin-only read)
|
| 117 |
+
match /subscribers/{subscriberId} {
|
| 118 |
+
allow read: if request.auth != null; // Admin can view mailing list
|
| 119 |
+
allow create: if isValidId(subscriberId) && isValidSubscriber(incoming());
|
| 120 |
+
allow update, delete: if false; // Newsletter entries are append-only/immutable
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
}
|
index.html
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<meta name="description" content="Haider Brothers Traders in Faisal Town, Lahore. Top-rated 4.8★ tire shop offering 24/7 tire emergency service, wheel alignment, repairs, and high-quality tires from Bridgestone, Michelin, Yokohama, Autogrip." />
|
| 7 |
+
<title>Haider Brothers Traders | Premium Tires & 24/7 Service, Faisal Town Lahore</title>
|
| 8 |
+
|
| 9 |
+
<!-- Open Graph (Facebook / LinkedIn) Meta Tags -->
|
| 10 |
+
<meta property="og:type" content="website" />
|
| 11 |
+
<meta property="og:title" content="Haider Brothers Traders | Premium Tires & 24/7 Service, Lahore" />
|
| 12 |
+
<meta property="og:description" content=" Lahore's top-rated 4.8★ tyre shop offering certified brand-new tyres, 3D wheel alignment, and 24/7 emergency roadside flat tire assistance in Faisal Town." />
|
| 13 |
+
<meta property="og:image" content="https://images.unsplash.com/photo-1542282088-fe8426682b8f?auto=format&fit=crop&q=80&w=1200&h=630" />
|
| 14 |
+
<meta property="og:url" content="https://haidertrading.com" />
|
| 15 |
+
<meta property="og:site_name" content="Haider Brothers Traders" />
|
| 16 |
+
<meta property="og:locale" content="en_US" />
|
| 17 |
+
|
| 18 |
+
<!-- Twitter Card Meta Tags -->
|
| 19 |
+
<meta name="twitter:card" content="summary_large_image" />
|
| 20 |
+
<meta name="twitter:title" content="Haider Brothers Traders | Premium Tires & 24/7 Service, Lahore" />
|
| 21 |
+
<meta name="twitter:description" content="Certified brand-new tires, premium 3D wheel alignment, computerized balancing, and 24/7 roadside emergency tire repair in Faisal Town, Lahore." />
|
| 22 |
+
<meta name="twitter:image" content="https://images.unsplash.com/photo-1542282088-fe8426682b8f?auto=format&fit=crop&q=80&w=1200&h=630" />
|
| 23 |
+
|
| 24 |
+
<!-- JSON-LD LocalBusiness Schema -->
|
| 25 |
+
<script type="application/ld+json">
|
| 26 |
+
{
|
| 27 |
+
"@context": "https://schema.org",
|
| 28 |
+
"@type": "AutoPartsStore",
|
| 29 |
+
"name": "Haider Brothers Traders",
|
| 30 |
+
"image": "https://images.unsplash.com/photo-1542282088-fe8426682b8f?auto=format&fit=crop&q=80&w=800",
|
| 31 |
+
"description": "Premium 24/7 tire service, sales, alignment, and repair in Faisal Town, Block C 1, Lahore, Pakistan.",
|
| 32 |
+
"telephone": "+923024594403",
|
| 33 |
+
"priceRange": "PKR",
|
| 34 |
+
"address": {
|
| 35 |
+
"@type": "PostalAddress",
|
| 36 |
+
"streetAddress": "36-C-1, Maulana Shaukat Ali Road, Block C-1 Faisal Town",
|
| 37 |
+
"addressLocality": "Lahore",
|
| 38 |
+
"addressRegion": "Punjab",
|
| 39 |
+
"postalCode": "54700",
|
| 40 |
+
"addressCountry": "PK"
|
| 41 |
+
},
|
| 42 |
+
"geo": {
|
| 43 |
+
"@type": "GeoCoordinates",
|
| 44 |
+
"latitude": 31.4812,
|
| 45 |
+
"longitude": 74.3031
|
| 46 |
+
},
|
| 47 |
+
"openingHoursSpecification": {
|
| 48 |
+
"@type": "OpeningHoursSpecification",
|
| 49 |
+
"dayOfWeek": [
|
| 50 |
+
"Monday",
|
| 51 |
+
"Tuesday",
|
| 52 |
+
"Wednesday",
|
| 53 |
+
"Thursday",
|
| 54 |
+
"Friday",
|
| 55 |
+
"Saturday",
|
| 56 |
+
"Sunday"
|
| 57 |
+
],
|
| 58 |
+
"opens": "00:00",
|
| 59 |
+
"closes": "23:59"
|
| 60 |
+
},
|
| 61 |
+
"aggregateRating": {
|
| 62 |
+
"@type": "AggregateRating",
|
| 63 |
+
"ratingValue": "4.8",
|
| 64 |
+
"reviewCount": "5"
|
| 65 |
+
},
|
| 66 |
+
"review": [
|
| 67 |
+
{
|
| 68 |
+
"@type": "Review",
|
| 69 |
+
"author": {
|
| 70 |
+
"@type": "Person",
|
| 71 |
+
"name": "gitsitmarketing"
|
| 72 |
+
},
|
| 73 |
+
"reviewRating": {
|
| 74 |
+
"@type": "Rating",
|
| 75 |
+
"ratingValue": "5"
|
| 76 |
+
},
|
| 77 |
+
"reviewBody": "Excellent service and quality of tires. My experience was good."
|
| 78 |
+
},
|
| 79 |
+
{
|
| 80 |
+
"@type": "Review",
|
| 81 |
+
"author": {
|
| 82 |
+
"@type": "Person",
|
| 83 |
+
"name": "M Bilal"
|
| 84 |
+
},
|
| 85 |
+
"reviewRating": {
|
| 86 |
+
"@type": "Rating",
|
| 87 |
+
"ratingValue": "5"
|
| 88 |
+
},
|
| 89 |
+
"reviewBody": "My experience was great with haider brother traders. They provided me good service and high quality product."
|
| 90 |
+
},
|
| 91 |
+
{
|
| 92 |
+
"@type": "Review",
|
| 93 |
+
"author": {
|
| 94 |
+
"@type": "Person",
|
| 95 |
+
"name": "Muhammad Ramzan"
|
| 96 |
+
},
|
| 97 |
+
"reviewRating": {
|
| 98 |
+
"@type": "Rating",
|
| 99 |
+
"ratingValue": "5"
|
| 100 |
+
},
|
| 101 |
+
"reviewBody": "Great quality and service."
|
| 102 |
+
}
|
| 103 |
+
]
|
| 104 |
+
}
|
| 105 |
+
</script>
|
| 106 |
+
</head>
|
| 107 |
+
<body>
|
| 108 |
+
<div id="root"></div>
|
| 109 |
+
<script type="module" src="/src/main.tsx"></script>
|
| 110 |
+
</body>
|
| 111 |
+
</html>
|
| 112 |
+
|
metadata.json
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Haider Brothers Traders",
|
| 3 |
+
"description": "Premium 24/7 tire service, sales, alignment, and repair in Faisal Town, Block C 1, Lahore, Pakistan.",
|
| 4 |
+
"requestFramePermissions": [],
|
| 5 |
+
"majorCapabilities": ["MAJOR_CAPABILITY_SERVER_SIDE_GEMINI_API"]
|
| 6 |
+
}
|
package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "react-example",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite --port=3000 --host=0.0.0.0",
|
| 8 |
+
"build": "vite build",
|
| 9 |
+
"preview": "vite preview",
|
| 10 |
+
"clean": "rm -rf dist server.js",
|
| 11 |
+
"lint": "tsc --noEmit"
|
| 12 |
+
},
|
| 13 |
+
"dependencies": {
|
| 14 |
+
"@google/genai": "^2.4.0",
|
| 15 |
+
"@tailwindcss/vite": "^4.1.14",
|
| 16 |
+
"@vitejs/plugin-react": "^5.0.4",
|
| 17 |
+
"dotenv": "^17.2.3",
|
| 18 |
+
"express": "^4.21.2",
|
| 19 |
+
"firebase": "^12.14.0",
|
| 20 |
+
"lucide-react": "^0.546.0",
|
| 21 |
+
"motion": "^12.23.24",
|
| 22 |
+
"react": "^19.0.1",
|
| 23 |
+
"react-dom": "^19.0.1",
|
| 24 |
+
"vite": "^6.2.3"
|
| 25 |
+
},
|
| 26 |
+
"devDependencies": {
|
| 27 |
+
"@types/express": "^4.17.21",
|
| 28 |
+
"@types/node": "^22.14.0",
|
| 29 |
+
"autoprefixer": "^10.4.21",
|
| 30 |
+
"esbuild": "^0.25.0",
|
| 31 |
+
"tailwindcss": "^4.1.14",
|
| 32 |
+
"tsx": "^4.21.0",
|
| 33 |
+
"typescript": "~5.8.2",
|
| 34 |
+
"vite": "^6.2.3"
|
| 35 |
+
}
|
| 36 |
+
}
|
src/App.tsx
ADDED
|
@@ -0,0 +1,798 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useEffect } from "react";
|
| 2 |
+
import { motion, AnimatePresence } from "motion/react";
|
| 3 |
+
import {
|
| 4 |
+
Sparkles,
|
| 5 |
+
MapPin,
|
| 6 |
+
Phone,
|
| 7 |
+
Clock,
|
| 8 |
+
Star,
|
| 9 |
+
Layers,
|
| 10 |
+
Compass,
|
| 11 |
+
MessageSquare,
|
| 12 |
+
Heart,
|
| 13 |
+
CheckCircle2,
|
| 14 |
+
ChevronRight,
|
| 15 |
+
TrendingUp,
|
| 16 |
+
Award,
|
| 17 |
+
Search,
|
| 18 |
+
ChevronDown,
|
| 19 |
+
ChevronUp,
|
| 20 |
+
Share2,
|
| 21 |
+
Lock,
|
| 22 |
+
Wrench,
|
| 23 |
+
HelpCircle,
|
| 24 |
+
FolderLock
|
| 25 |
+
} from "lucide-react";
|
| 26 |
+
|
| 27 |
+
import { BUSINESS_INFO, BRANDS, REVIEWS, TireProduct } from "./data";
|
| 28 |
+
import {
|
| 29 |
+
getTireProducts,
|
| 30 |
+
getReviews,
|
| 31 |
+
addReview,
|
| 32 |
+
SEED_TYRE_PRODUCTS
|
| 33 |
+
} from "./lib/tireService";
|
| 34 |
+
import { checkAuthState, logVisitorInfo, onProductsSnapshot } from "./lib/firebaseService";
|
| 35 |
+
|
| 36 |
+
import Navbar from "./components/Navbar";
|
| 37 |
+
import Logo from "./components/Logo";
|
| 38 |
+
import Hero from "./components/Hero";
|
| 39 |
+
import TireCard from "./components/TireCard";
|
| 40 |
+
import ProductDetailsModal from "./components/ProductDetailsModal";
|
| 41 |
+
import ReviewsSection from "./components/ReviewsSection";
|
| 42 |
+
import LocationContact from "./components/LocationContact";
|
| 43 |
+
import FloatingActions from "./components/FloatingActions";
|
| 44 |
+
import VehicleFitmentWidget from "./components/VehicleFitmentWidget";
|
| 45 |
+
import InventoryManager from "./components/InventoryManager";
|
| 46 |
+
import AdminLoginModal from "./components/AdminLoginModal";
|
| 47 |
+
import Promotions from "./components/Promotions";
|
| 48 |
+
import NewsletterSignup from "./components/NewsletterSignup";
|
| 49 |
+
import FAQSection from "./components/FAQSection";
|
| 50 |
+
|
| 51 |
+
export default function App() {
|
| 52 |
+
const [darkMode, setDarkMode] = useState<boolean>(true);
|
| 53 |
+
const [currentPage, setCurrentPage] = useState<string>("home");
|
| 54 |
+
const [currentUser, setCurrentUser] = useState<any | null>(null);
|
| 55 |
+
const [showAdminLoginModal, setShowAdminLoginModal] = useState<boolean>(false);
|
| 56 |
+
|
| 57 |
+
const isAuthorizedAdmin = currentUser?.email === "bahaduralimunnabhai@gmail.com";
|
| 58 |
+
|
| 59 |
+
// Redirect to home if unauthenticated user somehow sits on admin route
|
| 60 |
+
useEffect(() => {
|
| 61 |
+
if (currentPage === "admin" && !isAuthorizedAdmin) {
|
| 62 |
+
setCurrentPage("home");
|
| 63 |
+
}
|
| 64 |
+
}, [currentPage, isAuthorizedAdmin]);
|
| 65 |
+
|
| 66 |
+
// Track page navigation analytics safely
|
| 67 |
+
useEffect(() => {
|
| 68 |
+
logVisitorInfo(currentPage);
|
| 69 |
+
}, [currentPage]);
|
| 70 |
+
|
| 71 |
+
// Auth listener
|
| 72 |
+
useEffect(() => {
|
| 73 |
+
const unsubscribe = checkAuthState((user) => {
|
| 74 |
+
setCurrentUser(user);
|
| 75 |
+
});
|
| 76 |
+
return () => unsubscribe();
|
| 77 |
+
}, []);
|
| 78 |
+
|
| 79 |
+
// Master API database states loaded dynamically from Firestore
|
| 80 |
+
const [products, setProducts] = useState<TireProduct[]>([]);
|
| 81 |
+
const [reviews, setReviews] = useState<any[]>([]);
|
| 82 |
+
const [loading, setLoading] = useState<boolean>(true);
|
| 83 |
+
|
| 84 |
+
// Dynamic state selectors
|
| 85 |
+
const [activeSegment, setActiveSegment] = useState<'all' | 'hot' | 'new' | 'famous'>('all');
|
| 86 |
+
const [selectedBrand, setSelectedBrand] = useState<string>('All');
|
| 87 |
+
const [searchQuery, setSearchQuery] = useState<string>('');
|
| 88 |
+
const [priceSort, setPriceSort] = useState<'none' | 'low-high' | 'high-low'>('none');
|
| 89 |
+
const [selectedTire, setSelectedTire] = useState<TireProduct | null>(null);
|
| 90 |
+
|
| 91 |
+
// Load products dynamically in real-time and reviews on startup
|
| 92 |
+
useEffect(() => {
|
| 93 |
+
setLoading(true);
|
| 94 |
+
|
| 95 |
+
// Subscribe to live products collection updates
|
| 96 |
+
const unsubscribeProducts = onProductsSnapshot((snapshotList) => {
|
| 97 |
+
const mapped = snapshotList.map(p => ({
|
| 98 |
+
id: p.id,
|
| 99 |
+
name: p.name,
|
| 100 |
+
brand: p.brand,
|
| 101 |
+
size: p.size,
|
| 102 |
+
feature: p.feature,
|
| 103 |
+
price: p.price,
|
| 104 |
+
badge: p.badge || (p.category === "hot-selling" ? "Hot Selling" : p.category === "new-brands" ? "New Arrival" : "Famous"),
|
| 105 |
+
segment: p.segment || (p.category === "hot-selling" ? "hot" : p.category === "new-brands" ? "new" : "famous") as "hot" | "new" | "famous",
|
| 106 |
+
image: p.image || p.imageUrl,
|
| 107 |
+
stock: p.stock || 0,
|
| 108 |
+
description: p.description || `Premium quality tire from ${p.brand}.`
|
| 109 |
+
}));
|
| 110 |
+
setProducts(mapped);
|
| 111 |
+
setLoading(false);
|
| 112 |
+
});
|
| 113 |
+
|
| 114 |
+
loadReviews();
|
| 115 |
+
|
| 116 |
+
return () => {
|
| 117 |
+
unsubscribeProducts();
|
| 118 |
+
};
|
| 119 |
+
}, []);
|
| 120 |
+
|
| 121 |
+
const loadReviews = async () => {
|
| 122 |
+
try {
|
| 123 |
+
const dbReviews = await getReviews();
|
| 124 |
+
if (dbReviews && dbReviews.length > 0) {
|
| 125 |
+
setReviews(dbReviews);
|
| 126 |
+
} else {
|
| 127 |
+
setReviews(REVIEWS);
|
| 128 |
+
}
|
| 129 |
+
} catch (e) {
|
| 130 |
+
console.error("Firestore reviews fallback.", e);
|
| 131 |
+
setReviews(REVIEWS);
|
| 132 |
+
}
|
| 133 |
+
};
|
| 134 |
+
|
| 135 |
+
const handleAddNewReview = async (newReviewObj: any) => {
|
| 136 |
+
try {
|
| 137 |
+
await addReview(newReviewObj);
|
| 138 |
+
// Reload from cloud immediately
|
| 139 |
+
const dbReviews = await getReviews();
|
| 140 |
+
setReviews(dbReviews);
|
| 141 |
+
} catch (e) {
|
| 142 |
+
// Offline fallback append
|
| 143 |
+
setReviews((prev) => [newReviewObj, ...prev]);
|
| 144 |
+
}
|
| 145 |
+
};
|
| 146 |
+
|
| 147 |
+
// Helper action: when selecting recommended size from widget, immediately filter catalogue
|
| 148 |
+
const handleFilterSizeFromWidget = (sizeCode: string) => {
|
| 149 |
+
setSearchQuery(sizeCode);
|
| 150 |
+
setSelectedBrand("All");
|
| 151 |
+
setActiveSegment("all");
|
| 152 |
+
setPriceSort("none");
|
| 153 |
+
setCurrentPage("catalogue");
|
| 154 |
+
|
| 155 |
+
// Smooth scroll to top of database list
|
| 156 |
+
setTimeout(() => {
|
| 157 |
+
const el = document.getElementById("catalog-grid-top");
|
| 158 |
+
if (el) el.scrollIntoView({ behavior: "smooth", block: "start" });
|
| 159 |
+
}, 100);
|
| 160 |
+
};
|
| 161 |
+
|
| 162 |
+
// Derivative Filter for landing sections
|
| 163 |
+
const hotSellingProducts = products.filter(p => p.segment === 'hot');
|
| 164 |
+
const newArrivalProducts = products.filter(p => p.segment === 'new');
|
| 165 |
+
|
| 166 |
+
// Master Filter and Sorting engine for dynamic Catalogue list
|
| 167 |
+
const filteredCatalogProducts = products.filter((tire) => {
|
| 168 |
+
const matchesSegment = activeSegment === 'all' || tire.segment === activeSegment;
|
| 169 |
+
const matchesBrand = selectedBrand === 'All' || tire.brand.toLowerCase() === selectedBrand.toLowerCase();
|
| 170 |
+
|
| 171 |
+
const searchString = `${tire.name} ${tire.brand} ${tire.size} ${tire.feature} ${tire.description}`.toLowerCase();
|
| 172 |
+
const matchesSearch = searchString.includes(searchQuery.toLowerCase());
|
| 173 |
+
|
| 174 |
+
return matchesSegment && matchesBrand && matchesSearch;
|
| 175 |
+
});
|
| 176 |
+
|
| 177 |
+
// Apply Price Sorting configurations
|
| 178 |
+
const sortedCatalogProducts = [...filteredCatalogProducts].sort((a, b) => {
|
| 179 |
+
if (priceSort === "low-high") return a.price - b.price;
|
| 180 |
+
if (priceSort === "high-low") return b.price - a.price;
|
| 181 |
+
return 0; // retain database natural feed sorting
|
| 182 |
+
});
|
| 183 |
+
|
| 184 |
+
return (
|
| 185 |
+
<div className={`min-h-screen font-sans antialiased transition-colors duration-300 ${
|
| 186 |
+
darkMode ? "bg-brand-dark text-white" : "bg-slate-50 text-slate-900"
|
| 187 |
+
}`}>
|
| 188 |
+
|
| 189 |
+
{/* 24/7 Top Alert Ribbon with High Visibility SEO keyword highlights */}
|
| 190 |
+
<div className="bg-gradient-to-r from-brand-orange to-red-600 text-white text-center py-2 px-4 text-xs font-mono font-bold uppercase tracking-widest flex items-center justify-center space-x-2 relative z-50">
|
| 191 |
+
<span className="w-2.5 h-2.5 rounded-full bg-white animate-ping"></span>
|
| 192 |
+
<span className="hidden sm:inline">24/7 EMERGENCY TYRE PUNCTURE BREAKDOWN REPAIR ROAD ASSISTANCE IN LAHORE: </span>
|
| 193 |
+
<span className="sm:hidden">24/7 LAHORE EMERGENCY ASSISTANCE: </span>
|
| 194 |
+
<a href={`tel:${BUSINESS_INFO.phoneRaw}`} className="underline hover:opacity-85 ml-1 font-sans font-extrabold text-xs tracking-normal">
|
| 195 |
+
📞 {BUSINESS_INFO.phone}
|
| 196 |
+
</a>
|
| 197 |
+
</div>
|
| 198 |
+
|
| 199 |
+
{/* State-driven Navigation Bar */}
|
| 200 |
+
<Navbar
|
| 201 |
+
darkMode={darkMode}
|
| 202 |
+
setDarkMode={setDarkMode}
|
| 203 |
+
currentPage={currentPage}
|
| 204 |
+
setCurrentPage={setCurrentPage}
|
| 205 |
+
isAuthorizedAdmin={isAuthorizedAdmin}
|
| 206 |
+
/>
|
| 207 |
+
|
| 208 |
+
{/* Main Container Wrapper with Animation Transitions */}
|
| 209 |
+
<main className="transition-opacity duration-300">
|
| 210 |
+
|
| 211 |
+
{/* VIEW 1: HOME PAGE PAGE-TAB */}
|
| 212 |
+
{currentPage === "home" && (
|
| 213 |
+
<motion.div
|
| 214 |
+
initial={{ opacity: 0 }}
|
| 215 |
+
animate={{ opacity: 1 }}
|
| 216 |
+
exit={{ opacity: 0 }}
|
| 217 |
+
transition={{ duration: 0.4 }}
|
| 218 |
+
>
|
| 219 |
+
{/* Elegant Display Hero Header */}
|
| 220 |
+
<Hero darkMode={darkMode} />
|
| 221 |
+
|
| 222 |
+
{/* HIGH SEO TEXT SECTION & KEYWORDS */}
|
| 223 |
+
<section className={`py-6 border-y ${darkMode ? "bg-black/10 border-white/5" : "bg-slate-100 border-slate-200"}`}>
|
| 224 |
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center text-xs tracking-wide font-mono opacity-85">
|
| 225 |
+
<span>⚡ LAHORE INDUSTRIAL TRADING CORP • RETAIL & WHOLESALE RATES ON BRAND NEW TYRES IN FAISAL TOWN • CERTIFIED GERMAN TUNE-UP & ALIGNMENT STAGES</span>
|
| 226 |
+
</div>
|
| 227 |
+
</section>
|
| 228 |
+
|
| 229 |
+
{/* SEASONAL BUNDLE PROMOTIONS CAROUSEL SLIDER */}
|
| 230 |
+
<Promotions darkMode={darkMode} />
|
| 231 |
+
|
| 232 |
+
{/* DYNAMIC CAR MODEL VEHICLE INTEGRATION WIDGET */}
|
| 233 |
+
<section className="py-16 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
| 234 |
+
<div className="text-center max-w-2xl mx-auto mb-10">
|
| 235 |
+
<span className="text-xs font-mono tracking-widest uppercase bg-brand-orange/15 text-brand-orange px-3.5 py-1 rounded-full font-extrabold">
|
| 236 |
+
Car Fitment Search
|
| 237 |
+
</span>
|
| 238 |
+
<h2 className="text-3xl font-display font-extrabold tracking-tight mt-3">
|
| 239 |
+
Vehicle Recommended Tyre Size Finder
|
| 240 |
+
</h2>
|
| 241 |
+
<p className={`text-sm mt-2 ${darkMode ? "text-slate-400" : "text-slate-600"}`}>
|
| 242 |
+
Input or lookup your car name to pull custom rim specifications, load recommended profile compounds, and lookup live stock matching instantly.
|
| 243 |
+
</p>
|
| 244 |
+
</div>
|
| 245 |
+
|
| 246 |
+
{/* Mounted child fitment widget component */}
|
| 247 |
+
<VehicleFitmentWidget
|
| 248 |
+
darkMode={darkMode}
|
| 249 |
+
onFilterSize={handleFilterSizeFromWidget}
|
| 250 |
+
/>
|
| 251 |
+
</section>
|
| 252 |
+
|
| 253 |
+
{/* FEATURED: HOT SELLING TYRES LISTING */}
|
| 254 |
+
<section id="hot" className={`py-12 ${darkMode ? "bg-brand-dark/95" : "bg-white"}`}>
|
| 255 |
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
| 256 |
+
<div className="flex flex-col sm:flex-row items-baseline justify-between mb-8 border-b pb-4 border-black/5 dark:border-white/5">
|
| 257 |
+
<div>
|
| 258 |
+
<span className="text-brand-orange font-mono text-xs uppercase font-extrabold tracking-widest">
|
| 259 |
+
🔥 Top Seller Index
|
| 260 |
+
</span>
|
| 261 |
+
<h3 className="text-2xl sm:text-3xl font-display font-black tracking-tight mt-1">
|
| 262 |
+
Lahore's Demanded Tyres
|
| 263 |
+
</h3>
|
| 264 |
+
</div>
|
| 265 |
+
<button
|
| 266 |
+
onClick={() => { setCurrentPage("catalogue"); setActiveSegment("hot"); }}
|
| 267 |
+
className="text-xs font-bold text-brand-orange hover:underline focus:outline-none mt-2 sm:mt-0"
|
| 268 |
+
>
|
| 269 |
+
View All Popular Tyres →
|
| 270 |
+
</button>
|
| 271 |
+
</div>
|
| 272 |
+
|
| 273 |
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
| 274 |
+
{hotSellingProducts.slice(0, 4).map((tire) => (
|
| 275 |
+
<TireCard
|
| 276 |
+
key={tire.id}
|
| 277 |
+
tire={tire}
|
| 278 |
+
darkMode={darkMode}
|
| 279 |
+
onSelect={(t) => setSelectedTire(t)}
|
| 280 |
+
/>
|
| 281 |
+
))}
|
| 282 |
+
</div>
|
| 283 |
+
</div>
|
| 284 |
+
</section>
|
| 285 |
+
|
| 286 |
+
{/* BRAND DIFFERENTIATORS BENTO GRAPH DISPLAY SECTION */}
|
| 287 |
+
<section className={`py-20 ${darkMode ? "bg-black/30" : "bg-slate-100"}`}>
|
| 288 |
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
| 289 |
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-10 items-center">
|
| 290 |
+
<div>
|
| 291 |
+
<span className="text-brand-orange font-mono text-xs uppercase font-extrabold">
|
| 292 |
+
🛡️ Premium Security and Quality Safety
|
| 293 |
+
</span>
|
| 294 |
+
<h3 className="text-3xl font-display font-black tracking-tight mt-2 leading-tight">
|
| 295 |
+
Why Faisal Town Chooses Haider Brothers Traders?
|
| 296 |
+
</h3>
|
| 297 |
+
<p className={`text-sm mt-3 leading-relaxed ${darkMode ? "text-slate-400" : "text-slate-600"}`}>
|
| 298 |
+
Our mission is to guarantee maximum safety rating, high speed response, comfortable suspensions, and durable tyre lifespan for everyday Pakistani passenger sedans and heavy commercial vehicles. We operate 24 hours a day to prevent vehicle emergencies and offer full high-grade premium warranty validation.
|
| 299 |
+
</p>
|
| 300 |
+
|
| 301 |
+
<div className="space-y-4 mt-6">
|
| 302 |
+
<div className="flex items-start space-x-3">
|
| 303 |
+
<span className="p-1 px-1.8 rounded-lg bg-brand-orange/10 text-brand-orange font-bold text-xs mt-0.5">✔</span>
|
| 304 |
+
<div>
|
| 305 |
+
<h4 className="font-bold text-sm">German 3D Wheel Alignment stages</h4>
|
| 306 |
+
<p className={`text-xs ${darkMode ? "text-slate-400" : "text-slate-500"}`}>We restore precise steering control & eliminate early tyre scrubbing.</p>
|
| 307 |
+
</div>
|
| 308 |
+
</div>
|
| 309 |
+
|
| 310 |
+
<div className="flex items-start space-x-3">
|
| 311 |
+
<span className="p-1 px-1.8 rounded-lg bg-brand-orange/10 text-brand-orange font-bold text-xs mt-0.5">✔</span>
|
| 312 |
+
<div>
|
| 313 |
+
<h4 className="font-bold text-sm">100% Genuine Certified Imports</h4>
|
| 314 |
+
<p className={`text-xs ${darkMode ? "text-slate-400" : "text-slate-500"}`}>Say no to smuggling issues. Enjoy legal security with real DOT manufacturing years.</p>
|
| 315 |
+
</div>
|
| 316 |
+
</div>
|
| 317 |
+
</div>
|
| 318 |
+
</div>
|
| 319 |
+
|
| 320 |
+
<div className={`p-6 sm:p-8 rounded-3xl border ${
|
| 321 |
+
darkMode ? "bg-[#1c1c24] border-white/5 shadow-2xl" : "bg-white border-slate-200/80 shadow-md"
|
| 322 |
+
}`}>
|
| 323 |
+
<h4 className="font-display font-black text-l uppercase text-brand-orange text-center mb-6">
|
| 324 |
+
⚙️ Core Service Statistics
|
| 325 |
+
</h4>
|
| 326 |
+
|
| 327 |
+
<div className="space-y-4">
|
| 328 |
+
<div>
|
| 329 |
+
<div className="flex justify-between text-xs font-mono font-bold mb-1">
|
| 330 |
+
<span>Original Custom Warranty Claim Ratio</span>
|
| 331 |
+
<span className="text-brand-orange">100% Guaranteed</span>
|
| 332 |
+
</div>
|
| 333 |
+
<div className="w-full h-2 rounded-full bg-slate-200 dark:bg-black/30 overflow-hidden">
|
| 334 |
+
<div className="h-full bg-brand-orange w-full"></div>
|
| 335 |
+
</div>
|
| 336 |
+
</div>
|
| 337 |
+
|
| 338 |
+
<div>
|
| 339 |
+
<div className="flex justify-between text-xs font-mono font-bold mb-1">
|
| 340 |
+
<span>Response Speed under Emergency (Minutes)</span>
|
| 341 |
+
<span className="text-brand-orange">Within 25 Mins</span>
|
| 342 |
+
</div>
|
| 343 |
+
<div className="w-full h-2 rounded-full bg-slate-200 dark:bg-black/30 overflow-hidden">
|
| 344 |
+
<div className="h-full bg-brand-orange" style={{ width: "95%" }}></div>
|
| 345 |
+
</div>
|
| 346 |
+
</div>
|
| 347 |
+
|
| 348 |
+
<div>
|
| 349 |
+
<div className="flex justify-between text-xs font-mono font-bold mb-1">
|
| 350 |
+
<span>Certified Import Tyre Compound Durability</span>
|
| 351 |
+
<span className="text-brand-orange">50,000+ Kilometers</span>
|
| 352 |
+
</div>
|
| 353 |
+
<div className="w-full h-2 rounded-full bg-slate-200 dark:bg-black/30 overflow-hidden">
|
| 354 |
+
<div className="h-full bg-brand-orange" style={{ width: "90%" }}></div>
|
| 355 |
+
</div>
|
| 356 |
+
</div>
|
| 357 |
+
</div>
|
| 358 |
+
|
| 359 |
+
<div className="p-4 bg-brand-orange/5 border border-brand-orange/10 rounded-xl text-center mt-6">
|
| 360 |
+
<span className="text-xs font-semibold text-brand-orange font-mono">
|
| 361 |
+
🔥 CERTIFIED REPUTATION FOR QUALITY FITTING
|
| 362 |
+
</span>
|
| 363 |
+
</div>
|
| 364 |
+
</div>
|
| 365 |
+
</div>
|
| 366 |
+
</div>
|
| 367 |
+
</section>
|
| 368 |
+
|
| 369 |
+
{/* SECTIONS: LAHORE STREET LOCATION CONTACT PREVIEW */}
|
| 370 |
+
<LocationContact darkMode={darkMode} />
|
| 371 |
+
</motion.div>
|
| 372 |
+
)}
|
| 373 |
+
|
| 374 |
+
{/* VIEW 2: TYRE CATALOGUE PAGE-TAB */}
|
| 375 |
+
{currentPage === "catalogue" && (
|
| 376 |
+
<motion.div
|
| 377 |
+
initial={{ opacity: 0, y: 15 }}
|
| 378 |
+
animate={{ opacity: 1, y: 0 }}
|
| 379 |
+
exit={{ opacity: 0 }}
|
| 380 |
+
className="pt-24 pb-20 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"
|
| 381 |
+
>
|
| 382 |
+
{/* Header intro */}
|
| 383 |
+
<div className="text-center max-w-2xl mx-auto mb-12" id="catalog-grid-top">
|
| 384 |
+
<span className="text-xs font-mono tracking-widest uppercase bg-brand-orange/10 text-brand-orange px-3.5 py-1.5 rounded-full font-bold inline-block">
|
| 385 |
+
Distributer Price Index
|
| 386 |
+
</span>
|
| 387 |
+
<h1 className="text-3xl sm:text-5xl font-display font-black tracking-tight mt-4">
|
| 388 |
+
Full Tyre Inventory Catalogue
|
| 389 |
+
</h1>
|
| 390 |
+
<p className={`text-sm mt-3 ${darkMode ? "text-slate-400" : "text-slate-600"}`}>
|
| 391 |
+
Live stock records connected to Faisal Town Lahore branch. Filter by sizes, manufacturers, segment highlights, or order by price index instantly.
|
| 392 |
+
</p>
|
| 393 |
+
</div>
|
| 394 |
+
|
| 395 |
+
{/* Advanced Filters Block */}
|
| 396 |
+
<div className={`p-6 rounded-3xl border mb-10 space-y-6 ${
|
| 397 |
+
darkMode ? "glassmorphism box-glow" : "glassmorphism-light shadow-md"
|
| 398 |
+
}`}>
|
| 399 |
+
{/* Row 1: Search and Sorting controls */}
|
| 400 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
| 401 |
+
{/* Search Bar input */}
|
| 402 |
+
<div className="md:col-span-2 relative">
|
| 403 |
+
<span className={`text-xs font-semibold font-mono block mb-1 px-1 ${darkMode ? "text-slate-300" : "text-slate-600"}`}>
|
| 404 |
+
Filter by Keywords (e.g. Michelin, 185/65R14, grabber)
|
| 405 |
+
</span>
|
| 406 |
+
<div className="relative">
|
| 407 |
+
<input
|
| 408 |
+
type="text"
|
| 409 |
+
placeholder="Type tire model, width, ratio, code, or feature description..."
|
| 410 |
+
value={searchQuery}
|
| 411 |
+
onChange={(e) => setSearchQuery(e.target.value)}
|
| 412 |
+
className={`w-full p-3 pl-10 rounded-xl text-xs border focus:ring-1 focus:ring-brand-orange focus:outline-none ${
|
| 413 |
+
darkMode ? "bg-black/35 border-white/10 text-white placeholder-slate-500" : "bg-white border-slate-200 text-slate-800"
|
| 414 |
+
}`}
|
| 415 |
+
id="search-tyres-input"
|
| 416 |
+
/>
|
| 417 |
+
<Search className="w-4 h-4 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
| 418 |
+
</div>
|
| 419 |
+
</div>
|
| 420 |
+
|
| 421 |
+
{/* Price sorting selection dropdown */}
|
| 422 |
+
<div>
|
| 423 |
+
<span className={`text-xs font-semibold font-mono block mb-1 px-1 ${darkMode ? "text-slate-300" : "text-slate-600"}`}>
|
| 424 |
+
Sort by Price Index
|
| 425 |
+
</span>
|
| 426 |
+
<select
|
| 427 |
+
value={priceSort}
|
| 428 |
+
onChange={(e) => setPriceSort(e.target.value as any)}
|
| 429 |
+
className={`w-full p-3 rounded-xl text-xs border focus:ring-1 focus:ring-brand-orange focus:outline-none ${
|
| 430 |
+
darkMode ? "bg-black/35 border-white/10 text-white" : "bg-white border-slate-200 text-slate-800"
|
| 431 |
+
}`}
|
| 432 |
+
id="price-sort-select"
|
| 433 |
+
>
|
| 434 |
+
<option value="none">Database Standards</option>
|
| 435 |
+
<option value="low-high">Price: Low to High</option>
|
| 436 |
+
<option value="high-low">Price: High to Low</option>
|
| 437 |
+
</select>
|
| 438 |
+
</div>
|
| 439 |
+
</div>
|
| 440 |
+
|
| 441 |
+
{/* Row 2: Segments */}
|
| 442 |
+
<div className="space-y-2 pt-2 border-t border-black/5 dark:border-white/5">
|
| 443 |
+
<span className={`text-xs font-mono font-bold tracking-wider uppercase block ${darkMode ? "text-slate-400" : "text-slate-500"}`}>
|
| 444 |
+
Filter by Segment Categories
|
| 445 |
+
</span>
|
| 446 |
+
<div className="flex flex-wrap gap-2">
|
| 447 |
+
{[
|
| 448 |
+
{ id: 'all', label: 'All Products' },
|
| 449 |
+
{ id: 'hot', label: '🔥 Hot Selling' },
|
| 450 |
+
{ id: 'new', label: '🆕 New arrivals' },
|
| 451 |
+
{ id: 'famous', label: '⭐ Famous Brand Favorites' }
|
| 452 |
+
].map((seg) => (
|
| 453 |
+
<button
|
| 454 |
+
key={seg.id}
|
| 455 |
+
onClick={() => setActiveSegment(seg.id as any)}
|
| 456 |
+
className={`px-3 py-2 rounded-xl text-xs font-semibold transition-all ${
|
| 457 |
+
activeSegment === seg.id
|
| 458 |
+
? "bg-brand-orange text-white"
|
| 459 |
+
: darkMode
|
| 460 |
+
? "bg-white/5 border border-white/5 text-slate-300 hover:bg-white/10"
|
| 461 |
+
: "bg-slate-100 border border-slate-200 text-slate-700 hover:bg-slate-200"
|
| 462 |
+
}`}
|
| 463 |
+
id={`segment-btn-${seg.id}`}
|
| 464 |
+
>
|
| 465 |
+
{seg.label}
|
| 466 |
+
</button>
|
| 467 |
+
))}
|
| 468 |
+
</div>
|
| 469 |
+
</div>
|
| 470 |
+
|
| 471 |
+
{/* Row 3: Manufacturer filters */}
|
| 472 |
+
<div className="space-y-2 pt-2 border-t border-black/5 dark:border-white/5">
|
| 473 |
+
<span className={`text-xs font-mono font-bold tracking-wider uppercase block ${darkMode ? "text-slate-400" : "text-slate-500"}`}>
|
| 474 |
+
Filter by Brand Manufacturer
|
| 475 |
+
</span>
|
| 476 |
+
<div className="flex flex-wrap gap-1.5">
|
| 477 |
+
<button
|
| 478 |
+
onClick={() => setSelectedBrand('All')}
|
| 479 |
+
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${
|
| 480 |
+
selectedBrand === 'All'
|
| 481 |
+
? "border border-brand-orange text-brand-orange bg-brand-orange/10 font-bold"
|
| 482 |
+
: darkMode
|
| 483 |
+
? "border border-white/5 bg-white/5 text-slate-300 hover:text-white"
|
| 484 |
+
: "border border-slate-200 bg-white text-slate-700 hover:text-slate-900"
|
| 485 |
+
}`}
|
| 486 |
+
id="brand-btn-all"
|
| 487 |
+
>
|
| 488 |
+
All Brands
|
| 489 |
+
</button>
|
| 490 |
+
{BRANDS.map((br) => (
|
| 491 |
+
<button
|
| 492 |
+
key={br.name}
|
| 493 |
+
onClick={() => setSelectedBrand(br.name)}
|
| 494 |
+
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${
|
| 495 |
+
selectedBrand === br.name
|
| 496 |
+
? "border border-brand-orange text-brand-orange bg-brand-orange/10 font-bold"
|
| 497 |
+
: darkMode
|
| 498 |
+
? "border border-white/5 bg-white/5 text-slate-300 hover:text-white"
|
| 499 |
+
: "border border-slate-200 bg-white text-slate-700 hover:text-slate-900"
|
| 500 |
+
}`}
|
| 501 |
+
id={`brand-btn-${br.name.replace(/\s+/g, '-').toLowerCase()}`}
|
| 502 |
+
>
|
| 503 |
+
{br.name}
|
| 504 |
+
</button>
|
| 505 |
+
))}
|
| 506 |
+
</div>
|
| 507 |
+
</div>
|
| 508 |
+
</div>
|
| 509 |
+
|
| 510 |
+
{/* Results bar */}
|
| 511 |
+
<div className="flex justify-between items-center mb-6">
|
| 512 |
+
<span className="text-xs font-mono text-slate-400">
|
| 513 |
+
Found <span className="text-brand-orange font-bold font-sans">{sortedCatalogProducts.length}</span> matching products in Lahore branch
|
| 514 |
+
</span>
|
| 515 |
+
{searchQuery && (
|
| 516 |
+
<button
|
| 517 |
+
onClick={() => setSearchQuery("")}
|
| 518 |
+
className="text-xs text-brand-orange hover:underline font-bold"
|
| 519 |
+
>
|
| 520 |
+
Clear size queries
|
| 521 |
+
</button>
|
| 522 |
+
)}
|
| 523 |
+
</div>
|
| 524 |
+
|
| 525 |
+
{/* Dynamic Tyre Catalog Cards Grid */}
|
| 526 |
+
{sortedCatalogProducts.length > 0 ? (
|
| 527 |
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
| 528 |
+
{sortedCatalogProducts.map((tire) => (
|
| 529 |
+
<TireCard
|
| 530 |
+
key={tire.id}
|
| 531 |
+
tire={tire}
|
| 532 |
+
darkMode={darkMode}
|
| 533 |
+
onSelect={(t) => setSelectedTire(t)}
|
| 534 |
+
/>
|
| 535 |
+
))}
|
| 536 |
+
</div>
|
| 537 |
+
) : (
|
| 538 |
+
<div className={`p-12 text-center rounded-3xl border border-dashed text-slate-400 ${
|
| 539 |
+
darkMode ? "bg-white/5 border-white/10" : "bg-slate-50 border-slate-200"
|
| 540 |
+
} max-w-md mx-auto`}>
|
| 541 |
+
<span className="text-4xl block mb-3">🔍</span>
|
| 542 |
+
<h4 className="font-display font-bold text-md text-brand-orange">No exact specification tyre found</h4>
|
| 543 |
+
<p className={`text-xs mt-2 leading-relaxed ${darkMode ? "text-slate-400" : "text-slate-500"}`}>
|
| 544 |
+
We might still have warehouse stock of size custom model '{searchQuery}'! Please call or shoot a direct WhatsApp to our sales desk.
|
| 545 |
+
</p>
|
| 546 |
+
<div className="mt-6 flex justify-center space-x-3">
|
| 547 |
+
<button
|
| 548 |
+
onClick={() => { setSelectedBrand("All"); setActiveSegment("all"); setSearchQuery(""); }}
|
| 549 |
+
className="bg-brand-orange text-white text-xs px-4 py-2 rounded-xl transition-all hover:bg-brand-orange-dark font-medium"
|
| 550 |
+
>
|
| 551 |
+
Reset Filter options
|
| 552 |
+
</button>
|
| 553 |
+
<a
|
| 554 |
+
href={`tel:${BUSINESS_INFO.phoneRaw}`}
|
| 555 |
+
className={`text-xs px-4 py-2 border rounded-xl transition-all ${
|
| 556 |
+
darkMode ? "border-white/10 text-white" : "border-slate-300 text-slate-800 hover:bg-slate-50"
|
| 557 |
+
}`}
|
| 558 |
+
>
|
| 559 |
+
Call Sales Office
|
| 560 |
+
</a>
|
| 561 |
+
</div>
|
| 562 |
+
</div>
|
| 563 |
+
)}
|
| 564 |
+
</motion.div>
|
| 565 |
+
)}
|
| 566 |
+
|
| 567 |
+
{/* VIEW 3: LIVE REVIEWS BOARD PAGE-TAB */}
|
| 568 |
+
{currentPage === "reviews" && (
|
| 569 |
+
<motion.div
|
| 570 |
+
initial={{ opacity: 0, y: 15 }}
|
| 571 |
+
animate={{ opacity: 1, y: 0 }}
|
| 572 |
+
exit={{ opacity: 0 }}
|
| 573 |
+
className="pt-24"
|
| 574 |
+
>
|
| 575 |
+
{/* Direct mount child Reviews component */}
|
| 576 |
+
<ReviewsSection
|
| 577 |
+
darkMode={darkMode}
|
| 578 |
+
reviews={reviews}
|
| 579 |
+
onAddReview={handleAddNewReview}
|
| 580 |
+
/>
|
| 581 |
+
</motion.div>
|
| 582 |
+
)}
|
| 583 |
+
|
| 584 |
+
{/* VIEW 4: SHOWROOM LOCATION DETAILS PAGE-TAB */}
|
| 585 |
+
{currentPage === "showroom" && (
|
| 586 |
+
<motion.div
|
| 587 |
+
initial={{ opacity: 0, y: 15 }}
|
| 588 |
+
animate={{ opacity: 1, y: 0 }}
|
| 589 |
+
exit={{ opacity: 0 }}
|
| 590 |
+
className="pt-24 pb-20"
|
| 591 |
+
>
|
| 592 |
+
{/* Showroom location address layouts */}
|
| 593 |
+
<LocationContact darkMode={darkMode} />
|
| 594 |
+
|
| 595 |
+
{/* FITTING & ALIGNMENT GUIDELINES EXTRA INFO PANEL */}
|
| 596 |
+
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-12 grid grid-cols-1 md:grid-cols-2 gap-8">
|
| 597 |
+
<div className={`p-6 sm:p-8 rounded-3xl border ${
|
| 598 |
+
darkMode ? "bg-[#1c1c24] border-white/5" : "bg-slate-100 border-slate-200"
|
| 599 |
+
}`}>
|
| 600 |
+
<h3 className="font-display font-black text-l text-brand-orange mb-4">
|
| 601 |
+
🧭 ROAD EMERGENCIES IN LAHORE
|
| 602 |
+
</h3>
|
| 603 |
+
<p className={`text-xs sm:text-sm leading-relaxed ${darkMode ? "text-slate-400" : "text-slate-600"}`}>
|
| 604 |
+
Broke down late at midnight in Faisal Town, Garden Town, Model Town, Gulberg, or Johar Town due to tyre flat punctures? Do not panic. Simply trigger our emergency phone number 24/7. Our mobile fitment van is equipped with high quality compound kits to rescue you immediately on-site!
|
| 605 |
+
</p>
|
| 606 |
+
<div className="mt-4">
|
| 607 |
+
<a
|
| 608 |
+
href={`tel:${BUSINESS_INFO.phoneRaw}`}
|
| 609 |
+
className="inline-flex items-center space-x-1 bg-brand-orange text-white font-bold text-xs px-4 py-2 rounded-xl"
|
| 610 |
+
>
|
| 611 |
+
<span>Request Roadside Assistance</span>
|
| 612 |
+
</a>
|
| 613 |
+
</div>
|
| 614 |
+
</div>
|
| 615 |
+
|
| 616 |
+
<div className={`p-6 sm:p-8 rounded-3xl border ${
|
| 617 |
+
darkMode ? "bg-[#1c1c24] border-white/5" : "bg-slate-100 border-slate-200"
|
| 618 |
+
}`}>
|
| 619 |
+
<h3 className="font-display font-black text-l text-brand-orange mb-4">
|
| 620 |
+
🛠️ ALIGNMENT & COMPUTERIZED STEERING ADJUSTMENTS
|
| 621 |
+
</h3>
|
| 622 |
+
<p className={`text-xs sm:text-sm leading-relaxed ${darkMode ? "text-slate-400" : "text-slate-600"}`}>
|
| 623 |
+
We recommend doing computerized 3D wheel alignment checks and wheel balancing every 8,000 Kilometers to avoid uneven tread wire wear, suspension pull, and secure complete control of the car across highways at high speeds. Stop by during the day for discount fitment checking.
|
| 624 |
+
</p>
|
| 625 |
+
<p className="text-[11px] font-mono text-slate-400 mt-3">
|
| 626 |
+
⏱️ 3D Alignment Stage is hosted 24/7 for premium passenger cars.
|
| 627 |
+
</p>
|
| 628 |
+
</div>
|
| 629 |
+
</section>
|
| 630 |
+
|
| 631 |
+
{/* DYNAMIC FAQ ACCORDION COMPONENT WITH HIGHEST SEO RATINGS */}
|
| 632 |
+
<FAQSection darkMode={darkMode} />
|
| 633 |
+
</motion.div>
|
| 634 |
+
)}
|
| 635 |
+
|
| 636 |
+
{/* VIEW 5: INVENTORY MANAGER DB SEEDING PAGE-TAB */}
|
| 637 |
+
{currentPage === "admin" && (
|
| 638 |
+
<motion.div
|
| 639 |
+
initial={{ opacity: 0, y: 15 }}
|
| 640 |
+
animate={{ opacity: 1, y: 0 }}
|
| 641 |
+
exit={{ opacity: 0 }}
|
| 642 |
+
className="pt-28 pb-20 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"
|
| 643 |
+
>
|
| 644 |
+
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-2 border-b pb-4 border-black/5 dark:border-white/5">
|
| 645 |
+
<div>
|
| 646 |
+
<div className="inline-flex items-center space-x-2 bg-brand-orange/10 px-3 py-1 rounded-full text-brand-orange text-xs font-mono font-bold uppercase tracking-wider mb-2">
|
| 647 |
+
<FolderLock className="w-3.5 h-3.5" />
|
| 648 |
+
<span>Real Backend Integration Console</span>
|
| 649 |
+
</div>
|
| 650 |
+
<h1 className="text-3xl sm:text-4xl font-display font-black tracking-tight mt-1">
|
| 651 |
+
Firestore Inventory Manager
|
| 652 |
+
</h1>
|
| 653 |
+
<p className={`text-sm mt-1 max-w-2xl ${darkMode ? "text-slate-400" : "text-slate-600"}`}>
|
| 654 |
+
Admin board connected directly with our provisioned cloud Firestore. Any tyre product configured, edited, or removed in this environment reflects onto the landing page immediately.
|
| 655 |
+
</p>
|
| 656 |
+
</div>
|
| 657 |
+
|
| 658 |
+
<div className="flex items-center space-x-2 bg-green-500/10 border border-green-500/20 text-emerald-500 text-xs px-3 py-1.8 rounded-xl font-mono">
|
| 659 |
+
<span>Secure DB Mode</span>
|
| 660 |
+
</div>
|
| 661 |
+
</div>
|
| 662 |
+
|
| 663 |
+
{/* Main inventory administration dashboards */}
|
| 664 |
+
<InventoryManager
|
| 665 |
+
darkMode={darkMode}
|
| 666 |
+
onRefreshProducts={loadReviews}
|
| 667 |
+
/>
|
| 668 |
+
</motion.div>
|
| 669 |
+
)}
|
| 670 |
+
|
| 671 |
+
</main>
|
| 672 |
+
|
| 673 |
+
{/* COMPREHENSIVE HIGH SEO FOOTER ROW SUMMARY SECTION */}
|
| 674 |
+
<footer className={`pt-16 pb-12 relative overflow-hidden transition-colors duration-300 border-t ${
|
| 675 |
+
darkMode ? "bg-brand-dark border-white/5 text-slate-400" : "bg-[#f1f3f6] border-slate-200 text-slate-600"
|
| 676 |
+
}`}>
|
| 677 |
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10 text-xs">
|
| 678 |
+
<div className="grid grid-cols-1 md:grid-cols-4 gap-12 mb-12">
|
| 679 |
+
|
| 680 |
+
{/* Column 1: About imports */}
|
| 681 |
+
<div className="md:col-span-1.5 space-y-4">
|
| 682 |
+
<div className="flex items-center">
|
| 683 |
+
<Logo variant="full" darkMode={darkMode} className="h-12 w-auto -ml-3" />
|
| 684 |
+
</div>
|
| 685 |
+
<p className="leading-relaxed">
|
| 686 |
+
Premium quality wholesaler and certified dealer of brand new tyres. We specialize in providing verified fresh manufacturing date imports, computer balancing, and 3D wheel alignments in Faisal Town Lahore.
|
| 687 |
+
</p>
|
| 688 |
+
<div className="flex items-center space-x-1 font-mono text-[10px] text-brand-orange">
|
| 689 |
+
<span>⭐ 4.8 Ratings • Direct Google Mapping verified</span>
|
| 690 |
+
</div>
|
| 691 |
+
</div>
|
| 692 |
+
|
| 693 |
+
{/* Column 2: Quick navigation */}
|
| 694 |
+
<div className="md:col-span-0.5 space-y-3 font-mono">
|
| 695 |
+
<h4 className={`font-display font-medium text-xs uppercase tracking-widest ${darkMode ? "text-slate-200" : "text-slate-950"}`}>
|
| 696 |
+
Navigation
|
| 697 |
+
</h4>
|
| 698 |
+
<div className="flex flex-col space-y-2">
|
| 699 |
+
<button onClick={() => { setCurrentPage("home"); window.scrollTo({ top: 0 }); }} className="text-left hover:text-brand-orange transition-colors">Showroom Home</button>
|
| 700 |
+
<button onClick={() => { setCurrentPage("catalogue"); window.scrollTo({ top: 0 }); }} className="text-left hover:text-brand-orange transition-colors">Tyre Catalogue</button>
|
| 701 |
+
<button onClick={() => { setCurrentPage("reviews"); window.scrollTo({ top: 0 }); }} className="text-left hover:text-brand-orange transition-colors">Customer Feed</button>
|
| 702 |
+
<button onClick={() => { setCurrentPage("showroom"); window.scrollTo({ top: 0 }); }} className="text-left hover:text-brand-orange transition-colors">Lahore Location</button>
|
| 703 |
+
{isAuthorizedAdmin && (
|
| 704 |
+
<button onClick={() => { setCurrentPage("admin"); window.scrollTo({ top: 0 }); }} className="text-left hover:text-brand-orange transition-colors flex items-center gap-1">
|
| 705 |
+
<span>Inventory Manager</span>
|
| 706 |
+
</button>
|
| 707 |
+
)}
|
| 708 |
+
</div>
|
| 709 |
+
</div>
|
| 710 |
+
|
| 711 |
+
{/* Column 3: Contact highlights & Location */}
|
| 712 |
+
<div className="md:col-span-1 space-y-3 font-mono">
|
| 713 |
+
<h4 className={`font-display font-medium text-xs uppercase tracking-widest ${darkMode ? "text-slate-200" : "text-slate-950"}`}>
|
| 714 |
+
Showroom Contact
|
| 715 |
+
</h4>
|
| 716 |
+
<div className="flex flex-col space-y-1.5">
|
| 717 |
+
<a href={`tel:${BUSINESS_INFO.phoneRaw}`} className="hover:text-brand-orange transition-colors">
|
| 718 |
+
📞 Emergency: {BUSINESS_INFO.phone}
|
| 719 |
+
</a>
|
| 720 |
+
<a href={`${BUSINESS_INFO.whatsappUrl}?text=I'd%20like%20to%20inquire%20about%20tires`} target="_blank" rel="noreferrer" className="hover:text-brand-orange transition-colors">
|
| 721 |
+
💬 WhatsApp: Chat Live
|
| 722 |
+
</a>
|
| 723 |
+
<div className="text-slate-500 font-sans text-xs pt-1">
|
| 724 |
+
📍 {BUSINESS_INFO.address}
|
| 725 |
+
</div>
|
| 726 |
+
<div className="flex items-center space-x-1 text-brand-orange text-[9px] uppercase font-bold tracking-wider pt-0.5">
|
| 727 |
+
<MapPin className="w-3.5 h-3.5" />
|
| 728 |
+
<span>Faisal Town Lahore</span>
|
| 729 |
+
</div>
|
| 730 |
+
</div>
|
| 731 |
+
</div>
|
| 732 |
+
|
| 733 |
+
{/* Column 4: Newsletter signup */}
|
| 734 |
+
<div className="md:col-span-1">
|
| 735 |
+
<NewsletterSignup darkMode={darkMode} />
|
| 736 |
+
</div>
|
| 737 |
+
|
| 738 |
+
</div>
|
| 739 |
+
|
| 740 |
+
<div className="pt-8 border-t border-black/5 dark:border-white/5 flex flex-col sm:flex-row items-center justify-between text-[11px] text-slate-500 text-center">
|
| 741 |
+
<p className="mb-4 sm:mb-0">
|
| 742 |
+
© {new Date().getFullYear()} Haider Brothers Traders Lahore. All Rights Reserved. Fully optimized in production under Pakistan wholesale regulations.
|
| 743 |
+
<button
|
| 744 |
+
onClick={() => {
|
| 745 |
+
if (isAuthorizedAdmin) {
|
| 746 |
+
setCurrentPage("admin");
|
| 747 |
+
window.scrollTo({ top: 0 });
|
| 748 |
+
} else {
|
| 749 |
+
setShowAdminLoginModal(true);
|
| 750 |
+
}
|
| 751 |
+
}}
|
| 752 |
+
className="opacity-40 hover:opacity-100 transition-opacity ml-2 cursor-pointer inline-flex items-center gap-1 text-[10px]"
|
| 753 |
+
id="subtle-staff-entrance"
|
| 754 |
+
title="Management Console"
|
| 755 |
+
>
|
| 756 |
+
<Lock className="w-2.5 h-2.5 text-brand-orange inline" />
|
| 757 |
+
<span>Staff Portal</span>
|
| 758 |
+
</button>
|
| 759 |
+
</p>
|
| 760 |
+
<div className="flex items-center space-x-1 text-center justify-center">
|
| 761 |
+
<span>Drive Safely with certified tyre sizes</span>
|
| 762 |
+
<Heart className="w-3.5 h-3.5 text-brand-orange fill-current" />
|
| 763 |
+
<span>& robust grip safety rules</span>
|
| 764 |
+
</div>
|
| 765 |
+
</div>
|
| 766 |
+
</div>
|
| 767 |
+
</footer>
|
| 768 |
+
|
| 769 |
+
{/* Interactive Detail Modal view */}
|
| 770 |
+
<AnimatePresence>
|
| 771 |
+
{selectedTire && (
|
| 772 |
+
<ProductDetailsModal
|
| 773 |
+
tire={selectedTire}
|
| 774 |
+
onClose={() => setSelectedTire(null)}
|
| 775 |
+
darkMode={darkMode}
|
| 776 |
+
/>
|
| 777 |
+
)}
|
| 778 |
+
</AnimatePresence>
|
| 779 |
+
|
| 780 |
+
{/* Staff Authentication Portal Modal */}
|
| 781 |
+
<AdminLoginModal
|
| 782 |
+
isOpen={showAdminLoginModal}
|
| 783 |
+
onClose={() => setShowAdminLoginModal(false)}
|
| 784 |
+
onSuccess={() => {
|
| 785 |
+
setCurrentPage("admin");
|
| 786 |
+
window.scrollTo({ top: 0 });
|
| 787 |
+
}}
|
| 788 |
+
darkMode={darkMode}
|
| 789 |
+
/>
|
| 790 |
+
|
| 791 |
+
{/* Floating Corner CTAs handles */}
|
| 792 |
+
<FloatingActions
|
| 793 |
+
darkMode={darkMode}
|
| 794 |
+
selectedTire={selectedTire}
|
| 795 |
+
/>
|
| 796 |
+
</div>
|
| 797 |
+
);
|
| 798 |
+
}
|
src/assets/images/comfort_tire_1781509452553.jpg
ADDED
|
Git LFS Details
|
src/assets/images/sport_tire_1781509490802.jpg
ADDED
|
Git LFS Details
|
src/assets/images/suv_tire_1781509472580.jpg
ADDED
|
Git LFS Details
|
src/assets/images/winter_tire_1781509513584.jpg
ADDED
|
Git LFS Details
|
src/components/AdminLoginModal.tsx
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from "react";
|
| 2 |
+
import { X, Lock, RefreshCw, Mail, KeyRound } from "lucide-react";
|
| 3 |
+
import { loginAdmin } from "../lib/firebaseService";
|
| 4 |
+
|
| 5 |
+
interface AdminLoginModalProps {
|
| 6 |
+
isOpen: boolean;
|
| 7 |
+
onClose: () => void;
|
| 8 |
+
onSuccess: () => void;
|
| 9 |
+
darkMode: boolean;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export default function AdminLoginModal({ isOpen, onClose, onSuccess, darkMode }: AdminLoginModalProps) {
|
| 13 |
+
const [email, setEmail] = useState("");
|
| 14 |
+
const [password, setPassword] = useState("");
|
| 15 |
+
const [loading, setLoading] = useState(false);
|
| 16 |
+
const [error, setError] = useState("");
|
| 17 |
+
|
| 18 |
+
if (!isOpen) return null;
|
| 19 |
+
|
| 20 |
+
const handleSubmit = async (e: React.FormEvent) => {
|
| 21 |
+
e.preventDefault();
|
| 22 |
+
if (!email || !password) {
|
| 23 |
+
setError("Please fill in all fields.");
|
| 24 |
+
return;
|
| 25 |
+
}
|
| 26 |
+
setLoading(true);
|
| 27 |
+
setError("");
|
| 28 |
+
|
| 29 |
+
try {
|
| 30 |
+
await loginAdmin(email, password);
|
| 31 |
+
onSuccess();
|
| 32 |
+
onClose();
|
| 33 |
+
} catch (err: any) {
|
| 34 |
+
setError(err.message || "Invalid authentication credentials.");
|
| 35 |
+
} finally {
|
| 36 |
+
setLoading(false);
|
| 37 |
+
}
|
| 38 |
+
};
|
| 39 |
+
|
| 40 |
+
return (
|
| 41 |
+
<div className="fixed inset-0 z-55 flex items-center justify-center p-4 bg-black/70 backdrop-blur-xs">
|
| 42 |
+
<div
|
| 43 |
+
className={`w-full max-w-md p-6 sm:p-8 rounded-3xl border shadow-2xl relative transition-colors duration-300 ${
|
| 44 |
+
darkMode ? "bg-[#1c1c24] border-white/10 text-white" : "bg-white border-slate-200 text-slate-900"
|
| 45 |
+
}`}
|
| 46 |
+
>
|
| 47 |
+
<button
|
| 48 |
+
onClick={onClose}
|
| 49 |
+
className={`absolute top-4 right-4 p-1.5 rounded-lg transition-colors ${
|
| 50 |
+
darkMode ? "hover:bg-white/10 text-slate-400 hover:text-white" : "hover:bg-black/5 text-slate-500 hover:text-black"
|
| 51 |
+
}`}
|
| 52 |
+
>
|
| 53 |
+
<X className="w-5 h-5" />
|
| 54 |
+
</button>
|
| 55 |
+
|
| 56 |
+
<div className="flex items-center space-x-3 mb-6">
|
| 57 |
+
<div className="p-2.5 rounded-xl bg-brand-orange/15 text-brand-orange">
|
| 58 |
+
<Lock className="w-6 h-6" />
|
| 59 |
+
</div>
|
| 60 |
+
<div>
|
| 61 |
+
<h3 className="font-display font-extrabold text-lg text-brand-orange">Staff Authentication</h3>
|
| 62 |
+
<p className={`text-xs ${darkMode ? "text-slate-400" : "text-slate-500"}`}>
|
| 63 |
+
Direct database verification for Haider Brothers Traders admin
|
| 64 |
+
</p>
|
| 65 |
+
</div>
|
| 66 |
+
</div>
|
| 67 |
+
|
| 68 |
+
{error && (
|
| 69 |
+
<div className="mb-4 p-3 bg-red-600/15 border border-red-500/20 text-red-400 text-xs rounded-xl font-medium">
|
| 70 |
+
{error}
|
| 71 |
+
</div>
|
| 72 |
+
)}
|
| 73 |
+
|
| 74 |
+
<form onSubmit={handleSubmit} className="space-y-4">
|
| 75 |
+
<div>
|
| 76 |
+
<label className="text-[10px] uppercase font-mono tracking-wider block mb-1.5">
|
| 77 |
+
Secure Email Address
|
| 78 |
+
</label>
|
| 79 |
+
<div className="relative">
|
| 80 |
+
<input
|
| 81 |
+
type="email"
|
| 82 |
+
required
|
| 83 |
+
placeholder="bahaduralimunnabhai@gmail.com"
|
| 84 |
+
value={email}
|
| 85 |
+
onChange={(e) => setEmail(e.target.value)}
|
| 86 |
+
className={`w-full p-2.5 pl-9 rounded-xl text-xs border focus:outline-none focus:ring-2 focus:ring-brand-orange/40 focus:border-brand-orange ${
|
| 87 |
+
darkMode ? "bg-black/40 border-white/10 text-white" : "bg-slate-50 border-slate-200"
|
| 88 |
+
}`}
|
| 89 |
+
/>
|
| 90 |
+
<Mail className="w-4 h-4 text-slate-500 absolute left-3 top-1/2 -translate-y-1/2" />
|
| 91 |
+
</div>
|
| 92 |
+
</div>
|
| 93 |
+
|
| 94 |
+
<div>
|
| 95 |
+
<label className="text-[10px] uppercase font-mono tracking-wider block mb-1.5">
|
| 96 |
+
Enter Password
|
| 97 |
+
</label>
|
| 98 |
+
<div className="relative">
|
| 99 |
+
<input
|
| 100 |
+
type="password"
|
| 101 |
+
required
|
| 102 |
+
placeholder="••••••••"
|
| 103 |
+
value={password}
|
| 104 |
+
onChange={(e) => setPassword(e.target.value)}
|
| 105 |
+
className={`w-full p-2.5 pl-9 rounded-xl text-xs border focus:outline-none focus:ring-2 focus:ring-brand-orange/40 focus:border-brand-orange ${
|
| 106 |
+
darkMode ? "bg-black/40 border-white/10 text-white" : "bg-slate-50 border-slate-200"
|
| 107 |
+
}`}
|
| 108 |
+
/>
|
| 109 |
+
<KeyRound className="w-4 h-4 text-slate-500 absolute left-3 top-1/2 -translate-y-1/2" />
|
| 110 |
+
</div>
|
| 111 |
+
</div>
|
| 112 |
+
|
| 113 |
+
<button
|
| 114 |
+
type="submit"
|
| 115 |
+
disabled={loading}
|
| 116 |
+
className="w-full bg-gradient-to-r from-brand-orange to-red-600 hover:from-brand-orange-light hover:to-red-500 text-white font-bold p-3 rounded-xl text-xs flex items-center justify-center space-x-1.5 shadow-md shadow-brand-orange/20 transition-all cursor-pointer mt-2"
|
| 117 |
+
>
|
| 118 |
+
{loading ? (
|
| 119 |
+
<RefreshCw className="w-3.5 h-3.5 animate-spin" />
|
| 120 |
+
) : (
|
| 121 |
+
<Lock className="w-3.5 h-3.5" />
|
| 122 |
+
)}
|
| 123 |
+
<span>Verify Credentials</span>
|
| 124 |
+
</button>
|
| 125 |
+
</form>
|
| 126 |
+
</div>
|
| 127 |
+
</div>
|
| 128 |
+
);
|
| 129 |
+
}
|
src/components/FAQSection.tsx
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from "react";
|
| 2 |
+
import { motion, AnimatePresence } from "motion/react";
|
| 3 |
+
import {
|
| 4 |
+
HelpCircle,
|
| 5 |
+
ChevronDown,
|
| 6 |
+
ShieldCheck,
|
| 7 |
+
Wrench,
|
| 8 |
+
Clock,
|
| 9 |
+
Search,
|
| 10 |
+
BadgeHelp,
|
| 11 |
+
Tag
|
| 12 |
+
} from "lucide-react";
|
| 13 |
+
|
| 14 |
+
interface FAQItem {
|
| 15 |
+
id: string;
|
| 16 |
+
question: string;
|
| 17 |
+
answer: string;
|
| 18 |
+
keywords: string[];
|
| 19 |
+
category: "warranty" | "services" | "general";
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
interface FAQSectionProps {
|
| 23 |
+
darkMode: boolean;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
export default function FAQSection({ darkMode }: FAQSectionProps) {
|
| 27 |
+
const [openId, setOpenId] = useState<string | null>("warranty-duration");
|
| 28 |
+
const [filterCategory, setFilterCategory] = useState<"all" | "warranty" | "services">("all");
|
| 29 |
+
const [searchQuery, setSearchQuery] = useState("");
|
| 30 |
+
|
| 31 |
+
const FAQS: FAQItem[] = [
|
| 32 |
+
{
|
| 33 |
+
id: "warranty-duration",
|
| 34 |
+
question: "What official warranty is provided with premium imported tyres from Yokohama, Michelin, and Continental?",
|
| 35 |
+
answer: "All premium brand tyres (including Yokohama, Michelin, and Continental) purchased from Haider Brothers Faisal Town come with a 5-Year Continuous Structural & Tread Warranty. This warranty protects against tread separation, structural defects, factory compounds failures, and early bulging under standard driving specifications. We assist with 100% genuine claims fast-tracked directly with manufacturer dealers without complications.",
|
| 36 |
+
keywords: ["warranty Yokohama", "Michelin warranty", "Continental tyres Lahore", "imported tyre warranty"],
|
| 37 |
+
category: "warranty"
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
id: "tire-installation-benefits",
|
| 41 |
+
question: "Is there a charge for tyre installation, computerized balancing, and nitrogen air filling in Faisal Town showroom?",
|
| 42 |
+
answer: "No, tyre installation at our Faisal Town showroom is 100% free with the purchase of any tyre set. For seasonal promotional packages or sets of 4 premium tyres, we include computerized 3D wheel alignment, premium computerized wheel balancing, durable tubeless valves, and lifetime nitrogen gas top-up sessions absolutely free. No hidden service charges apply.",
|
| 43 |
+
keywords: ["free tyre installation", "3D wheel alignment", "wheel balancing Faisal Town", "free nitrogen Lahore"],
|
| 44 |
+
category: "services"
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
id: "checking-dot-year",
|
| 48 |
+
question: "How can I verify the genuine manufacturing year and DOT marking of tyres purchased at Haider Brothers?",
|
| 49 |
+
answer: "We guarantee 100% authentic tyre imports with visible, untouched DOT stamps. The DOT code is a 4-digit code hot-stamped on the tire sidewall; the first two digits represent the manufacturing week, and the last two represent the year (e.g., '1226' means 12th week of 2026). Haider Brothers never stocks smuggled or expired stock, securing complete legal peace of mind with legal custom imports.",
|
| 50 |
+
keywords: ["tyre manufacturing year", "how to read DOT markings", "genuine imported tyres", "fresh tyre stock Lahore"],
|
| 51 |
+
category: "warranty"
|
| 52 |
+
},
|
| 53 |
+
{
|
| 54 |
+
id: "pothole-road-hazard",
|
| 55 |
+
question: "Does the warranty cover tyre punctures, roadside sidewall cuts, or pothole impact damage?",
|
| 56 |
+
answer: "Official manufacturer structural warranties cover material defects and manufacturing flaws. Road hazards such as standard tyre punctures, impact damage from deep Lahore potholes, sidewall kerb cuts, or debris damage are not traditionally covered under standard structural warranty terms. However, we offer premium tyre repair and patching services at our showroom anytime, or immediate tyre roadside replacements via our emergency van.",
|
| 57 |
+
keywords: ["pothole tyre damage warranty", "sidewall cuts repair Lahore", "tire puncture service", "road hazard assistance"],
|
| 58 |
+
category: "warranty"
|
| 59 |
+
},
|
| 60 |
+
{
|
| 61 |
+
id: "emergency-road-service",
|
| 62 |
+
question: "What should I do if my sedan tire bursts late at night in Lahore? Do you offer 24/7 emergency roadside support?",
|
| 63 |
+
answer: "Yes, Haider Brothers operates a dedicated 24 Hours Emergency Roadside Tyre Assistance service. If you encounter a flat tire, puncture, or rim issue late at night anywhere around Faisal Town, Model Town, Gulberg, Johar Town, or Garden Town, call our emergency hotline at +92 302 4594403 immediately. Our responsive mobile service van will dispatch with fresh stock to assist on-site.",
|
| 64 |
+
keywords: ["24/7 tyre repair Lahore", "midnight flat tire recovery", "emergency roadside assistance", "mobile tire van Faisal Town"],
|
| 65 |
+
category: "services"
|
| 66 |
+
},
|
| 67 |
+
{
|
| 68 |
+
id: "alignment-frequency",
|
| 69 |
+
question: "How often should I get computerized 3D wheel alignment checks and wheel balancing for Pakistani roads?",
|
| 70 |
+
answer: "For Pakistani road terrains, we strongly recommend a computerized 3D wheel alignment and professional wheel balancing check every 8,000 to 10,000 Kilometers, or whenever you feel alignment pulling to one side. Regular alignments prevent premature tread wear (uneven tire scrubbing), keep steering vibration-free on motorways at high speeds, and optimize vehicle fuel economy.",
|
| 71 |
+
keywords: ["computerized wheel alignment frequency", "tire balancing intervals", "how to prevent uneven tire wear", "Pakistani road maintenance"],
|
| 72 |
+
category: "services"
|
| 73 |
+
}
|
| 74 |
+
];
|
| 75 |
+
|
| 76 |
+
const toggleFAQ = (id: string) => {
|
| 77 |
+
setOpenId((prev) => (prev === id ? null : id));
|
| 78 |
+
};
|
| 79 |
+
|
| 80 |
+
// Filter and search logic for great SEO reach
|
| 81 |
+
const filteredFAQs = FAQS.filter((faq) => {
|
| 82 |
+
const categoryMatches = filterCategory === "all" || faq.category === filterCategory;
|
| 83 |
+
const searchString = `${faq.question} ${faq.answer} ${faq.keywords.join(" ")}`.toLowerCase();
|
| 84 |
+
const queryMatches = searchString.includes(searchQuery.toLowerCase());
|
| 85 |
+
return categoryMatches && queryMatches;
|
| 86 |
+
});
|
| 87 |
+
|
| 88 |
+
return (
|
| 89 |
+
<section
|
| 90 |
+
id="showroom-faqs-accordion"
|
| 91 |
+
className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 mt-16 pt-12 border-t border-black/5 dark:border-white/5"
|
| 92 |
+
>
|
| 93 |
+
{/* Schema.org Structured Data Injection for high-fidelity Google SEO relevance */}
|
| 94 |
+
<script type="application/ld+json">
|
| 95 |
+
{JSON.stringify({
|
| 96 |
+
"@context": "https://schema.org",
|
| 97 |
+
"@type": "FAQPage",
|
| 98 |
+
"mainEntity": FAQS.map((faq) => ({
|
| 99 |
+
"@type": "Question",
|
| 100 |
+
"name": faq.question,
|
| 101 |
+
"acceptedAnswer": {
|
| 102 |
+
"@type": "Answer",
|
| 103 |
+
"text": faq.answer
|
| 104 |
+
}
|
| 105 |
+
}))
|
| 106 |
+
})}
|
| 107 |
+
</script>
|
| 108 |
+
|
| 109 |
+
<div className="text-center max-w-2xl mx-auto mb-8">
|
| 110 |
+
<span className="text-xs font-mono tracking-widest uppercase bg-brand-orange/15 text-brand-orange px-3.5 py-1 rounded-full font-bold inline-flex items-center gap-1.5 shadow-xs">
|
| 111 |
+
<BadgeHelp className="w-3.5 h-3.5 animate-pulse" />
|
| 112 |
+
<span>Haider Brothers FAQ Hub</span>
|
| 113 |
+
</span>
|
| 114 |
+
<h2 className="text-2xl sm:text-3.5xl font-display font-black tracking-tight mt-3">
|
| 115 |
+
Tire Warranty & Service FAQs
|
| 116 |
+
</h2>
|
| 117 |
+
<p className={`text-xs sm:text-sm mt-2 leading-relaxed ${darkMode ? "text-slate-400" : "text-slate-600"}`}>
|
| 118 |
+
Honest, transparent answers about premium tyre warranties, alignment services, and emergency 24/7 roadside assistances in Faisal Town Lahore.
|
| 119 |
+
</p>
|
| 120 |
+
</div>
|
| 121 |
+
|
| 122 |
+
{/* Segment Category Switcher + SEO Search Input */}
|
| 123 |
+
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 justify-between mb-6">
|
| 124 |
+
<div className="flex p-1 rounded-xl bg-slate-200/50 dark:bg-black/30 border border-slate-300/30 dark:border-white/5 self-start">
|
| 125 |
+
{[
|
| 126 |
+
{ id: "all", label: "All Questions" },
|
| 127 |
+
{ id: "warranty", label: "🛡️ Warranty" },
|
| 128 |
+
{ id: "services", label: "🛠️ Services & 3D Align" }
|
| 129 |
+
].map((cat) => (
|
| 130 |
+
<button
|
| 131 |
+
key={cat.id}
|
| 132 |
+
onClick={() => setFilterCategory(cat.id as any)}
|
| 133 |
+
className={`px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
|
| 134 |
+
filterCategory === cat.id
|
| 135 |
+
? "bg-brand-orange text-white"
|
| 136 |
+
: "text-slate-500 hover:text-slate-800 dark:hover:text-slate-200"
|
| 137 |
+
}`}
|
| 138 |
+
type="button"
|
| 139 |
+
>
|
| 140 |
+
{cat.label}
|
| 141 |
+
</button>
|
| 142 |
+
))}
|
| 143 |
+
</div>
|
| 144 |
+
|
| 145 |
+
{/* Live Search inside FAQ content with key phrases */}
|
| 146 |
+
<div className="relative max-w-xs w-full">
|
| 147 |
+
<input
|
| 148 |
+
type="text"
|
| 149 |
+
placeholder="Search FAQs (e.g. alignment, DOT, Michelin)..."
|
| 150 |
+
value={searchQuery}
|
| 151 |
+
onChange={(e) => setSearchQuery(e.target.value)}
|
| 152 |
+
className={`w-full p-2.5 pl-8 text-xs rounded-xl border focus:ring-1 focus:ring-brand-orange focus:outline-hidden ${
|
| 153 |
+
darkMode
|
| 154 |
+
? "bg-black/35 border-white/10 text-white placeholder-slate-500"
|
| 155 |
+
: "bg-white border-slate-200 text-slate-850 placeholder-slate-400"
|
| 156 |
+
}`}
|
| 157 |
+
/>
|
| 158 |
+
<Search className="w-3.5 h-3.5 text-slate-400 absolute left-2.5 top-1/2 -translate-y-1/2" />
|
| 159 |
+
</div>
|
| 160 |
+
</div>
|
| 161 |
+
|
| 162 |
+
{/* Accordion List Container */}
|
| 163 |
+
{filteredFAQs.length > 0 ? (
|
| 164 |
+
<div className="space-y-3.5">
|
| 165 |
+
{filteredFAQs.map((faq) => {
|
| 166 |
+
const isOpen = openId === faq.id;
|
| 167 |
+
return (
|
| 168 |
+
<div
|
| 169 |
+
key={faq.id}
|
| 170 |
+
className={`rounded-2xl border transition-all duration-300 overflow-hidden ${
|
| 171 |
+
isOpen
|
| 172 |
+
? darkMode
|
| 173 |
+
? "bg-[#1c1c24] border-brand-orange/40"
|
| 174 |
+
: "bg-white border-brand-orange shadow-md"
|
| 175 |
+
: darkMode
|
| 176 |
+
? "bg-black/25 border-white/5 hover:border-white/10"
|
| 177 |
+
: "bg-white border-slate-200 hover:border-slate-300"
|
| 178 |
+
}`}
|
| 179 |
+
id={`faq-item-${faq.id}`}
|
| 180 |
+
>
|
| 181 |
+
{/* Header Interactive Trigger Button */}
|
| 182 |
+
<button
|
| 183 |
+
onClick={() => toggleFAQ(faq.id)}
|
| 184 |
+
className="w-full p-4 sm:p-5 flex items-center justify-between font-sans font-bold text-xs sm:text-sm text-left transition-colors focus:outline-hidden relative group"
|
| 185 |
+
aria-expanded={isOpen}
|
| 186 |
+
type="button"
|
| 187 |
+
>
|
| 188 |
+
<div className="flex items-start space-x-3 pr-4">
|
| 189 |
+
{faq.category === "warranty" ? (
|
| 190 |
+
<ShieldCheck className="w-4 h-4 text-brand-orange mt-0.5 shrink-0" />
|
| 191 |
+
) : faq.category === "services" ? (
|
| 192 |
+
<Wrench className="w-4 h-4 text-brand-orange mt-0.5 shrink-0" />
|
| 193 |
+
) : (
|
| 194 |
+
<HelpCircle className="w-4 h-4 text-brand-orange mt-0.5 shrink-0" />
|
| 195 |
+
)}
|
| 196 |
+
<span className={darkMode ? "text-slate-100 group-hover:text-white" : "text-slate-850 group-hover:text-brand-orange"}>
|
| 197 |
+
{faq.question}
|
| 198 |
+
</span>
|
| 199 |
+
</div>
|
| 200 |
+
|
| 201 |
+
<span className={`p-1 rounded-lg transition-transform duration-300 ${
|
| 202 |
+
isOpen ? "bg-brand-orange/15 text-brand-orange rotate-180" : "text-slate-400 hover:text-slate-600"
|
| 203 |
+
}`}>
|
| 204 |
+
<ChevronDown className="w-4 h-4 shrink-0" />
|
| 205 |
+
</span>
|
| 206 |
+
</button>
|
| 207 |
+
|
| 208 |
+
{/* Smooth Expandable Accordion Body Panels */}
|
| 209 |
+
<AnimatePresence initial={false}>
|
| 210 |
+
{isOpen && (
|
| 211 |
+
<motion.div
|
| 212 |
+
initial={{ height: 0, opacity: 0 }}
|
| 213 |
+
animate={{ height: "auto", opacity: 1 }}
|
| 214 |
+
exit={{ height: 0, opacity: 0 }}
|
| 215 |
+
transition={{ duration: 0.25, ease: "easeInOut" }}
|
| 216 |
+
>
|
| 217 |
+
<div className={`p-4 sm:p-5 pt-0 border-t font-sans text-xs sm:text-[13px] leading-relaxed select-none ${
|
| 218 |
+
darkMode ? "border-white/5 text-slate-300" : "border-slate-100 text-slate-700"
|
| 219 |
+
}`}>
|
| 220 |
+
<p>{faq.answer}</p>
|
| 221 |
+
|
| 222 |
+
{/* SEO Keyword tags indicator for algorithmic reach */}
|
| 223 |
+
<div className="mt-3.5 flex flex-wrap gap-1.5 items-center opacity-80">
|
| 224 |
+
<Tag className="w-3 h-3 text-brand-orange shrink-0" />
|
| 225 |
+
<span className="text-[10px] font-mono text-slate-400">Related indexes:</span>
|
| 226 |
+
{faq.keywords.map((kw, i) => (
|
| 227 |
+
<span
|
| 228 |
+
key={i}
|
| 229 |
+
className="text-[9px] font-mono bg-brand-orange/5 text-brand-orange/80 px-2 py-0.5 rounded-md border border-brand-orange/10"
|
| 230 |
+
>
|
| 231 |
+
{kw}
|
| 232 |
+
</span>
|
| 233 |
+
))}
|
| 234 |
+
</div>
|
| 235 |
+
</div>
|
| 236 |
+
</motion.div>
|
| 237 |
+
)}
|
| 238 |
+
</AnimatePresence>
|
| 239 |
+
</div>
|
| 240 |
+
);
|
| 241 |
+
})}
|
| 242 |
+
</div>
|
| 243 |
+
) : (
|
| 244 |
+
<div className={`p-10 text-center rounded-2xl border border-dashed text-slate-400 ${
|
| 245 |
+
darkMode ? "bg-white/5 border-white/10" : "bg-slate-50 border-slate-200"
|
| 246 |
+
}`}>
|
| 247 |
+
<HelpCircle className="w-8 h-8 mx-auto text-slate-500 mb-2 animate-bounce" />
|
| 248 |
+
<h4 className="font-bold text-xs">No matching faq answers found</h4>
|
| 249 |
+
<p className="text-[11px] mt-1">Try searching for simple words like "warranty", "3D alignment", or "DOT".</p>
|
| 250 |
+
</div>
|
| 251 |
+
)}
|
| 252 |
+
|
| 253 |
+
{/* Extra Service Pledge stamp */}
|
| 254 |
+
<div className={`mt-8 p-4 rounded-2xl border text-center font-mono text-[10px] ${
|
| 255 |
+
darkMode ? "bg-brand-orange/5 border-brand-orange/10" : "bg-orange-50/50 border-brand-orange/20"
|
| 256 |
+
}`}>
|
| 257 |
+
<span className="text-brand-orange font-bold font-sans">🛡️ CERTIFIED DIRECT CLAIM GUARANTOR: </span>
|
| 258 |
+
<span className={darkMode ? "text-slate-400" : "text-slate-600"}>We deal strictly in direct legal brand imports on all 2026 batches. No duplicate or high wear compounds guaranteed.</span>
|
| 259 |
+
</div>
|
| 260 |
+
</section>
|
| 261 |
+
);
|
| 262 |
+
}
|
src/components/FloatingActions.tsx
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { motion, AnimatePresence } from "motion/react";
|
| 2 |
+
import { Phone, MessageSquare } from "lucide-react";
|
| 3 |
+
import { BUSINESS_INFO, TireProduct } from "../data";
|
| 4 |
+
|
| 5 |
+
interface FloatingActionsProps {
|
| 6 |
+
darkMode: boolean;
|
| 7 |
+
selectedTire?: TireProduct | null;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export default function FloatingActions({ darkMode, selectedTire }: FloatingActionsProps) {
|
| 11 |
+
// Format dynamic WhatsApp URL
|
| 12 |
+
const getWhatsAppUrl = () => {
|
| 13 |
+
if (!selectedTire) {
|
| 14 |
+
return `${BUSINESS_INFO.whatsappUrl}?text=${encodeURIComponent(
|
| 15 |
+
"Hello Haider Brothers, I need assistance with tyres right now"
|
| 16 |
+
)}`;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const message = `Hello Haider Brothers, I'm interested in the following tyre:
|
| 20 |
+
|
| 21 |
+
🚗 *Tyre details:*
|
| 22 |
+
• *Brand:* ${selectedTire.brand}
|
| 23 |
+
• *Model:* ${selectedTire.name}
|
| 24 |
+
• *Size:* ${selectedTire.size}
|
| 25 |
+
• *Features:* ${selectedTire.feature}
|
| 26 |
+
• *Price:* PKR ${selectedTire.price.toLocaleString()}
|
| 27 |
+
• *Stock status:* ${selectedTire.stock !== undefined ? `${selectedTire.stock} Units Available` : "Available"}
|
| 28 |
+
|
| 29 |
+
Could you please confirm the current stock and availability for installation? Thank you!`;
|
| 30 |
+
|
| 31 |
+
return `${BUSINESS_INFO.whatsappUrl}?text=${encodeURIComponent(message)}`;
|
| 32 |
+
};
|
| 33 |
+
|
| 34 |
+
const hasSelectedTire = !!selectedTire;
|
| 35 |
+
|
| 36 |
+
return (
|
| 37 |
+
<div className="fixed bottom-6 right-6 z-40 flex flex-col space-y-3.5 pointer-events-auto items-end">
|
| 38 |
+
{/* Alert Tooltip for Selected Tyre */}
|
| 39 |
+
<AnimatePresence>
|
| 40 |
+
{selectedTire && (
|
| 41 |
+
<motion.div
|
| 42 |
+
initial={{ opacity: 0, scale: 0.85, y: 15 }}
|
| 43 |
+
animate={{ opacity: 1, scale: 1, y: 0 }}
|
| 44 |
+
exit={{ opacity: 0, scale: 0.85, y: 15 }}
|
| 45 |
+
className={`mr-1 mb-1 p-3.5 rounded-2xl shadow-xl max-w-xs border text-left scale-95 origin-bottom-right flex flex-col space-y-1 backdrop-blur-md ${
|
| 46 |
+
darkMode
|
| 47 |
+
? "bg-black/95 border-white/10 text-white"
|
| 48 |
+
: "bg-white/95 border-slate-200 text-slate-800"
|
| 49 |
+
}`}
|
| 50 |
+
>
|
| 51 |
+
<div className="flex items-center space-x-1.5 text-brand-orange text-[10px] uppercase font-mono tracking-wider font-extrabold">
|
| 52 |
+
<span className="w-1.5 h-1.5 rounded-full bg-brand-orange animate-ping shrink-0" />
|
| 53 |
+
<span>Direct Product Inquiry</span>
|
| 54 |
+
</div>
|
| 55 |
+
<p className={`text-xs font-bold leading-tight line-clamp-1 ${darkMode ? "text-slate-100" : "text-slate-850"}`}>
|
| 56 |
+
{selectedTire.brand} {selectedTire.name}
|
| 57 |
+
</p>
|
| 58 |
+
<p className={`text-[10px] leading-snug ${darkMode ? "text-slate-400" : "text-slate-500"}`}>
|
| 59 |
+
Click below to send instant specifications over WhatsApp!
|
| 60 |
+
</p>
|
| 61 |
+
</motion.div>
|
| 62 |
+
)}
|
| 63 |
+
</AnimatePresence>
|
| 64 |
+
|
| 65 |
+
{/* Clickable Phone Call dialer */}
|
| 66 |
+
<a
|
| 67 |
+
href={`tel:${BUSINESS_INFO.phoneRaw}`}
|
| 68 |
+
className="w-12 h-12 rounded-full bg-gradient-to-br from-brand-orange to-red-600 text-white flex items-center justify-center shadow-lg shadow-brand-orange/30 hover:scale-110 active:scale-95 transition-all group"
|
| 69 |
+
title="Dial emergency tire service line"
|
| 70 |
+
id="floating-call-btn"
|
| 71 |
+
>
|
| 72 |
+
<Phone className="w-5 h-5 group-hover:rotate-12 transition-transform" />
|
| 73 |
+
</a>
|
| 74 |
+
|
| 75 |
+
{/* Floating interactive WhatsApp */}
|
| 76 |
+
<a
|
| 77 |
+
href={getWhatsAppUrl()}
|
| 78 |
+
target="_blank"
|
| 79 |
+
rel="noreferrer referrer"
|
| 80 |
+
className={`relative w-12 h-12 rounded-full text-white flex items-center justify-center shadow-lg transition-all group ${
|
| 81 |
+
hasSelectedTire
|
| 82 |
+
? "bg-brand-orange hover:bg-brand-orange-dark shadow-brand-orange/40 hover:scale-112"
|
| 83 |
+
: "bg-green-500 hover:bg-green-600 shadow-green-500/30 hover:scale-110"
|
| 84 |
+
}`}
|
| 85 |
+
title={hasSelectedTire ? `Inquire about ${selectedTire.brand} ${selectedTire.name} on WhatsApp` : "Chat live on WhatsApp"}
|
| 86 |
+
id="floating-whatsapp-btn"
|
| 87 |
+
>
|
| 88 |
+
{/* Pulsating green or orange ring indication */}
|
| 89 |
+
<span className={`absolute inset-0 rounded-full animate-ping pointer-events-none scale-105 ${
|
| 90 |
+
hasSelectedTire ? "bg-brand-orange/40" : "bg-green-500/40"
|
| 91 |
+
}`}></span>
|
| 92 |
+
<MessageSquare className="w-5 h-5 group-hover:-translate-y-0.5 transition-transform" />
|
| 93 |
+
</a>
|
| 94 |
+
</div>
|
| 95 |
+
);
|
| 96 |
+
}
|
src/components/Hero.tsx
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { motion } from "motion/react";
|
| 2 |
+
import { Phone, ArrowRight, Star, Clock, Sparkles, ShieldCheck, MapPin } from "lucide-react";
|
| 3 |
+
import { BUSINESS_INFO, BRANDS } from "../data";
|
| 4 |
+
|
| 5 |
+
interface HeroProps {
|
| 6 |
+
darkMode: boolean;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
export default function Hero({ darkMode }: HeroProps) {
|
| 10 |
+
const containerVariants = {
|
| 11 |
+
hidden: { opacity: 0 },
|
| 12 |
+
visible: {
|
| 13 |
+
opacity: 1,
|
| 14 |
+
transition: {
|
| 15 |
+
staggerChildren: 0.1,
|
| 16 |
+
delayChildren: 0.1
|
| 17 |
+
}
|
| 18 |
+
}
|
| 19 |
+
};
|
| 20 |
+
|
| 21 |
+
const itemVariants = {
|
| 22 |
+
hidden: { opacity: 0, y: 20 },
|
| 23 |
+
visible: { opacity: 1, y: 0, transition: { duration: 0.6, ease: "easeOut" } }
|
| 24 |
+
};
|
| 25 |
+
|
| 26 |
+
return (
|
| 27 |
+
<section
|
| 28 |
+
id="home"
|
| 29 |
+
className={`relative min-h-screen pt-28 pb-16 flex flex-col justify-center overflow-hidden transition-colors duration-300 ${
|
| 30 |
+
darkMode ? "bg-brand-dark text-white" : "bg-slate-50 text-slate-900"
|
| 31 |
+
}`}
|
| 32 |
+
>
|
| 33 |
+
{/* Dynamic Background Playing Tire Video */}
|
| 34 |
+
<div className="absolute inset-0 z-0 pointer-events-none overflow-hidden">
|
| 35 |
+
<video
|
| 36 |
+
autoPlay
|
| 37 |
+
loop
|
| 38 |
+
muted
|
| 39 |
+
playsInline
|
| 40 |
+
className={`w-full h-full object-cover transition-opacity duration-700 ${
|
| 41 |
+
darkMode ? "opacity-25" : "opacity-10"
|
| 42 |
+
}`}
|
| 43 |
+
src="https://assets.mixkit.co/videos/preview/mixkit-tire-of-a-car-wheels-spinning-on-the-road-41484-large.mp4"
|
| 44 |
+
style={{ objectFit: "cover" }}
|
| 45 |
+
/>
|
| 46 |
+
{/* Horizontal Dark Gradient Mask for Left-Column Text Readability */}
|
| 47 |
+
<div
|
| 48 |
+
className={`absolute inset-0 transition-opacity duration-500 bg-gradient-to-r ${
|
| 49 |
+
darkMode
|
| 50 |
+
? "from-brand-dark via-brand-dark/85 to-transparent"
|
| 51 |
+
: "from-slate-100 via-slate-100/75 to-transparent"
|
| 52 |
+
}`}
|
| 53 |
+
/>
|
| 54 |
+
{/* Vertical Transition Mask to Blend into the next sections flawlessly */}
|
| 55 |
+
<div
|
| 56 |
+
className={`absolute inset-0 transition-opacity duration-500 bg-gradient-to-t ${
|
| 57 |
+
darkMode
|
| 58 |
+
? "from-brand-dark via-transparent to-transparent"
|
| 59 |
+
: "from-slate-50 via-transparent to-transparent"
|
| 60 |
+
}`}
|
| 61 |
+
/>
|
| 62 |
+
</div>
|
| 63 |
+
|
| 64 |
+
{/* Decorative Orbs with glassmersion */}
|
| 65 |
+
<div className="absolute top-1/4 right-0 w-80 h-80 rounded-full bg-gradient-to-br from-brand-orange/30 to-rose-600/10 blur-[100px] pointer-events-none animate-pulse-slow"></div>
|
| 66 |
+
<div className="absolute bottom-1/4 -left-20 w-96 h-96 rounded-full bg-gradient-to-br from-blue-600/10 to-brand-orange/10 blur-[120px] pointer-events-none"></div>
|
| 67 |
+
|
| 68 |
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10 w-full">
|
| 69 |
+
<motion.div
|
| 70 |
+
variants={containerVariants}
|
| 71 |
+
initial="hidden"
|
| 72 |
+
animate="visible"
|
| 73 |
+
className="grid grid-cols-1 lg:grid-cols-12 gap-12 items-center"
|
| 74 |
+
>
|
| 75 |
+
{/* Left Hero Column */}
|
| 76 |
+
<div className="lg:col-span-7 space-y-8">
|
| 77 |
+
<motion.div variants={itemVariants} className="inline-flex items-center space-x-2 bg-brand-orange/10 dark:bg-brand-orange/15 border border-brand-orange/30 px-3.5 py-1.5 rounded-full text-brand-orange">
|
| 78 |
+
<Sparkles className="w-4 h-4" />
|
| 79 |
+
<span className="text-xs font-mono font-semibold tracking-wider uppercase">Faisal Town's #1 Rated Tyre Store</span>
|
| 80 |
+
</motion.div>
|
| 81 |
+
|
| 82 |
+
<motion.div variants={itemVariants} className="space-y-4">
|
| 83 |
+
<h1 className="font-display text-4xl sm:text-5xl md:text-6xl font-extrabold tracking-tight leading-[1.05]">
|
| 84 |
+
Premium Tyres <br />
|
| 85 |
+
<span className="text-transparent bg-clip-text bg-gradient-to-r from-brand-orange via-orange-500 to-red-600">
|
| 86 |
+
Built For Safety
|
| 87 |
+
</span>
|
| 88 |
+
</h1>
|
| 89 |
+
<p className={`text-base sm:text-lg max-w-xl ${
|
| 90 |
+
darkMode ? "text-slate-300" : "text-slate-600"
|
| 91 |
+
}`}>
|
| 92 |
+
Authorized distributor of world-renowned tyre brands in Lahore. From high-speed touring tyres to rugged off-road compounds, enjoy
|
| 93 |
+
<span className="text-brand-orange font-semibold"> 24/7 emergency tyre repairs</span> & alignment services.
|
| 94 |
+
</p>
|
| 95 |
+
</motion.div>
|
| 96 |
+
|
| 97 |
+
{/* Quick Actions Buttons */}
|
| 98 |
+
<motion.div variants={itemVariants} className="flex flex-col sm:flex-row gap-4">
|
| 99 |
+
<a
|
| 100 |
+
href="#all-products"
|
| 101 |
+
className="group flex items-center justify-center space-x-2 bg-gradient-to-br from-brand-orange to-red-600 hover:from-brand-orange-light hover:to-red-500 text-white px-7 py-4 rounded-xl text-base font-semibold shadow-lg shadow-brand-orange/25 hover:shadow-brand-orange/40 hover:-translate-y-0.5 transition-all text-center"
|
| 102 |
+
>
|
| 103 |
+
<span>View Tyre Catalog</span>
|
| 104 |
+
<ArrowRight className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
|
| 105 |
+
</a>
|
| 106 |
+
|
| 107 |
+
<a
|
| 108 |
+
href={`tel:${BUSINESS_INFO.phoneRaw}`}
|
| 109 |
+
className={`flex items-center justify-center space-x-2 border px-7 py-4 rounded-xl text-base font-semibold transition-all hover:-translate-y-0.5 ${
|
| 110 |
+
darkMode
|
| 111 |
+
? "border-white/10 dark:hover:bg-white/5 bg-white/5 active:bg-white/10 text-white"
|
| 112 |
+
: "border-slate-300 hover:bg-slate-100 bg-white shadow-sm text-slate-800"
|
| 113 |
+
}`}
|
| 114 |
+
>
|
| 115 |
+
<Phone className="w-5 h-5 text-brand-orange animate-wiggle" />
|
| 116 |
+
<span>Call Hotline +92 302 4594403</span>
|
| 117 |
+
</a>
|
| 118 |
+
</motion.div>
|
| 119 |
+
|
| 120 |
+
{/* Micro badges */}
|
| 121 |
+
<motion.div variants={itemVariants} className="grid grid-cols-3 gap-4 border-t pt-8 border-black/5 dark:border-white/5">
|
| 122 |
+
<div className="flex flex-col">
|
| 123 |
+
<span className="text-brand-orange font-display text-2xl font-bold">100% Original</span>
|
| 124 |
+
<span className={`text-xs ${darkMode ? "text-slate-400" : "text-slate-500"}`}>Authentic Import Warranty</span>
|
| 125 |
+
</div>
|
| 126 |
+
<div className="flex flex-col">
|
| 127 |
+
<span className="text-brand-orange font-display text-2xl font-bold">24/7 Hours</span>
|
| 128 |
+
<span className={`text-xs ${darkMode ? "text-slate-400" : "text-slate-500"}`}>Night Emergency Road Rescue</span>
|
| 129 |
+
</div>
|
| 130 |
+
<div className="flex flex-col">
|
| 131 |
+
<span className="text-brand-orange font-display text-2xl font-bold">Same Day</span>
|
| 132 |
+
<span className={`text-xs ${darkMode ? "text-slate-400" : "text-slate-500"}`}>Instant Fitting & Tuning</span>
|
| 133 |
+
</div>
|
| 134 |
+
</motion.div>
|
| 135 |
+
</div>
|
| 136 |
+
|
| 137 |
+
{/* Right Bento Grid Column */}
|
| 138 |
+
<div className="lg:col-span-5 grid grid-cols-2 gap-4">
|
| 139 |
+
{/* Bento Card 1: Map Google Ratings */}
|
| 140 |
+
<motion.div
|
| 141 |
+
variants={itemVariants}
|
| 142 |
+
whileHover={{ y: -4, scale: 1.02 }}
|
| 143 |
+
className={`col-span-2 p-5 rounded-3xl ${
|
| 144 |
+
darkMode ? "glassmorphism relative overflow-hidden" : "glassmorphism-light shadow-md"
|
| 145 |
+
}`}
|
| 146 |
+
>
|
| 147 |
+
<div className="flex items-start justify-between">
|
| 148 |
+
<div>
|
| 149 |
+
<span className={`text-xs font-mono tracking-wider uppercase block ${darkMode ? "text-slate-400" : "text-slate-500"}`}>Google Reviews</span>
|
| 150 |
+
<h3 className="text-2xl font-display font-extrabold mt-1 text-brand-orange">4.8 ★ Rating</h3>
|
| 151 |
+
<p className={`text-xs mt-2 ${darkMode ? "text-slate-400" : "text-slate-600"}`}>Overwhelming praise for swift customer support & precise alignment.</p>
|
| 152 |
+
</div>
|
| 153 |
+
<div className="flex space-x-1 bg-brand-orange/10 p-2 rounded-xl text-brand-orange">
|
| 154 |
+
<Star className="w-5 h-5 fill-current" />
|
| 155 |
+
<Star className="w-5 h-5 fill-current" />
|
| 156 |
+
<Star className="w-5 h-5 fill-current" />
|
| 157 |
+
<Star className="w-5 h-5 fill-current" />
|
| 158 |
+
<Star className="w-5 h-5 fill-current" />
|
| 159 |
+
</div>
|
| 160 |
+
</div>
|
| 161 |
+
{/* Profile group miniature */}
|
| 162 |
+
<div className="flex items-center space-x-2.5 mt-4 pt-4 border-t border-black/5 dark:border-white/5">
|
| 163 |
+
<div className="flex items-center -space-x-2 overflow-hidden">
|
| 164 |
+
<div className="w-7 h-7 rounded-full ring-2 ring-white dark:ring-[#1c1c24] bg-gradient-to-br from-emerald-500 to-teal-600 text-[10px] text-white flex items-center justify-center font-bold uppercase shadow-sm">
|
| 165 |
+
GM
|
| 166 |
+
</div>
|
| 167 |
+
<div className="w-7 h-7 rounded-full ring-2 ring-white dark:ring-[#1c1c24] bg-gradient-to-br from-blue-500 to-indigo-600 text-[10px] text-white flex items-center justify-center font-bold uppercase shadow-sm">
|
| 168 |
+
MB
|
| 169 |
+
</div>
|
| 170 |
+
<div className="w-7 h-7 rounded-full ring-2 ring-white dark:ring-[#1c1c24] bg-gradient-to-br from-purple-500 to-fuchsia-600 text-[10px] text-white flex items-center justify-center font-bold uppercase shadow-sm">
|
| 171 |
+
MR
|
| 172 |
+
</div>
|
| 173 |
+
</div>
|
| 174 |
+
<span className={`text-[11px] font-medium tracking-wide ${darkMode ? "text-slate-400" : "text-slate-600"}`}>
|
| 175 |
+
Verified Customer Comments on Maps
|
| 176 |
+
</span>
|
| 177 |
+
</div>
|
| 178 |
+
</motion.div>
|
| 179 |
+
|
| 180 |
+
{/* Bento Card 2: 24/7 Hours */}
|
| 181 |
+
<motion.div
|
| 182 |
+
variants={itemVariants}
|
| 183 |
+
whileHover={{ y: -4 }}
|
| 184 |
+
className={`p-5 rounded-3xl ${
|
| 185 |
+
darkMode ? "glassmorphism" : "glassmorphism-light shadow-md"
|
| 186 |
+
}`}
|
| 187 |
+
>
|
| 188 |
+
<div className="w-10 h-10 rounded-xl bg-orange-500/10 text-brand-orange flex items-center justify-center mb-4">
|
| 189 |
+
<Clock className="w-5 h-5" />
|
| 190 |
+
</div>
|
| 191 |
+
<h4 className="font-display font-bold text-lg">Always Open</h4>
|
| 192 |
+
<p className={`text-xs mt-1 ${darkMode ? "text-slate-400" : "text-slate-500"}`}>
|
| 193 |
+
Emergency assistance, fitting & air services 24 hours open.
|
| 194 |
+
</p>
|
| 195 |
+
</motion.div>
|
| 196 |
+
|
| 197 |
+
{/* Bento Card 3: Secure Products */}
|
| 198 |
+
<motion.div
|
| 199 |
+
variants={itemVariants}
|
| 200 |
+
whileHover={{ y: -4 }}
|
| 201 |
+
className={`p-5 rounded-3xl ${
|
| 202 |
+
darkMode ? "glassmorphism" : "glassmorphism-light shadow-md"
|
| 203 |
+
}`}
|
| 204 |
+
>
|
| 205 |
+
<div className="w-10 h-10 rounded-xl bg-green-500/10 text-green-500 flex items-center justify-center mb-4">
|
| 206 |
+
<ShieldCheck className="w-5 h-5" />
|
| 207 |
+
</div>
|
| 208 |
+
<h4 className="font-display font-bold text-lg">100% Genuine</h4>
|
| 209 |
+
<p className={`text-xs mt-1 ${darkMode ? "text-slate-400" : "text-slate-500"}`}>
|
| 210 |
+
Authorized dealer of global and local major brands.
|
| 211 |
+
</p>
|
| 212 |
+
</motion.div>
|
| 213 |
+
|
| 214 |
+
{/* Bento Card 4: Location Pin */}
|
| 215 |
+
<motion.div
|
| 216 |
+
variants={itemVariants}
|
| 217 |
+
whileHover={{ y: -4 }}
|
| 218 |
+
className={`col-span-2 p-5 rounded-3xl flex items-center space-x-4 ${
|
| 219 |
+
darkMode ? "glassmorphism" : "glassmorphism-light shadow-md"
|
| 220 |
+
}`}
|
| 221 |
+
>
|
| 222 |
+
<div className="w-12 h-12 rounded-xl bg-red-500/10 text-red-500 flex items-center justify-center shrink-0">
|
| 223 |
+
<MapPin className="w-6 h-6 animate-bounce" />
|
| 224 |
+
</div>
|
| 225 |
+
<div>
|
| 226 |
+
<span className={`text-[10px] font-mono uppercase ${darkMode ? "text-slate-400" : "text-slate-500"}`}>Faisal Town, Block C 1, Lahore</span>
|
| 227 |
+
<h4 className="font-display font-bold text-md mt-0.5">Near Center Flats</h4>
|
| 228 |
+
<p className={`text-xs ${darkMode ? "text-slate-400" : "text-slate-500"}`}>F8G5+H9, Lahore, Pakistan</p>
|
| 229 |
+
</div>
|
| 230 |
+
</motion.div>
|
| 231 |
+
</div>
|
| 232 |
+
</motion.div>
|
| 233 |
+
|
| 234 |
+
{/* Brand Stripe Ribbons Area */}
|
| 235 |
+
<div className="mt-20 pt-8 border-t border-black/5 dark:border-white/5">
|
| 236 |
+
<p className={`text-center font-mono text-xs tracking-widest uppercase mb-6 ${
|
| 237 |
+
darkMode ? "text-slate-400" : "text-slate-500"
|
| 238 |
+
}`}>
|
| 239 |
+
Official Retailer / Authorized Distributor Of Premium Brands
|
| 240 |
+
</p>
|
| 241 |
+
|
| 242 |
+
<div className="relative w-full overflow-hidden whitespace-nowrap py-4">
|
| 243 |
+
{/* Smooth mask on edges */}
|
| 244 |
+
<div className={`absolute top-0 left-0 w-20 h-full z-10 pointer-events-none bg-gradient-to-r ${
|
| 245 |
+
darkMode ? "from-brand-dark to-transparent" : "from-slate-50 to-transparent"
|
| 246 |
+
}`}></div>
|
| 247 |
+
<div className={`absolute top-0 right-0 w-20 h-full z-10 pointer-events-none bg-gradient-to-l ${
|
| 248 |
+
darkMode ? "from-brand-dark to-transparent" : "from-slate-50 to-transparent"
|
| 249 |
+
}`}></div>
|
| 250 |
+
|
| 251 |
+
{/* Marquee scroll loop */}
|
| 252 |
+
<div className="inline-flex space-x-12 animate-marquee-scroll">
|
| 253 |
+
{[...BRANDS, ...BRANDS].map((brand, bIdx) => (
|
| 254 |
+
<div
|
| 255 |
+
key={`${brand.name}-${bIdx}`}
|
| 256 |
+
className={`inline-flex items-center space-x-2 bg-white/5 dark:bg-white/5 px-4 py-2.5 rounded-xl border ${
|
| 257 |
+
brand.hasDirectAccent
|
| 258 |
+
? "border-brand-orange/40 text-brand-orange"
|
| 259 |
+
: darkMode
|
| 260 |
+
? "border-white/5 text-slate-300 hover:border-white/25 hover:text-white"
|
| 261 |
+
: "border-slate-200 text-slate-700 hover:border-slate-400 hover:text-slate-900"
|
| 262 |
+
} transition-colors cursor-default`}
|
| 263 |
+
>
|
| 264 |
+
{/* Brand visual emoji / tire mockup symbol */}
|
| 265 |
+
<span className="text-sm font-semibold">⚙️</span>
|
| 266 |
+
<span className="font-display font-bold uppercase tracking-wider text-sm">{brand.name}</span>
|
| 267 |
+
</div>
|
| 268 |
+
))}
|
| 269 |
+
</div>
|
| 270 |
+
</div>
|
| 271 |
+
</div>
|
| 272 |
+
</div>
|
| 273 |
+
</section>
|
| 274 |
+
);
|
| 275 |
+
}
|
src/components/InventoryManager.tsx
ADDED
|
@@ -0,0 +1,1311 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect, useRef } from "react";
|
| 2 |
+
import { TireProduct } from "../data";
|
| 3 |
+
import {
|
| 4 |
+
getTireProducts,
|
| 5 |
+
saveTireProduct,
|
| 6 |
+
deleteTireProduct
|
| 7 |
+
} from "../lib/tireService";
|
| 8 |
+
import {
|
| 9 |
+
loginAdmin,
|
| 10 |
+
logoutAdmin,
|
| 11 |
+
checkAuthState,
|
| 12 |
+
createAdminUser,
|
| 13 |
+
bulkUpdatePrices,
|
| 14 |
+
getLowStockProducts,
|
| 15 |
+
onProductsSnapshot,
|
| 16 |
+
exportToJSON,
|
| 17 |
+
importFromJSON,
|
| 18 |
+
isBrowserOnline,
|
| 19 |
+
registerNetworkStateListener,
|
| 20 |
+
uploadProductImage,
|
| 21 |
+
ProductDocument,
|
| 22 |
+
getVisitorLogs,
|
| 23 |
+
VisitorRecord
|
| 24 |
+
} from "../lib/firebaseService";
|
| 25 |
+
import {
|
| 26 |
+
Plus,
|
| 27 |
+
Trash2,
|
| 28 |
+
Edit,
|
| 29 |
+
Database,
|
| 30 |
+
Wrench,
|
| 31 |
+
RefreshCw,
|
| 32 |
+
Sparkles,
|
| 33 |
+
Save,
|
| 34 |
+
X,
|
| 35 |
+
Search,
|
| 36 |
+
Tag,
|
| 37 |
+
Check,
|
| 38 |
+
User,
|
| 39 |
+
Lock,
|
| 40 |
+
LogOut,
|
| 41 |
+
Download,
|
| 42 |
+
Upload,
|
| 43 |
+
AlertTriangle,
|
| 44 |
+
FileSpreadsheet,
|
| 45 |
+
Wifi,
|
| 46 |
+
WifiOff,
|
| 47 |
+
Image as ImageIcon,
|
| 48 |
+
Eye,
|
| 49 |
+
Users,
|
| 50 |
+
Monitor,
|
| 51 |
+
Globe
|
| 52 |
+
} from "lucide-react";
|
| 53 |
+
import { User as AuthUser } from "firebase/auth";
|
| 54 |
+
|
| 55 |
+
interface InventoryManagerProps {
|
| 56 |
+
darkMode: boolean;
|
| 57 |
+
onRefreshProducts: () => void;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
export default function InventoryManager({ darkMode, onRefreshProducts }: InventoryManagerProps) {
|
| 61 |
+
const [products, setProducts] = useState<TireProduct[]>([]);
|
| 62 |
+
const [loading, setLoading] = useState<boolean>(true);
|
| 63 |
+
const [searchQuery, setSearchQuery] = useState<string>("");
|
| 64 |
+
const [showAddForm, setShowAddForm] = useState<boolean>(false);
|
| 65 |
+
const [editingProduct, setEditingProduct] = useState<any | null>(null);
|
| 66 |
+
|
| 67 |
+
// Connection State
|
| 68 |
+
const [isOnline, setIsOnline] = useState<boolean>(isBrowserOnline());
|
| 69 |
+
|
| 70 |
+
// Authentication State
|
| 71 |
+
const [currentUser, setCurrentUser] = useState<AuthUser | null>(null);
|
| 72 |
+
const [authEmail, setAuthEmail] = useState<string>("");
|
| 73 |
+
const [authPassword, setAuthPassword] = useState<string>("");
|
| 74 |
+
const [isRegisterMode, setIsRegisterMode] = useState<boolean>(false);
|
| 75 |
+
const [authLoading, setAuthLoading] = useState<boolean>(false);
|
| 76 |
+
|
| 77 |
+
// Bulk operation states
|
| 78 |
+
const [bulkPercent, setBulkPercent] = useState<number>(10);
|
| 79 |
+
const [bulkLoading, setBulkLoading] = useState<boolean>(false);
|
| 80 |
+
|
| 81 |
+
// Form State for Adding / Editing
|
| 82 |
+
const [formId, setFormId] = useState<string>("");
|
| 83 |
+
const [formName, setFormName] = useState<string>("");
|
| 84 |
+
const [formBrand, setFormBrand] = useState<string>("");
|
| 85 |
+
const [formSize, setFormSize] = useState<string>("");
|
| 86 |
+
const [formFeature, setFormFeature] = useState<string>("");
|
| 87 |
+
const [formPrice, setFormPrice] = useState<number>(15000);
|
| 88 |
+
const [formStock, setFormStock] = useState<number>(10);
|
| 89 |
+
const [formBadge, setFormBadge] = useState<string>("Hot Selling");
|
| 90 |
+
const [formSegment, setFormSegment] = useState<"hot" | "new" | "famous">("hot");
|
| 91 |
+
const [formImage, setFormImage] = useState<string>("");
|
| 92 |
+
const [formDescription, setFormDescription] = useState<string>("");
|
| 93 |
+
|
| 94 |
+
// Upload state
|
| 95 |
+
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
| 96 |
+
const [uploadLoading, setUploadLoading] = useState<boolean>(false);
|
| 97 |
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
| 98 |
+
|
| 99 |
+
// Low stock products alert list
|
| 100 |
+
const [lowStockProducts, setLowStockProducts] = useState<ProductDocument[]>([]);
|
| 101 |
+
|
| 102 |
+
// Visitor Analytics State
|
| 103 |
+
const [visitorLogs, setVisitorLogs] = useState<VisitorRecord[]>([]);
|
| 104 |
+
const [visitorStatsLoading, setVisitorStatsLoading] = useState<boolean>(false);
|
| 105 |
+
const [activeAdminTab, setActiveAdminTab] = useState<"products" | "visitors">("products");
|
| 106 |
+
|
| 107 |
+
// Notification Toast log
|
| 108 |
+
const [logMessage, setLogMessage] = useState<{ text: string; type: "success" | "error" | "info" } | null>(null);
|
| 109 |
+
|
| 110 |
+
// Synchronize visitor analytics list when admin is authenticated
|
| 111 |
+
useEffect(() => {
|
| 112 |
+
if (currentUser) {
|
| 113 |
+
fetchVisitorAnalytics();
|
| 114 |
+
} else {
|
| 115 |
+
setVisitorLogs([]);
|
| 116 |
+
}
|
| 117 |
+
}, [currentUser]);
|
| 118 |
+
|
| 119 |
+
const fetchVisitorAnalytics = async () => {
|
| 120 |
+
setVisitorStatsLoading(true);
|
| 121 |
+
try {
|
| 122 |
+
const logs = await getVisitorLogs();
|
| 123 |
+
setVisitorLogs(logs);
|
| 124 |
+
} catch (err) {
|
| 125 |
+
console.error("Failed to load visitor logs:", err);
|
| 126 |
+
} finally {
|
| 127 |
+
setVisitorStatsLoading(false);
|
| 128 |
+
}
|
| 129 |
+
};
|
| 130 |
+
|
| 131 |
+
// Trigger custom state snapshots and online listener hooks
|
| 132 |
+
useEffect(() => {
|
| 133 |
+
// 1. Real-time online checker
|
| 134 |
+
const removeNetworkListener = registerNetworkStateListener((online) => {
|
| 135 |
+
setIsOnline(online);
|
| 136 |
+
triggerToast(online ? "System online. Real-time databases synced." : "Workspace offline. Some writes may be queued.", online ? "success" : "error");
|
| 137 |
+
});
|
| 138 |
+
|
| 139 |
+
// 2. Auth listener
|
| 140 |
+
const removeAuthListener = checkAuthState((user) => {
|
| 141 |
+
setCurrentUser(user);
|
| 142 |
+
});
|
| 143 |
+
|
| 144 |
+
// 3. Real-time Snapshot listeners
|
| 145 |
+
const removeSnapshotListener = onProductsSnapshot((snapshotList) => {
|
| 146 |
+
const mapped = snapshotList.map(p => ({
|
| 147 |
+
id: p.id,
|
| 148 |
+
name: p.name,
|
| 149 |
+
brand: p.brand,
|
| 150 |
+
size: p.size,
|
| 151 |
+
feature: p.feature,
|
| 152 |
+
price: p.price,
|
| 153 |
+
badge: p.badge || (p.category === "hot-selling" ? "Hot Selling" : p.category === "new-brands" ? "New Arrival" : "Famous"),
|
| 154 |
+
segment: p.segment || (p.category === "hot-selling" ? "hot" : p.category === "new-brands" ? "new" : "famous"),
|
| 155 |
+
image: p.image || p.imageUrl,
|
| 156 |
+
stock: p.stock || 0,
|
| 157 |
+
description: p.description || ""
|
| 158 |
+
}));
|
| 159 |
+
setProducts(mapped);
|
| 160 |
+
setLoading(false);
|
| 161 |
+
|
| 162 |
+
// Update low stock list
|
| 163 |
+
getLowStockProducts(5).then(lows => setLowStockProducts(lows)).catch(err => console.log(err));
|
| 164 |
+
});
|
| 165 |
+
|
| 166 |
+
return () => {
|
| 167 |
+
removeNetworkListener();
|
| 168 |
+
removeAuthListener();
|
| 169 |
+
removeSnapshotListener();
|
| 170 |
+
};
|
| 171 |
+
}, []);
|
| 172 |
+
|
| 173 |
+
const triggerToast = (text: string, type: "success" | "error" | "info" = "success") => {
|
| 174 |
+
setLogMessage({ text, type });
|
| 175 |
+
setTimeout(() => setLogMessage(null), 4000);
|
| 176 |
+
};
|
| 177 |
+
|
| 178 |
+
// Login handler
|
| 179 |
+
const handleAuthSubmit = async (e: React.FormEvent) => {
|
| 180 |
+
e.preventDefault();
|
| 181 |
+
if (!authEmail || !authPassword) {
|
| 182 |
+
triggerToast("Please supply email and password", "error");
|
| 183 |
+
return;
|
| 184 |
+
}
|
| 185 |
+
setAuthLoading(true);
|
| 186 |
+
try {
|
| 187 |
+
if (isRegisterMode) {
|
| 188 |
+
await createAdminUser(authEmail, authPassword);
|
| 189 |
+
triggerToast("Admin created and authenticated successfully!", "success");
|
| 190 |
+
} else {
|
| 191 |
+
await loginAdmin(authEmail, authPassword);
|
| 192 |
+
triggerToast("Authenticated successfully as Administrator!", "success");
|
| 193 |
+
}
|
| 194 |
+
setAuthEmail("");
|
| 195 |
+
setAuthPassword("");
|
| 196 |
+
} catch (err: any) {
|
| 197 |
+
triggerToast(err.message || "Authentication attempt rejected.", "error");
|
| 198 |
+
} finally {
|
| 199 |
+
setAuthLoading(false);
|
| 200 |
+
}
|
| 201 |
+
};
|
| 202 |
+
|
| 203 |
+
const handleSignOut = async () => {
|
| 204 |
+
try {
|
| 205 |
+
await logoutAdmin();
|
| 206 |
+
triggerToast("Signed out successfully.", "info");
|
| 207 |
+
} catch (err) {
|
| 208 |
+
triggerToast("Signout failed.", "error");
|
| 209 |
+
}
|
| 210 |
+
};
|
| 211 |
+
|
| 212 |
+
// Pre-fill edit form
|
| 213 |
+
const handleEditClick = (prod: any) => {
|
| 214 |
+
setEditingProduct(prod);
|
| 215 |
+
setFormId(prod.id);
|
| 216 |
+
setFormName(prod.name);
|
| 217 |
+
setFormBrand(prod.brand);
|
| 218 |
+
setFormSize(prod.size);
|
| 219 |
+
setFormFeature(prod.feature);
|
| 220 |
+
setFormPrice(prod.price);
|
| 221 |
+
setFormStock(prod.stock || 10);
|
| 222 |
+
setFormBadge(prod.badge);
|
| 223 |
+
setFormSegment(prod.segment);
|
| 224 |
+
setFormImage(prod.image);
|
| 225 |
+
setFormDescription(prod.description);
|
| 226 |
+
setShowAddForm(true); // Open the form drawer
|
| 227 |
+
window.scrollTo({ top: 120, behavior: "smooth" });
|
| 228 |
+
};
|
| 229 |
+
|
| 230 |
+
// Reset/Clear Form Fields
|
| 231 |
+
const clearForm = () => {
|
| 232 |
+
setEditingProduct(null);
|
| 233 |
+
setFormId("");
|
| 234 |
+
setFormName("");
|
| 235 |
+
setFormBrand("");
|
| 236 |
+
setFormSize("");
|
| 237 |
+
setFormFeature("");
|
| 238 |
+
setFormPrice(15000);
|
| 239 |
+
setFormStock(10);
|
| 240 |
+
setFormBadge("New Arrival");
|
| 241 |
+
setFormSegment("new");
|
| 242 |
+
setFormImage("");
|
| 243 |
+
setFormDescription("");
|
| 244 |
+
setSelectedFile(null);
|
| 245 |
+
};
|
| 246 |
+
|
| 247 |
+
// Handle storage file uploads
|
| 248 |
+
const handleFileUploadChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
| 249 |
+
if (e.target.files && e.target.files[0]) {
|
| 250 |
+
setSelectedFile(e.target.files[0]);
|
| 251 |
+
}
|
| 252 |
+
};
|
| 253 |
+
|
| 254 |
+
// Trigger storage upload
|
| 255 |
+
const handleImageUploadToStorage = async () => {
|
| 256 |
+
if (!selectedFile) {
|
| 257 |
+
triggerToast("Please select a file to upload first.", "info");
|
| 258 |
+
return;
|
| 259 |
+
}
|
| 260 |
+
if (!formName) {
|
| 261 |
+
triggerToast("Please define the Tyre Name first to tag your image correctly.", "info");
|
| 262 |
+
return;
|
| 263 |
+
}
|
| 264 |
+
setUploadLoading(true);
|
| 265 |
+
try {
|
| 266 |
+
const responseUrl = await uploadProductImage(selectedFile, formName);
|
| 267 |
+
setFormImage(responseUrl);
|
| 268 |
+
triggerToast("Image asset uploaded to Storage bucket successfully!", "success");
|
| 269 |
+
setSelectedFile(null);
|
| 270 |
+
} catch (err: any) {
|
| 271 |
+
triggerToast(err.message || "Failed to upload image. Writes are restricted.", "error");
|
| 272 |
+
} finally {
|
| 273 |
+
setUploadLoading(false);
|
| 274 |
+
}
|
| 275 |
+
};
|
| 276 |
+
|
| 277 |
+
// Create / Save Handler (triggers Firebase writing)
|
| 278 |
+
const handleSaveProduct = async (e: React.FormEvent) => {
|
| 279 |
+
e.preventDefault();
|
| 280 |
+
if (!formName || !formBrand || !formSize || !formPrice) {
|
| 281 |
+
triggerToast("Please fill in all mandatory inputs.", "error");
|
| 282 |
+
return;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
if (!currentUser) {
|
| 286 |
+
triggerToast("Unauthorized operation. Please log in as Administrator first.", "error");
|
| 287 |
+
return;
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
const imgUrl = formImage.trim() || "https://images.unsplash.com/photo-1542282088-fe8426682b8f?auto=format&fit=crop&q=80&w=600";
|
| 291 |
+
const customId = formId || `custom-${Date.now()}`;
|
| 292 |
+
|
| 293 |
+
const parsedPrice = parseInt(String(formPrice), 10);
|
| 294 |
+
const parsedStock = parseInt(String(formStock), 10);
|
| 295 |
+
if (isNaN(parsedPrice) || parsedPrice <= 0) {
|
| 296 |
+
triggerToast("Price must be a valid positive integer.", "error");
|
| 297 |
+
return;
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
const categoryMapped = formSegment === "hot" ? "hot-selling" : formSegment === "new" ? "new-brands" : "famous";
|
| 301 |
+
|
| 302 |
+
const payload: TireProduct & { stock: number } = {
|
| 303 |
+
id: customId,
|
| 304 |
+
name: formName,
|
| 305 |
+
brand: formBrand,
|
| 306 |
+
size: formSize,
|
| 307 |
+
feature: formFeature,
|
| 308 |
+
price: parsedPrice,
|
| 309 |
+
badge: formBadge,
|
| 310 |
+
segment: formSegment,
|
| 311 |
+
image: imgUrl,
|
| 312 |
+
stock: parsedStock,
|
| 313 |
+
description: formDescription.trim() || `Premium high-durability tire configured specifically for the local roads of Lahore.`
|
| 314 |
+
};
|
| 315 |
+
|
| 316 |
+
try {
|
| 317 |
+
await saveTireProduct(payload);
|
| 318 |
+
triggerToast(
|
| 319 |
+
editingProduct
|
| 320 |
+
? `Successfully saved modifications for '${payload.name}'!`
|
| 321 |
+
: `Successfully registered and seeded '${payload.name}' into the database!`,
|
| 322 |
+
"success"
|
| 323 |
+
);
|
| 324 |
+
clearForm();
|
| 325 |
+
setShowAddForm(false);
|
| 326 |
+
onRefreshProducts(); // Trigger app refetch
|
| 327 |
+
} catch (err: any) {
|
| 328 |
+
console.error(err);
|
| 329 |
+
triggerToast(`Operation rejected. Verify schema rules or Admin privileges.`, "error");
|
| 330 |
+
}
|
| 331 |
+
};
|
| 332 |
+
|
| 333 |
+
// Delete Handler
|
| 334 |
+
const handleDeleteClick = async (id: string, name: string) => {
|
| 335 |
+
if (!currentUser) {
|
| 336 |
+
triggerToast("Authentication required. Please log in to complete deletions.", "error");
|
| 337 |
+
return;
|
| 338 |
+
}
|
| 339 |
+
if (!window.confirm(`Are you absolutely sure you want to delete '${name}'?`)) {
|
| 340 |
+
return;
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
try {
|
| 344 |
+
await deleteTireProduct(id);
|
| 345 |
+
triggerToast(`Deleted '${name}' from Firestore!`, "info");
|
| 346 |
+
onRefreshProducts();
|
| 347 |
+
} catch (err) {
|
| 348 |
+
triggerToast("Restricted write. Confirm your role.", "error");
|
| 349 |
+
}
|
| 350 |
+
};
|
| 351 |
+
|
| 352 |
+
// Bulk rate updater
|
| 353 |
+
const handleBulkUpdate = async () => {
|
| 354 |
+
if (!currentUser) {
|
| 355 |
+
triggerToast("Admin authentication is required for bulk price variations.", "error");
|
| 356 |
+
return;
|
| 357 |
+
}
|
| 358 |
+
if (!window.confirm(`Are you sure you want to alter ALL inventory prices by ${bulkPercent}%?`)) {
|
| 359 |
+
return;
|
| 360 |
+
}
|
| 361 |
+
setBulkLoading(true);
|
| 362 |
+
try {
|
| 363 |
+
await bulkUpdatePrices(bulkPercent);
|
| 364 |
+
triggerToast(`Successfully adjusted all catalog rates in bulk by ${bulkPercent}%!`, "success");
|
| 365 |
+
onRefreshProducts();
|
| 366 |
+
} catch (err: any) {
|
| 367 |
+
triggerToast(err.message || "Bulk operation failed. Check rules permissions.", "error");
|
| 368 |
+
} finally {
|
| 369 |
+
setBulkLoading(false);
|
| 370 |
+
}
|
| 371 |
+
};
|
| 372 |
+
|
| 373 |
+
// JSON backup download
|
| 374 |
+
const handleExportJSON = async () => {
|
| 375 |
+
try {
|
| 376 |
+
await exportToJSON();
|
| 377 |
+
triggerToast("Product catalog downloaded successfully!", "success");
|
| 378 |
+
} catch (e) {
|
| 379 |
+
triggerToast("Export failed.", "error");
|
| 380 |
+
}
|
| 381 |
+
};
|
| 382 |
+
|
| 383 |
+
// JSON backup upload
|
| 384 |
+
const handleImportJSONChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
| 385 |
+
if (!currentUser) {
|
| 386 |
+
triggerToast("Admin authentication is required to run database imports.", "error");
|
| 387 |
+
return;
|
| 388 |
+
}
|
| 389 |
+
if (e.target.files && e.target.files[0]) {
|
| 390 |
+
const file = e.target.files[0];
|
| 391 |
+
if (!window.confirm(`This will import products from '${file.name}'. Duplicate profiles will be ignored. Proceed?`)) {
|
| 392 |
+
return;
|
| 393 |
+
}
|
| 394 |
+
try {
|
| 395 |
+
const outcomes = await importFromJSON(file);
|
| 396 |
+
triggerToast(`Import completed with success! Added: ${outcomes.imported} items, Ignored (Duplicates): ${outcomes.duplicates} items.`, "success");
|
| 397 |
+
onRefreshProducts();
|
| 398 |
+
} catch (err: any) {
|
| 399 |
+
triggerToast(err.message || "Failed to process database import file.", "error");
|
| 400 |
+
}
|
| 401 |
+
}
|
| 402 |
+
};
|
| 403 |
+
|
| 404 |
+
// Filter local inventory display list
|
| 405 |
+
const filteredProducts = products.filter((p) => {
|
| 406 |
+
const term = searchQuery.toLowerCase();
|
| 407 |
+
return (
|
| 408 |
+
p.name.toLowerCase().includes(term) ||
|
| 409 |
+
p.brand.toLowerCase().includes(term) ||
|
| 410 |
+
p.size.toLowerCase().includes(term) ||
|
| 411 |
+
p.segment.toLowerCase().includes(term)
|
| 412 |
+
);
|
| 413 |
+
});
|
| 414 |
+
|
| 415 |
+
return (
|
| 416 |
+
<div className="space-y-8">
|
| 417 |
+
{/* Toast Alert popup banner */}
|
| 418 |
+
{logMessage && (
|
| 419 |
+
<div className={`fixed bottom-6 right-6 z-55 max-w-sm rounded-2xl p-4 shadow-2xl border transition-all duration-300 flex items-center space-x-3 text-sm font-semibold animate-bounce ${
|
| 420 |
+
logMessage.type === "success"
|
| 421 |
+
? "bg-green-600 text-white border-green-500"
|
| 422 |
+
: logMessage.type === "error"
|
| 423 |
+
? "bg-red-600 text-white border-red-500"
|
| 424 |
+
: "bg-blue-600 text-white border-blue-500"
|
| 425 |
+
}`}>
|
| 426 |
+
<span>{logMessage.type === "success" ? "✔" : "ℹ"}</span>
|
| 427 |
+
<p className="flex-1">{logMessage.text}</p>
|
| 428 |
+
</div>
|
| 429 |
+
)}
|
| 430 |
+
|
| 431 |
+
{/* Connection Checker & Admin Identity Banner */}
|
| 432 |
+
<div className={`p-4 rounded-2xl border flex flex-col sm:flex-row items-center justify-between gap-4 font-mono text-xs ${
|
| 433 |
+
darkMode ? "bg-white/5 border-white/10" : "bg-white border-slate-200"
|
| 434 |
+
}`}>
|
| 435 |
+
<div className="flex items-center space-x-2">
|
| 436 |
+
{isOnline ? (
|
| 437 |
+
<span className="flex items-center space-x-1.5 text-emerald-500">
|
| 438 |
+
<Wifi className="w-4 h-4" />
|
| 439 |
+
<span className="font-bold">SYSTEM ONLINE (bahaduralimunnabhai@gmail.com)</span>
|
| 440 |
+
</span>
|
| 441 |
+
) : (
|
| 442 |
+
<span className="flex items-center space-x-1.5 text-rose-500">
|
| 443 |
+
<WifiOff className="w-4 h-4 animate-pulse" />
|
| 444 |
+
<span className="font-bold">SYSTEM OFFLINE (LOCAL CACHE SHIELDS ACTIVE)</span>
|
| 445 |
+
</span>
|
| 446 |
+
)}
|
| 447 |
+
</div>
|
| 448 |
+
|
| 449 |
+
<div className="flex items-center space-x-3">
|
| 450 |
+
{currentUser ? (
|
| 451 |
+
<div className="flex items-center space-x-2">
|
| 452 |
+
<span className="text-emerald-400 font-semibold truncate max-w-xs">
|
| 453 |
+
👤 Admin: {currentUser.email}
|
| 454 |
+
</span>
|
| 455 |
+
<button
|
| 456 |
+
onClick={handleSignOut}
|
| 457 |
+
className="bg-red-500/10 hover:bg-red-500/20 text-red-400 p-1.5 px-3 rounded-lg border border-red-500/20 flex items-center space-x-1 transition-all"
|
| 458 |
+
>
|
| 459 |
+
<LogOut className="w-3.5 h-3.5" />
|
| 460 |
+
<span>Logout</span>
|
| 461 |
+
</button>
|
| 462 |
+
</div>
|
| 463 |
+
) : (
|
| 464 |
+
<span className="text-amber-500 font-semibold flex items-center space-x-1">
|
| 465 |
+
<Lock className="w-3.5 h-3.5" />
|
| 466 |
+
<span>Role: Unauthenticated Client (Sign in below to write to DB)</span>
|
| 467 |
+
</span>
|
| 468 |
+
)}
|
| 469 |
+
</div>
|
| 470 |
+
</div>
|
| 471 |
+
|
| 472 |
+
{/* Admin Credentials Login Form Panel (when not logged in) */}
|
| 473 |
+
{!currentUser && (
|
| 474 |
+
<form
|
| 475 |
+
onSubmit={handleAuthSubmit}
|
| 476 |
+
className={`p-6 rounded-3xl border ${
|
| 477 |
+
darkMode ? "bg-[#1c1c24] border-white/5" : "bg-white border-slate-200 shadow-sm"
|
| 478 |
+
}`}
|
| 479 |
+
id="admin-login-form"
|
| 480 |
+
>
|
| 481 |
+
<div className="flex items-center space-x-2 mb-4">
|
| 482 |
+
<Lock className="w-5 h-5 text-brand-orange" />
|
| 483 |
+
<h3 className="font-display font-bold text-base">
|
| 484 |
+
{isRegisterMode ? "Create New Security Admin Account" : "Registered Administrator Authentication"}
|
| 485 |
+
</h3>
|
| 486 |
+
</div>
|
| 487 |
+
<p className="text-xs text-slate-400 mb-4 leading-relaxed">
|
| 488 |
+
By default, Firestore security rules block unauthorized browser requests. Use this panel to sign in with an administrator email and password, or quickly register a credentials profile below!
|
| 489 |
+
</p>
|
| 490 |
+
|
| 491 |
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
| 492 |
+
<div>
|
| 493 |
+
<label className="text-[10px] uppercase font-mono tracking-wider block mb-1">Email Username</label>
|
| 494 |
+
<input
|
| 495 |
+
type="email"
|
| 496 |
+
required
|
| 497 |
+
placeholder="e.g. admin@haiderbrothers.com"
|
| 498 |
+
value={authEmail}
|
| 499 |
+
onChange={(e) => setAuthEmail(e.target.value)}
|
| 500 |
+
className={`w-full p-2.5 rounded-xl text-xs border focus:outline-none focus:ring-1 focus:ring-brand-orange ${
|
| 501 |
+
darkMode ? "bg-black/35 border-white/10 text-white" : "bg-slate-50 border-slate-200"
|
| 502 |
+
}`}
|
| 503 |
+
id="admin-email-field"
|
| 504 |
+
/>
|
| 505 |
+
</div>
|
| 506 |
+
|
| 507 |
+
<div>
|
| 508 |
+
<label className="text-[10px] uppercase font-mono tracking-wider block mb-1">Secure Password</label>
|
| 509 |
+
<input
|
| 510 |
+
type="password"
|
| 511 |
+
required
|
| 512 |
+
placeholder="••••••••"
|
| 513 |
+
value={authPassword}
|
| 514 |
+
onChange={(e) => setAuthPassword(e.target.value)}
|
| 515 |
+
className={`w-full p-2.5 rounded-xl text-xs border focus:outline-none focus:ring-1 focus:ring-brand-orange ${
|
| 516 |
+
darkMode ? "bg-black/35 border-white/10 text-white" : "bg-slate-50 border-slate-200"
|
| 517 |
+
}`}
|
| 518 |
+
id="admin-password-field"
|
| 519 |
+
/>
|
| 520 |
+
</div>
|
| 521 |
+
</div>
|
| 522 |
+
|
| 523 |
+
<div className="mt-4 flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3">
|
| 524 |
+
<button
|
| 525 |
+
type="button"
|
| 526 |
+
onClick={() => setIsRegisterMode(!isRegisterMode)}
|
| 527 |
+
className="text-xs text-brand-orange hover:underline self-start text-left"
|
| 528 |
+
>
|
| 529 |
+
{isRegisterMode ? "Already have an account? Sign in here" : "Don't have an admin credential? Register one instantly"}
|
| 530 |
+
</button>
|
| 531 |
+
|
| 532 |
+
<button
|
| 533 |
+
type="submit"
|
| 534 |
+
disabled={authLoading}
|
| 535 |
+
className="bg-brand-orange hover:bg-brand-orange-dark text-white font-bold p-2.5 px-6 rounded-xl text-xs flex items-center justify-center space-x-1.5 shadow-md shadow-brand-orange/20"
|
| 536 |
+
id="admin-auth-btn"
|
| 537 |
+
>
|
| 538 |
+
{authLoading ? (
|
| 539 |
+
<RefreshCw className="w-3.5 h-3.5 animate-spin" />
|
| 540 |
+
) : (
|
| 541 |
+
<User className="w-3.5 h-3.5" />
|
| 542 |
+
)}
|
| 543 |
+
<span>{isRegisterMode ? "Create Admin Credentials" : "Authorize Session"}</span>
|
| 544 |
+
</button>
|
| 545 |
+
</div>
|
| 546 |
+
</form>
|
| 547 |
+
)}
|
| 548 |
+
|
| 549 |
+
{/* KPI stats display */}
|
| 550 |
+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
| 551 |
+
<div className={`p-5 rounded-2xl border flex items-center space-x-4 transition-all ${
|
| 552 |
+
darkMode ? "bg-[#1c1c24] border-white/5" : "bg-white border-slate-200/60 shadow-xs"
|
| 553 |
+
}`}>
|
| 554 |
+
<div className="p-3 bg-brand-orange/10 rounded-xl text-brand-orange font-bold text-lg">
|
| 555 |
+
<Database className="w-5 h-5" />
|
| 556 |
+
</div>
|
| 557 |
+
<div>
|
| 558 |
+
<span className={`text-[10px] font-mono block ${darkMode ? "text-slate-400" : "text-slate-500"}`}>
|
| 559 |
+
FIRESTORE PRODUCTS COUNT
|
| 560 |
+
</span>
|
| 561 |
+
<span className="text-xl sm:text-2xl font-display font-black text-brand-orange leading-none">
|
| 562 |
+
{products.length} Items
|
| 563 |
+
</span>
|
| 564 |
+
</div>
|
| 565 |
+
</div>
|
| 566 |
+
|
| 567 |
+
<div className={`p-5 rounded-2xl border flex items-center space-x-4 transition-all ${
|
| 568 |
+
darkMode ? "bg-[#1c1c24] border-white/5" : "bg-white border-slate-200/60 shadow-xs"
|
| 569 |
+
}`}>
|
| 570 |
+
<div className="p-3 bg-green-500/10 rounded-xl text-green-500 font-bold text-lg">
|
| 571 |
+
<Tag className="w-5 h-5" />
|
| 572 |
+
</div>
|
| 573 |
+
<div>
|
| 574 |
+
<span className={`text-[10px] font-mono block ${darkMode ? "text-slate-400" : "text-slate-500"}`}>
|
| 575 |
+
AVERAGE WHOLESALE RATE
|
| 576 |
+
</span>
|
| 577 |
+
<span className="text-xl sm:text-2xl font-display font-black text-green-500 leading-none">
|
| 578 |
+
PKR {Math.round(products.reduce((acc, current) => acc + current.price, 0) / (products.length || 1)).toLocaleString()}
|
| 579 |
+
</span>
|
| 580 |
+
</div>
|
| 581 |
+
</div>
|
| 582 |
+
|
| 583 |
+
<div className={`p-5 rounded-2xl border flex items-center space-x-4 transition-all ${
|
| 584 |
+
darkMode ? "bg-[#1c1c24] border-white/5" : "bg-white border-slate-200/60 shadow-xs"
|
| 585 |
+
}`}>
|
| 586 |
+
<div className="p-3 bg-indigo-500/10 rounded-xl text-indigo-500 font-bold text-lg">
|
| 587 |
+
<AlertTriangle className="w-5 h-5" />
|
| 588 |
+
</div>
|
| 589 |
+
<div>
|
| 590 |
+
<span className={`text-[10px] font-mono block ${darkMode ? "text-slate-400" : "text-slate-500"}`}>
|
| 591 |
+
CRITICAL RESTOCK LOG (STOCK < 5)
|
| 592 |
+
</span>
|
| 593 |
+
<span className="text-xl sm:text-2xl font-display font-black text-indigo-500 leading-none">
|
| 594 |
+
{lowStockProducts.length} Items Alerted
|
| 595 |
+
</span>
|
| 596 |
+
</div>
|
| 597 |
+
</div>
|
| 598 |
+
</div>
|
| 599 |
+
|
| 600 |
+
{/* Bulk editing & backup portability deck (visible when logged in) */}
|
| 601 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
| 602 |
+
{/* Module A: Bulk Adjustments */}
|
| 603 |
+
<div className={`p-5 rounded-3xl border flex flex-col justify-between ${
|
| 604 |
+
darkMode ? "bg-[#1c1c24] border-white/5" : "bg-white border-slate-200 shadow-sm"
|
| 605 |
+
}`}>
|
| 606 |
+
<div>
|
| 607 |
+
<h4 className="font-display font-bold text-sm tracking-tight flex items-center space-x-1">
|
| 608 |
+
<Tag className="w-4 h-4 text-emerald-500" />
|
| 609 |
+
<span>Bulk Margin Adjustment Command</span>
|
| 610 |
+
</h4>
|
| 611 |
+
<p className="text-xs text-slate-400 mt-1 leading-relaxed">
|
| 612 |
+
Instantly increment or decrement ALL prices of tires stored in your Firestore database using a single batch payload.
|
| 613 |
+
</p>
|
| 614 |
+
|
| 615 |
+
<div className="mt-4 space-y-3">
|
| 616 |
+
<label className="text-[10px] font-mono text-slate-400 block">
|
| 617 |
+
Adjust Rate (Percentage % Value): <span className="text-brand-orange font-bold font-sans text-sm">{bulkPercent > 0 ? `+${bulkPercent}` : bulkPercent}%</span>
|
| 618 |
+
</label>
|
| 619 |
+
<div className="flex items-center space-x-3">
|
| 620 |
+
<span className="text-[10px] text-slate-500 font-mono">-50%</span>
|
| 621 |
+
<input
|
| 622 |
+
type="range"
|
| 623 |
+
min="-50"
|
| 624 |
+
max="50"
|
| 625 |
+
step="5"
|
| 626 |
+
value={bulkPercent}
|
| 627 |
+
onChange={(e) => setBulkPercent(parseInt(e.target.value, 10))}
|
| 628 |
+
className="w-full accent-brand-orange bg-slate-200 dark:bg-neutral-800 rounded-lg"
|
| 629 |
+
/>
|
| 630 |
+
<span className="text-[10px] text-slate-500 font-mono">+50%</span>
|
| 631 |
+
</div>
|
| 632 |
+
</div>
|
| 633 |
+
</div>
|
| 634 |
+
|
| 635 |
+
<div className="mt-6">
|
| 636 |
+
<button
|
| 637 |
+
onClick={handleBulkUpdate}
|
| 638 |
+
disabled={bulkLoading || !currentUser}
|
| 639 |
+
className={`w-full p-2.5 rounded-xl text-xs font-bold font-sans transition-all flex items-center justify-center space-x-1.5 ${
|
| 640 |
+
currentUser
|
| 641 |
+
? "bg-[#10b981] text-white hover:bg-emerald-600 shadow-md shadow-emerald-500/10"
|
| 642 |
+
: "bg-slate-300 dark:bg-neutral-800 text-slate-500 cursor-not-allowed"
|
| 643 |
+
}`}
|
| 644 |
+
>
|
| 645 |
+
{bulkLoading ? (
|
| 646 |
+
<RefreshCw className="w-4 h-4 animate-spin" />
|
| 647 |
+
) : (
|
| 648 |
+
<Wrench className="w-4 h-4" />
|
| 649 |
+
)}
|
| 650 |
+
<span>{currentUser ? `Apply ${bulkPercent}% Price Adjustment` : "Login to enable bulk command"}</span>
|
| 651 |
+
</button>
|
| 652 |
+
</div>
|
| 653 |
+
</div>
|
| 654 |
+
|
| 655 |
+
{/* Module B: JSON Data Portability (Export & Import) */}
|
| 656 |
+
<div className={`p-5 rounded-3xl border flex flex-col justify-between ${
|
| 657 |
+
darkMode ? "bg-[#1c1c24] border-white/5" : "bg-white border-slate-200 shadow-sm"
|
| 658 |
+
}`}>
|
| 659 |
+
<div>
|
| 660 |
+
<h4 className="font-display font-bold text-sm tracking-tight flex items-center space-x-1">
|
| 661 |
+
<FileSpreadsheet className="w-4 h-4 text-indigo-500" />
|
| 662 |
+
<span>Firebase Backup Data Portability</span>
|
| 663 |
+
</h4>
|
| 664 |
+
<p className="text-xs text-slate-400 mt-1 leading-relaxed">
|
| 665 |
+
Download your complete inventory collection as a standard JSON file, or restore assets from backups with automatic duplicate detection.
|
| 666 |
+
</p>
|
| 667 |
+
</div>
|
| 668 |
+
|
| 669 |
+
<div className="grid grid-cols-2 gap-3 mt-6">
|
| 670 |
+
{/* Export */}
|
| 671 |
+
<button
|
| 672 |
+
onClick={handleExportJSON}
|
| 673 |
+
className={`p-3 rounded-xl border text-xs font-bold transition-all flex items-center justify-center space-x-1.5 ${
|
| 674 |
+
darkMode
|
| 675 |
+
? "border-indigo-500/20 bg-indigo-500/10 text-indigo-400 hover:bg-indigo-500/20"
|
| 676 |
+
: "border-indigo-200 bg-indigo-50 text-indigo-800 hover:bg-indigo-100"
|
| 677 |
+
}`}
|
| 678 |
+
>
|
| 679 |
+
<Download className="w-4 h-4" />
|
| 680 |
+
<span>Export Backup</span>
|
| 681 |
+
</button>
|
| 682 |
+
|
| 683 |
+
{/* Import file mask */}
|
| 684 |
+
<div className="relative">
|
| 685 |
+
<input
|
| 686 |
+
type="file"
|
| 687 |
+
accept=".json"
|
| 688 |
+
disabled={!currentUser}
|
| 689 |
+
onChange={handleImportJSONChange}
|
| 690 |
+
className="absolute inset-0 opacity-0 cursor-pointer disabled:cursor-not-allowed"
|
| 691 |
+
id="file-import-input"
|
| 692 |
+
/>
|
| 693 |
+
<span className={`w-full p-3 rounded-xl border text-xs font-bold transition-all flex items-center justify-center space-x-1.5 ${
|
| 694 |
+
currentUser
|
| 695 |
+
? darkMode
|
| 696 |
+
? "border-violet-500/20 bg-violet-500/10 text-violet-400 hover:bg-violet-500/20"
|
| 697 |
+
: "border-violet-200 bg-violet-50 text-violet-800 hover:bg-violet-100"
|
| 698 |
+
: "border-slate-300 dark:border-neutral-800 bg-slate-300/10 dark:bg-neutral-800/25 text-slate-500 cursor-not-allowed"
|
| 699 |
+
}`}>
|
| 700 |
+
<Upload className="w-4 h-4" />
|
| 701 |
+
<span>Upload JSON</span>
|
| 702 |
+
</span>
|
| 703 |
+
</div>
|
| 704 |
+
</div>
|
| 705 |
+
</div>
|
| 706 |
+
</div>
|
| 707 |
+
|
| 708 |
+
{/* Button controls */}
|
| 709 |
+
<div className="flex flex-col sm:flex-row gap-3 justify-between items-stretch sm:items-center">
|
| 710 |
+
<div className="flex items-center gap-2">
|
| 711 |
+
{currentUser && (
|
| 712 |
+
<button
|
| 713 |
+
onClick={() => { clearForm(); setShowAddForm(!showAddForm); }}
|
| 714 |
+
className="bg-brand-orange hover:bg-brand-orange-dark text-white font-bold px-4 py-2.5 rounded-xl text-xs flex items-center gap-1.5 transition-all shadow-md shadow-brand-orange/20"
|
| 715 |
+
id="admin-new-tyre-btn"
|
| 716 |
+
>
|
| 717 |
+
{showAddForm ? <X className="w-4 h-4" /> : <Plus className="w-4 h-4" />}
|
| 718 |
+
<span>{showAddForm ? "Close Form Panel" : "Add New Tyre"}</span>
|
| 719 |
+
</button>
|
| 720 |
+
)}
|
| 721 |
+
|
| 722 |
+
<button
|
| 723 |
+
onClick={() => onRefreshProducts()}
|
| 724 |
+
className={`p-2.5 rounded-xl border flex items-center gap-1 text-xs transition-all ${
|
| 725 |
+
darkMode ? "border-white/10 hover:bg-white/5 text-slate-300" : "border-slate-200 hover:bg-slate-50 text-slate-700"
|
| 726 |
+
}`}
|
| 727 |
+
title="Refresh list"
|
| 728 |
+
id="admin-sync-btn"
|
| 729 |
+
>
|
| 730 |
+
<RefreshCw className="w-3.5 h-3.5" />
|
| 731 |
+
<span>Sync DB</span>
|
| 732 |
+
</button>
|
| 733 |
+
</div>
|
| 734 |
+
|
| 735 |
+
{/* Display badge for low stock warning items */}
|
| 736 |
+
{lowStockProducts.length > 0 && (
|
| 737 |
+
<div className="bg-amber-500/10 border border-amber-500/20 text-amber-500 py-2.5 px-3.5 rounded-xl text-xs font-semibold flex items-center space-x-1.5">
|
| 738 |
+
<AlertTriangle className="w-4.5 h-4.5 animate-pulse" />
|
| 739 |
+
<span>Stock alerts: {lowStockProducts.length} items cataloged under 5 units!</span>
|
| 740 |
+
</div>
|
| 741 |
+
)}
|
| 742 |
+
</div>
|
| 743 |
+
|
| 744 |
+
{/* Add / Edit Form Panel */}
|
| 745 |
+
{showAddForm && currentUser && (
|
| 746 |
+
<form
|
| 747 |
+
onSubmit={handleSaveProduct}
|
| 748 |
+
className={`p-6 sm:p-8 rounded-2xl border transition-all ${
|
| 749 |
+
darkMode ? "glassmorphism box-glow" : "glassmorphism-light shadow-xl"
|
| 750 |
+
}`}
|
| 751 |
+
id="tire-product-form"
|
| 752 |
+
>
|
| 753 |
+
<div className="flex items-center justify-between pb-4 border-b border-black/5 dark:border-white/5 mb-6">
|
| 754 |
+
<h4 className="font-display font-bold text-md flex items-center gap-2">
|
| 755 |
+
<Sparkles className="w-5 h-5 text-brand-orange" />
|
| 756 |
+
<span>{editingProduct ? `Edit Tyre Configuration: ${editingProduct.name}` : "Create New Tyre Product"}</span>
|
| 757 |
+
</h4>
|
| 758 |
+
<button
|
| 759 |
+
type="button"
|
| 760 |
+
onClick={clearForm}
|
| 761 |
+
className={`text-xs ${darkMode ? "text-slate-400 hover:text-white" : "text-slate-600 hover:text-black"}`}
|
| 762 |
+
>
|
| 763 |
+
Clear Values
|
| 764 |
+
</button>
|
| 765 |
+
</div>
|
| 766 |
+
|
| 767 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
| 768 |
+
{/* Tyre Name */}
|
| 769 |
+
<div>
|
| 770 |
+
<label className="text-xs font-mono font-semibold block mb-1">Tyre Name (Model) *</label>
|
| 771 |
+
<input
|
| 772 |
+
type="text"
|
| 773 |
+
required
|
| 774 |
+
placeholder="e.g. Pilot Sport 5"
|
| 775 |
+
value={formName}
|
| 776 |
+
onChange={(e) => setFormName(e.target.value)}
|
| 777 |
+
className={`w-full p-2.5 rounded-xl text-xs border focus:ring-1 focus:ring-brand-orange focus:outline-none ${
|
| 778 |
+
darkMode ? "bg-black/35 border-white/10 text-white" : "bg-white border-slate-200 text-slate-800"
|
| 779 |
+
}`}
|
| 780 |
+
/>
|
| 781 |
+
</div>
|
| 782 |
+
|
| 783 |
+
{/* Brand */}
|
| 784 |
+
<div>
|
| 785 |
+
<label className="text-xs font-mono font-semibold block mb-1">Brand (Manufacturer) *</label>
|
| 786 |
+
<input
|
| 787 |
+
type="text"
|
| 788 |
+
required
|
| 789 |
+
placeholder="e.g. Michelin"
|
| 790 |
+
value={formBrand}
|
| 791 |
+
onChange={(e) => setFormBrand(e.target.value)}
|
| 792 |
+
className={`w-full p-2.5 rounded-xl text-xs border focus:ring-1 focus:ring-brand-orange focus:outline-none ${
|
| 793 |
+
darkMode ? "bg-black/35 border-white/10 text-white" : "bg-white border-slate-200 text-slate-800"
|
| 794 |
+
}`}
|
| 795 |
+
/>
|
| 796 |
+
</div>
|
| 797 |
+
|
| 798 |
+
{/* Size */}
|
| 799 |
+
<div>
|
| 800 |
+
<label className="text-xs font-mono font-semibold block mb-1">Tyre Size Code *</label>
|
| 801 |
+
<input
|
| 802 |
+
type="text"
|
| 803 |
+
required
|
| 804 |
+
placeholder="e.g. 205/55R16"
|
| 805 |
+
value={formSize}
|
| 806 |
+
onChange={(e) => setFormSize(e.target.value)}
|
| 807 |
+
className={`w-full p-2.5 rounded-xl text-xs border focus:ring-1 focus:ring-brand-orange focus:outline-none ${
|
| 808 |
+
darkMode ? "bg-black/35 border-white/10 text-white" : "bg-white border-slate-200 text-slate-800"
|
| 809 |
+
}`}
|
| 810 |
+
/>
|
| 811 |
+
</div>
|
| 812 |
+
|
| 813 |
+
{/* Feature Highlight */}
|
| 814 |
+
<div>
|
| 815 |
+
<label className="text-xs font-mono font-semibold block mb-1">Highlight Feature *</label>
|
| 816 |
+
<input
|
| 817 |
+
type="text"
|
| 818 |
+
required
|
| 819 |
+
placeholder="e.g. Comfort ride and wet braking compound"
|
| 820 |
+
value={formFeature}
|
| 821 |
+
onChange={(e) => setFormFeature(e.target.value)}
|
| 822 |
+
className={`w-full p-2.5 rounded-xl text-xs border focus:ring-1 focus:ring-brand-orange focus:outline-none ${
|
| 823 |
+
darkMode ? "bg-black/35 border-white/10 text-white" : "bg-white border-slate-200 text-slate-800"
|
| 824 |
+
}`}
|
| 825 |
+
/>
|
| 826 |
+
</div>
|
| 827 |
+
|
| 828 |
+
{/* Price (PKR) */}
|
| 829 |
+
<div>
|
| 830 |
+
<label className="text-xs font-mono font-semibold block mb-1">Price (PKR Rate) *</label>
|
| 831 |
+
<input
|
| 832 |
+
type="number"
|
| 833 |
+
required
|
| 834 |
+
min={1}
|
| 835 |
+
placeholder="e.g. 15000"
|
| 836 |
+
value={formPrice}
|
| 837 |
+
onChange={(e) => setFormPrice(parseInt(e.target.value, 10) || 15000)}
|
| 838 |
+
className={`w-full p-2.5 rounded-xl text-xs border focus:ring-1 focus:ring-brand-orange focus:outline-none ${
|
| 839 |
+
darkMode ? "bg-black/35 border-white/10 text-white" : "bg-white border-slate-200 text-slate-800"
|
| 840 |
+
}`}
|
| 841 |
+
/>
|
| 842 |
+
</div>
|
| 843 |
+
|
| 844 |
+
{/* Segment Type */}
|
| 845 |
+
<div>
|
| 846 |
+
<label className="text-xs font-mono font-semibold block mb-1">Segment Category *</label>
|
| 847 |
+
<select
|
| 848 |
+
required
|
| 849 |
+
value={formSegment}
|
| 850 |
+
onChange={(e) => {
|
| 851 |
+
setFormSegment(e.target.value as any);
|
| 852 |
+
if (e.target.value === "hot") setFormBadge("Hot Selling");
|
| 853 |
+
else if (e.target.value === "new") setFormBadge("New Arrival");
|
| 854 |
+
else setFormBadge("Famous");
|
| 855 |
+
}}
|
| 856 |
+
className={`w-full p-2.5 rounded-xl text-xs border focus:ring-1 focus:ring-brand-orange focus:outline-none ${
|
| 857 |
+
darkMode ? "bg-black/35 border-white/10 text-white" : "bg-white border-slate-200 text-slate-800"
|
| 858 |
+
}`}
|
| 859 |
+
>
|
| 860 |
+
<option value="hot">Hot Selling</option>
|
| 861 |
+
<option value="new">New Arrival</option>
|
| 862 |
+
<option value="famous">Famous Bestseller</option>
|
| 863 |
+
</select>
|
| 864 |
+
</div>
|
| 865 |
+
|
| 866 |
+
{/* Stock Levels */}
|
| 867 |
+
<div>
|
| 868 |
+
<label className="text-xs font-mono font-semibold block mb-1">Showroom Stock Units *</label>
|
| 869 |
+
<input
|
| 870 |
+
type="number"
|
| 871 |
+
required
|
| 872 |
+
min={0}
|
| 873 |
+
placeholder="e.g. 10"
|
| 874 |
+
value={formStock}
|
| 875 |
+
onChange={(e) => setFormStock(parseInt(e.target.value, 10) || 0)}
|
| 876 |
+
className={`w-full p-2.5 rounded-xl text-xs border focus:ring-1 focus:ring-brand-orange focus:outline-none ${
|
| 877 |
+
darkMode ? "bg-black/35 border-white/10 text-white" : "bg-white border-slate-200 text-slate-800"
|
| 878 |
+
}`}
|
| 879 |
+
/>
|
| 880 |
+
</div>
|
| 881 |
+
|
| 882 |
+
{/* Image path */}
|
| 883 |
+
<div>
|
| 884 |
+
<label className="text-xs font-mono font-semibold block mb-1">Image Address URL</label>
|
| 885 |
+
<input
|
| 886 |
+
type="text"
|
| 887 |
+
placeholder="Leave blank to upload file instead"
|
| 888 |
+
value={formImage}
|
| 889 |
+
onChange={(e) => setFormImage(e.target.value)}
|
| 890 |
+
className={`w-full p-2.5 rounded-xl text-xs border focus:ring-1 focus:ring-brand-orange focus:outline-none ${
|
| 891 |
+
darkMode ? "bg-black/35 border-white/10 text-white" : "bg-white border-slate-200 text-slate-800"
|
| 892 |
+
}`}
|
| 893 |
+
/>
|
| 894 |
+
</div>
|
| 895 |
+
|
| 896 |
+
{/* Firebase Storage Image Upload module */}
|
| 897 |
+
<div>
|
| 898 |
+
<label className="text-xs font-mono font-semibold block mb-1">Upload To Storage Bucket</label>
|
| 899 |
+
<div className="flex items-center space-x-1">
|
| 900 |
+
<input
|
| 901 |
+
type="file"
|
| 902 |
+
accept="image/*"
|
| 903 |
+
onChange={handleFileUploadChange}
|
| 904 |
+
ref={fileInputRef}
|
| 905 |
+
className="hidden"
|
| 906 |
+
/>
|
| 907 |
+
<button
|
| 908 |
+
type="button"
|
| 909 |
+
onClick={() => fileInputRef.current?.click()}
|
| 910 |
+
className={`flex-1 p-2.5 rounded-xl text-[10px] font-bold border flex items-center justify-center space-x-1 ${
|
| 911 |
+
darkMode ? "border-white/10 bg-white/5 text-slate-300" : "border-slate-200 bg-slate-50 text-slate-700"
|
| 912 |
+
}`}
|
| 913 |
+
>
|
| 914 |
+
<ImageIcon className="w-3.5 h-3.5 text-brand-orange" />
|
| 915 |
+
<span className="truncate">{selectedFile ? selectedFile.name : "Select Asset"}</span>
|
| 916 |
+
</button>
|
| 917 |
+
{selectedFile && (
|
| 918 |
+
<button
|
| 919 |
+
type="button"
|
| 920 |
+
onClick={handleImageUploadToStorage}
|
| 921 |
+
disabled={uploadLoading}
|
| 922 |
+
className="p-2.5 bg-brand-orange text-white text-[10px] font-bold rounded-xl flex items-center space-x-0.5"
|
| 923 |
+
>
|
| 924 |
+
{uploadLoading ? <RefreshCw className="w-3 h-3 animate-spin" /> : <Upload className="w-3 h-3" />}
|
| 925 |
+
<span>Upload</span>
|
| 926 |
+
</button>
|
| 927 |
+
)}
|
| 928 |
+
</div>
|
| 929 |
+
</div>
|
| 930 |
+
|
| 931 |
+
{/* Display Badge */}
|
| 932 |
+
<div>
|
| 933 |
+
<label className="text-xs font-mono block mb-1">Display Flag Badge</label>
|
| 934 |
+
<input
|
| 935 |
+
type="text"
|
| 936 |
+
value={formBadge}
|
| 937 |
+
onChange={(e) => setFormBadge(e.target.value)}
|
| 938 |
+
className={`w-full p-2.5 rounded-xl text-xs border focus:ring-1 focus:ring-brand-orange focus:outline-none ${
|
| 939 |
+
darkMode ? "bg-black/35 border-white/10 text-white" : "bg-white border-slate-200 text-slate-800"
|
| 940 |
+
}`}
|
| 941 |
+
/>
|
| 942 |
+
</div>
|
| 943 |
+
|
| 944 |
+
{/* Detailed Description */}
|
| 945 |
+
<div className="md:col-span-2">
|
| 946 |
+
<label className="text-xs font-mono font-semibold block mb-1">Detailed Commercial Description</label>
|
| 947 |
+
<textarea
|
| 948 |
+
rows={2}
|
| 949 |
+
placeholder="Provide details about speed rating, mileage targets, and wet grip attributes..."
|
| 950 |
+
value={formDescription}
|
| 951 |
+
onChange={(e) => setFormDescription(e.target.value)}
|
| 952 |
+
className={`w-full p-2.5 rounded-xl text-xs border focus:ring-1 focus:ring-brand-orange focus:outline-none ${
|
| 953 |
+
darkMode ? "bg-black/35 border-white/10 text-white" : "bg-white border-slate-200 text-slate-800"
|
| 954 |
+
}`}
|
| 955 |
+
/>
|
| 956 |
+
</div>
|
| 957 |
+
</div>
|
| 958 |
+
|
| 959 |
+
<div className="mt-6 flex justify-end space-x-2">
|
| 960 |
+
<button
|
| 961 |
+
type="button"
|
| 962 |
+
onClick={() => { clearForm(); setShowAddForm(false); }}
|
| 963 |
+
className={`px-4 py-2 rounded-xl text-xs font-semibold ${
|
| 964 |
+
darkMode ? "bg-white/5 hover:bg-white/10 text-slate-300" : "bg-slate-100 hover:bg-slate-200 text-slate-700"
|
| 965 |
+
}`}
|
| 966 |
+
>
|
| 967 |
+
Cancel
|
| 968 |
+
</button>
|
| 969 |
+
<button
|
| 970 |
+
type="submit"
|
| 971 |
+
className="bg-brand-orange hover:bg-brand-orange-dark text-white font-bold px-5 py-2 rounded-xl text-xs flex items-center space-x-1 shadow-md shadow-brand-orange/25"
|
| 972 |
+
>
|
| 973 |
+
<Save className="w-3.5 h-3.5" />
|
| 974 |
+
<span>{editingProduct ? "Save Modifications" : "SeedTest/Publish to Firestore"}</span>
|
| 975 |
+
</button>
|
| 976 |
+
</div>
|
| 977 |
+
</form>
|
| 978 |
+
)}
|
| 979 |
+
|
| 980 |
+
{/* Admin Feature Tabs */}
|
| 981 |
+
{currentUser && (
|
| 982 |
+
<div className="flex border-b border-black/5 dark:border-white/10 pb-0.5 space-x-6">
|
| 983 |
+
<button
|
| 984 |
+
type="button"
|
| 985 |
+
onClick={() => setActiveAdminTab("products")}
|
| 986 |
+
className={`pb-3 text-sm font-bold font-display relative transition-all flex items-center space-x-2 ${
|
| 987 |
+
activeAdminTab === "products"
|
| 988 |
+
? "text-brand-orange"
|
| 989 |
+
: "text-slate-400 hover:text-slate-300"
|
| 990 |
+
}`}
|
| 991 |
+
>
|
| 992 |
+
<span>Tire Store Catalog</span>
|
| 993 |
+
{activeAdminTab === "products" && (
|
| 994 |
+
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-brand-orange rounded-full" />
|
| 995 |
+
)}
|
| 996 |
+
</button>
|
| 997 |
+
<button
|
| 998 |
+
type="button"
|
| 999 |
+
onClick={() => setActiveAdminTab("visitors")}
|
| 1000 |
+
className={`pb-3 text-sm font-bold font-display relative transition-all flex items-center space-x-2 ${
|
| 1001 |
+
activeAdminTab === "visitors"
|
| 1002 |
+
? "text-brand-orange"
|
| 1003 |
+
: "text-slate-400 hover:text-slate-300"
|
| 1004 |
+
}`}
|
| 1005 |
+
>
|
| 1006 |
+
<span className="flex items-center space-x-1.5">
|
| 1007 |
+
<span>Visitor Traffic Analytics</span>
|
| 1008 |
+
<span className="text-[10px] bg-brand-orange/15 text-brand-orange px-2 py-0.5 rounded-full font-mono">
|
| 1009 |
+
{visitorLogs.length} Logs
|
| 1010 |
+
</span>
|
| 1011 |
+
</span>
|
| 1012 |
+
{activeAdminTab === "visitors" && (
|
| 1013 |
+
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-brand-orange rounded-full" />
|
| 1014 |
+
)}
|
| 1015 |
+
</button>
|
| 1016 |
+
</div>
|
| 1017 |
+
)}
|
| 1018 |
+
|
| 1019 |
+
{activeAdminTab === "products" ? (
|
| 1020 |
+
/* Main Inventory Listings Search table panel */
|
| 1021 |
+
<div className={`rounded-2xl border overflow-hidden ${
|
| 1022 |
+
darkMode ? "bg-black/20 border-white/10" : "bg-white border-slate-200/80 shadow-xs"
|
| 1023 |
+
}`}>
|
| 1024 |
+
{/* Search header info */}
|
| 1025 |
+
<div className="p-4 border-b border-black/5 dark:border-white/5 flex flex-col md:flex-row gap-3 justify-between items-stretch md:items-center">
|
| 1026 |
+
<h5 className="font-display font-medium text-sm">
|
| 1027 |
+
Live Tire Catalog List ({filteredProducts.length} entries shown)
|
| 1028 |
+
</h5>
|
| 1029 |
+
|
| 1030 |
+
<div className="relative max-w-sm w-full">
|
| 1031 |
+
<input
|
| 1032 |
+
type="text"
|
| 1033 |
+
placeholder="Search by brand, model, size..."
|
| 1034 |
+
value={searchQuery}
|
| 1035 |
+
onChange={(e) => setSearchQuery(e.target.value)}
|
| 1036 |
+
className={`w-full p-2 pl-9 rounded-xl text-xs border focus:ring-1 focus:ring-brand-orange focus:outline-none ${
|
| 1037 |
+
darkMode ? "bg-black/30 border-white/10 text-white" : "bg-slate-50 border-slate-200 text-slate-800"
|
| 1038 |
+
}`}
|
| 1039 |
+
id="inventory-search-input"
|
| 1040 |
+
/>
|
| 1041 |
+
<Search className="w-4 h-4 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
| 1042 |
+
</div>
|
| 1043 |
+
</div>
|
| 1044 |
+
|
| 1045 |
+
{/* Loading Indicator */}
|
| 1046 |
+
{loading ? (
|
| 1047 |
+
<div className="p-12 text-center">
|
| 1048 |
+
<RefreshCw className="w-8 h-8 text-brand-orange animate-spin mx-auto mb-2" />
|
| 1049 |
+
<p className="text-xs text-slate-400">Loading catalog indexing records...</p>
|
| 1050 |
+
</div>
|
| 1051 |
+
) : filteredProducts.length > 0 ? (
|
| 1052 |
+
<div className="overflow-x-auto">
|
| 1053 |
+
<table className="w-full text-left text-xs">
|
| 1054 |
+
<thead className={`font-mono text-[10px] uppercase font-bold text-slate-500 border-b ${
|
| 1055 |
+
darkMode ? "bg-white/5 border-white/5" : "bg-slate-50 border-slate-100"
|
| 1056 |
+
}`}>
|
| 1057 |
+
<tr>
|
| 1058 |
+
<th className="p-3">Tyre (Make/Model)</th>
|
| 1059 |
+
<th className="p-3">Brand</th>
|
| 1060 |
+
<th className="p-3">Size Code</th>
|
| 1061 |
+
<th className="p-3">Wholesale Price</th>
|
| 1062 |
+
<th className="p-3">Stock level</th>
|
| 1063 |
+
<th className="p-3">Segment Highlight</th>
|
| 1064 |
+
{currentUser && <th className="p-3 text-right">Actions</th>}
|
| 1065 |
+
</tr>
|
| 1066 |
+
</thead>
|
| 1067 |
+
<tbody className="divide-y divide-black/5 dark:divide-white/5">
|
| 1068 |
+
{filteredProducts.map((p: any) => (
|
| 1069 |
+
<tr key={p.id} className={`hover:bg-brand-orange/5 transition-colors`}>
|
| 1070 |
+
<td className="p-3 font-semibold text-brand-orange flex items-center gap-2">
|
| 1071 |
+
<div className="w-6 h-6 rounded bg-slate-500/10 overflow-hidden shrink-0">
|
| 1072 |
+
<img src={p.image} className="w-full h-full object-cover" alt="" />
|
| 1073 |
+
</div>
|
| 1074 |
+
<span>{p.name}</span>
|
| 1075 |
+
</td>
|
| 1076 |
+
<td className="p-3 text-slate-400 dark:text-slate-300">{p.brand}</td>
|
| 1077 |
+
<td className="p-3 font-mono font-medium">{p.size}</td>
|
| 1078 |
+
<td className="p-3 font-bold text-green-500">PKR {p.price.toLocaleString()}</td>
|
| 1079 |
+
<td className="p-3 font-mono">
|
| 1080 |
+
<span className={`p-1 px-2.5 rounded-full text-[10px] font-bold truncate ${
|
| 1081 |
+
(p.stock || 0) < 5
|
| 1082 |
+
? "bg-rose-500/15 text-rose-500 border border-rose-500/10 animate-pulse"
|
| 1083 |
+
: "bg-emerald-500/15 text-emerald-500 border border-emerald-500/10"
|
| 1084 |
+
}`}>
|
| 1085 |
+
{p.stock !== undefined ? `${p.stock} Units` : "10 Units"}
|
| 1086 |
+
</span>
|
| 1087 |
+
</td>
|
| 1088 |
+
<td className="p-3 uppercase font-mono text-[10px] tracking-wider text-slate-400">
|
| 1089 |
+
<span className="bg-slate-500/10 px-2 py-0.5 rounded-md">
|
| 1090 |
+
{p.segment}
|
| 1091 |
+
</span>
|
| 1092 |
+
</td>
|
| 1093 |
+
{currentUser && (
|
| 1094 |
+
<td className="p-3 text-right space-x-1">
|
| 1095 |
+
<button
|
| 1096 |
+
type="button"
|
| 1097 |
+
onClick={() => handleEditClick(p)}
|
| 1098 |
+
className={`p-1.5 rounded-lg text-blue-500 hover:bg-blue-500/10`}
|
| 1099 |
+
title="Edit product"
|
| 1100 |
+
>
|
| 1101 |
+
<Edit className="w-4 h-4" />
|
| 1102 |
+
</button>
|
| 1103 |
+
<button
|
| 1104 |
+
type="button"
|
| 1105 |
+
onClick={() => handleDeleteClick(p.id, p.name)}
|
| 1106 |
+
className={`p-1.5 rounded-lg text-red-500 hover:bg-red-500/10`}
|
| 1107 |
+
title="Delete product"
|
| 1108 |
+
>
|
| 1109 |
+
<Trash2 className="w-4 h-4" />
|
| 1110 |
+
</button>
|
| 1111 |
+
</td>
|
| 1112 |
+
)}
|
| 1113 |
+
</tr>
|
| 1114 |
+
))}
|
| 1115 |
+
</tbody>
|
| 1116 |
+
</table>
|
| 1117 |
+
</div>
|
| 1118 |
+
) : (
|
| 1119 |
+
<div className="p-10 text-center text-slate-400">
|
| 1120 |
+
<span className="text-2xl block mb-2">🎈</span>
|
| 1121 |
+
<p className="text-xs">No matching tyre configurations found in database.</p>
|
| 1122 |
+
</div>
|
| 1123 |
+
)}
|
| 1124 |
+
</div>
|
| 1125 |
+
) : (
|
| 1126 |
+
/* Visitor Traffic Analytics Panel */
|
| 1127 |
+
<div className="space-y-6">
|
| 1128 |
+
{/* Analytics KPI grid */}
|
| 1129 |
+
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
|
| 1130 |
+
<div className={`p-4 rounded-2xl border ${darkMode ? "bg-[#1c1c24] border-white/5" : "bg-white border-slate-200/80 shadow-xs"}`}>
|
| 1131 |
+
<div className="flex items-center justify-between mb-2">
|
| 1132 |
+
<span className="text-[10px] font-mono uppercase text-slate-400 font-bold">Total Page Views</span>
|
| 1133 |
+
<Eye className="w-4 h-4 text-brand-orange" />
|
| 1134 |
+
</div>
|
| 1135 |
+
<div className="text-2xl font-black font-display text-brand-orange">{visitorLogs.length} Views</div>
|
| 1136 |
+
<p className="text-[10px] text-slate-400 mt-1">Cumulative page navigation hits logged</p>
|
| 1137 |
+
</div>
|
| 1138 |
+
|
| 1139 |
+
<div className={`p-4 rounded-2xl border ${darkMode ? "bg-[#1c1c24] border-white/5" : "bg-white border-slate-200/80 shadow-xs"}`}>
|
| 1140 |
+
<div className="flex items-center justify-between mb-2">
|
| 1141 |
+
<span className="text-[10px] font-mono uppercase text-slate-400 font-bold">Unique Visitors</span>
|
| 1142 |
+
<Users className="w-4 h-4 text-emerald-500" />
|
| 1143 |
+
</div>
|
| 1144 |
+
<div className="text-2xl font-black font-display text-emerald-500">
|
| 1145 |
+
{new Set(visitorLogs.map(l => l.sessionId)).size} Sessions
|
| 1146 |
+
</div>
|
| 1147 |
+
<p className="text-[10px] text-slate-400 mt-1">Total singular client cookie sessions</p>
|
| 1148 |
+
</div>
|
| 1149 |
+
|
| 1150 |
+
<div className={`p-4 rounded-2xl border ${darkMode ? "bg-[#1c1c24] border-white/5" : "bg-white border-slate-200/80 shadow-xs"}`}>
|
| 1151 |
+
<div className="flex items-center justify-between mb-2">
|
| 1152 |
+
<span className="text-[10px] font-mono uppercase text-slate-400 font-bold">Primary Live Hub</span>
|
| 1153 |
+
<Globe className="w-4 h-4 text-indigo-500" />
|
| 1154 |
+
</div>
|
| 1155 |
+
<div className="text-sm font-black font-display text-indigo-400 truncate uppercase mt-1">
|
| 1156 |
+
{(() => {
|
| 1157 |
+
const counts: Record<string, number> = {};
|
| 1158 |
+
visitorLogs.forEach(l => { counts[l.pagePath] = (counts[l.pagePath] || 0) + 1; });
|
| 1159 |
+
const top = Object.entries(counts).sort((a,b) => b[1]-a[1])[0];
|
| 1160 |
+
return top ? `${top[0]} (${top[1]} views)` : "N/A";
|
| 1161 |
+
})()}
|
| 1162 |
+
</div>
|
| 1163 |
+
<p className="text-[10px] text-slate-400 mt-1">Section driving maximum user count</p>
|
| 1164 |
+
</div>
|
| 1165 |
+
|
| 1166 |
+
<div className={`p-4 rounded-2xl border ${darkMode ? "bg-[#1c1c24] border-white/5" : "bg-white border-slate-200/80 shadow-xs"}`}>
|
| 1167 |
+
<div className="flex items-center justify-between mb-2">
|
| 1168 |
+
<span className="text-[10px] font-mono uppercase text-slate-400 font-bold">Dominant OS Client</span>
|
| 1169 |
+
<Monitor className="w-4 h-4 text-purple-500" />
|
| 1170 |
+
</div>
|
| 1171 |
+
<div className="text-2xl font-black font-display text-purple-400">
|
| 1172 |
+
{(() => {
|
| 1173 |
+
const counts: Record<string, number> = {};
|
| 1174 |
+
visitorLogs.forEach(l => { counts[l.platform] = (counts[l.platform] || 0) + 1; });
|
| 1175 |
+
const top = Object.entries(counts).sort((a,b) => b[1]-a[1])[0];
|
| 1176 |
+
return top ? top[0] : "Web Portal";
|
| 1177 |
+
})()}
|
| 1178 |
+
</div>
|
| 1179 |
+
<p className="text-[10px] text-slate-400 mt-1">Identified operating system signature</p>
|
| 1180 |
+
</div>
|
| 1181 |
+
</div>
|
| 1182 |
+
|
| 1183 |
+
{/* Graphics layout columns */}
|
| 1184 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
| 1185 |
+
{/* Section visits bar breakdown */}
|
| 1186 |
+
<div className={`p-5 rounded-2xl border ${darkMode ? "bg-black/25 border-white/10" : "bg-white border-slate-200/80 shadow-xs"}`}>
|
| 1187 |
+
<h6 className="font-display font-medium text-xs mb-4 uppercase tracking-wider text-slate-400">Section Hits Proportion</h6>
|
| 1188 |
+
<div className="space-y-3.5">
|
| 1189 |
+
{(() => {
|
| 1190 |
+
const counts: Record<string, number> = {};
|
| 1191 |
+
visitorLogs.forEach(l => { counts[l.pagePath] = (counts[l.pagePath] || 0) + 1; });
|
| 1192 |
+
const items = Object.entries(counts).sort((a,b)=>b[1]-a[1]);
|
| 1193 |
+
const total = visitorLogs.length || 1;
|
| 1194 |
+
return items.slice(0, 5).map(([page, count]) => {
|
| 1195 |
+
const pct = Math.round((count / total) * 100);
|
| 1196 |
+
return (
|
| 1197 |
+
<div key={page} className="space-y-1">
|
| 1198 |
+
<div className="flex justify-between text-[11px] font-mono">
|
| 1199 |
+
<span className="text-slate-300 font-semibold uppercase">{page}</span>
|
| 1200 |
+
<span className="text-slate-400">{count} views ({pct}%)</span>
|
| 1201 |
+
</div>
|
| 1202 |
+
<div className="h-1.5 w-full bg-slate-800 rounded-full overflow-hidden">
|
| 1203 |
+
<div className="h-full bg-brand-orange" style={{ width: `${pct}%` }} />
|
| 1204 |
+
</div>
|
| 1205 |
+
</div>
|
| 1206 |
+
);
|
| 1207 |
+
});
|
| 1208 |
+
})()}
|
| 1209 |
+
{visitorLogs.length === 0 && <p className="text-xs text-slate-500 text-center py-4">No page flow patterns registered yet.</p>}
|
| 1210 |
+
</div>
|
| 1211 |
+
</div>
|
| 1212 |
+
|
| 1213 |
+
{/* Operating systems bar breakdown */}
|
| 1214 |
+
<div className={`p-5 rounded-2xl border ${darkMode ? "bg-black/25 border-white/10" : "bg-white border-slate-200/80 shadow-xs"}`}>
|
| 1215 |
+
<h6 className="font-display font-medium text-xs mb-4 uppercase tracking-wider text-slate-400">System Platforms Split</h6>
|
| 1216 |
+
<div className="space-y-3.5">
|
| 1217 |
+
{(() => {
|
| 1218 |
+
const counts: Record<string, number> = {};
|
| 1219 |
+
visitorLogs.forEach(l => { counts[l.platform] = (counts[l.platform] || 0) + 1; });
|
| 1220 |
+
const items = Object.entries(counts).sort((a,b)=>b[1]-a[1]);
|
| 1221 |
+
const total = visitorLogs.length || 1;
|
| 1222 |
+
return items.slice(0, 5).map(([plat, count]) => {
|
| 1223 |
+
const pct = Math.round((count / total) * 100);
|
| 1224 |
+
return (
|
| 1225 |
+
<div key={plat} className="space-y-1">
|
| 1226 |
+
<div className="flex justify-between text-[11px] font-mono">
|
| 1227 |
+
<span className="text-slate-300 font-semibold">{plat}</span>
|
| 1228 |
+
<span className="text-slate-400">{count} hits ({pct}%)</span>
|
| 1229 |
+
</div>
|
| 1230 |
+
<div className="h-1.5 w-full bg-slate-800 rounded-full overflow-hidden">
|
| 1231 |
+
<div className="h-full bg-emerald-500" style={{ width: `${pct}%` }} />
|
| 1232 |
+
</div>
|
| 1233 |
+
</div>
|
| 1234 |
+
);
|
| 1235 |
+
});
|
| 1236 |
+
})()}
|
| 1237 |
+
{visitorLogs.length === 0 && <p className="text-xs text-slate-500 text-center py-4">Device platform stream is currently empty.</p>}
|
| 1238 |
+
</div>
|
| 1239 |
+
</div>
|
| 1240 |
+
</div>
|
| 1241 |
+
|
| 1242 |
+
{/* Sequential Live Visitory Logs stream table */}
|
| 1243 |
+
<div className={`rounded-2xl border overflow-hidden ${darkMode ? "bg-black/20 border-white/10" : "bg-white border-slate-200/80 shadow-xs"}`}>
|
| 1244 |
+
<div className="p-4 border-b border-black/5 dark:border-white/5 flex justify-between items-center bg-black/10">
|
| 1245 |
+
<h6 className="font-display font-medium text-xs uppercase tracking-wider text-slate-400">Visitation Sequence Logs</h6>
|
| 1246 |
+
<button
|
| 1247 |
+
type="button"
|
| 1248 |
+
onClick={fetchVisitorAnalytics}
|
| 1249 |
+
className="text-xs font-semibold text-brand-orange hover:underline flex items-center space-x-1"
|
| 1250 |
+
>
|
| 1251 |
+
<RefreshCw className="w-3.5 h-3.5" />
|
| 1252 |
+
<span>Refresh Live Traffic</span>
|
| 1253 |
+
</button>
|
| 1254 |
+
</div>
|
| 1255 |
+
|
| 1256 |
+
{visitorStatsLoading ? (
|
| 1257 |
+
<div className="p-12 text-center">
|
| 1258 |
+
<RefreshCw className="w-8 h-8 text-brand-orange animate-spin mx-auto mb-2" />
|
| 1259 |
+
<p className="text-xs text-slate-400">Querying analytical traffic logs from cloud...</p>
|
| 1260 |
+
</div>
|
| 1261 |
+
) : visitorLogs.length > 0 ? (
|
| 1262 |
+
<div className="overflow-x-auto max-h-[380px] overflow-y-auto">
|
| 1263 |
+
<table className="w-full text-left text-xs">
|
| 1264 |
+
<thead className={`font-mono text-[10px] uppercase font-bold text-slate-500 border-b ${
|
| 1265 |
+
darkMode ? "bg-neutral-900 border-white/5" : "bg-slate-50 border-slate-100"
|
| 1266 |
+
}`}>
|
| 1267 |
+
<tr>
|
| 1268 |
+
<th className="p-3 pl-4">Timestamp (Asia/Karachi)</th>
|
| 1269 |
+
<th className="p-3">Session Hash</th>
|
| 1270 |
+
<th className="p-3">Device OS</th>
|
| 1271 |
+
<th className="p-3">Client Language</th>
|
| 1272 |
+
<th className="p-3">Section Section ID</th>
|
| 1273 |
+
<th className="p-3 pr-4 truncate max-w-xs">User Agent string</th>
|
| 1274 |
+
</tr>
|
| 1275 |
+
</thead>
|
| 1276 |
+
<tbody className="divide-y divide-black/5 dark:divide-white/5 font-mono text-[11px]">
|
| 1277 |
+
{visitorLogs.map((log) => {
|
| 1278 |
+
const timeStr = log.timestamp
|
| 1279 |
+
? new Date(log.timestamp.seconds * 1000).toLocaleString("en-PK", { timeZone: "Asia/Karachi" })
|
| 1280 |
+
: "Syncing timestamp...";
|
| 1281 |
+
return (
|
| 1282 |
+
<tr key={log.id} className="hover:bg-brand-orange/5 transition-colors text-slate-400 dark:text-slate-300">
|
| 1283 |
+
<td className="p-3 pl-4 font-bold text-slate-500">{timeStr}</td>
|
| 1284 |
+
<td className="p-3 font-semibold text-emerald-400">{log.sessionId.substring(4, 14)}...</td>
|
| 1285 |
+
<td className="p-3 font-sans">
|
| 1286 |
+
<span className="p-1 px-2 text-[10px] font-semibold bg-indigo-500/10 text-indigo-400 border border-indigo-500/5 rounded-md">
|
| 1287 |
+
{log.platform}
|
| 1288 |
+
</span>
|
| 1289 |
+
</td>
|
| 1290 |
+
<td className="p-3">{log.language}</td>
|
| 1291 |
+
<td className="p-3 font-bold text-brand-orange uppercase">{log.pagePath}</td>
|
| 1292 |
+
<td className="p-3 pr-4 truncate max-w-[200px] text-[10px] text-slate-500" title={log.userAgent}>
|
| 1293 |
+
{log.userAgent}
|
| 1294 |
+
</td>
|
| 1295 |
+
</tr>
|
| 1296 |
+
);
|
| 1297 |
+
})}
|
| 1298 |
+
</tbody>
|
| 1299 |
+
</table>
|
| 1300 |
+
</div>
|
| 1301 |
+
) : (
|
| 1302 |
+
<div className="p-12 text-center text-slate-400">
|
| 1303 |
+
<p className="text-xs">No user navigation sessions registered in the Firebase storage logs yet.</p>
|
| 1304 |
+
</div>
|
| 1305 |
+
)}
|
| 1306 |
+
</div>
|
| 1307 |
+
</div>
|
| 1308 |
+
)}
|
| 1309 |
+
</div>
|
| 1310 |
+
);
|
| 1311 |
+
}
|
src/components/LocationContact.tsx
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect } from "react";
|
| 2 |
+
import { Phone, MapPin, Clock, MessageSquare, Compass, ClipboardCheck, Send, Sparkles } from "lucide-react";
|
| 3 |
+
import { BUSINESS_INFO } from "../data";
|
| 4 |
+
|
| 5 |
+
interface LocationContactProps {
|
| 6 |
+
darkMode: boolean;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
export default function LocationContact({ darkMode }: LocationContactProps) {
|
| 10 |
+
const [lahoreTime, setLahoreTime] = useState("");
|
| 11 |
+
const [inquiryText, setInquiryText] = useState("");
|
| 12 |
+
const [inquirySuccess, setInquirySuccess] = useState(false);
|
| 13 |
+
|
| 14 |
+
useEffect(() => {
|
| 15 |
+
const updateTime = () => {
|
| 16 |
+
try {
|
| 17 |
+
const timeString = new Date().toLocaleTimeString("en-US", {
|
| 18 |
+
timeZone: "Asia/Karachi",
|
| 19 |
+
hour: "2-digit",
|
| 20 |
+
minute: "2-digit",
|
| 21 |
+
second: "2-digit",
|
| 22 |
+
hour12: true
|
| 23 |
+
});
|
| 24 |
+
setLahoreTime(timeString);
|
| 25 |
+
} catch (e) {
|
| 26 |
+
// Fallback if platform has minor timezone translation block
|
| 27 |
+
const now = new Date();
|
| 28 |
+
const localUTC = now.getTime() + now.getTimezoneOffset() * 60000;
|
| 29 |
+
const pktTime = new Date(localUTC + 3600000 * 5); // GMT+5
|
| 30 |
+
setLahoreTime(pktTime.toLocaleTimeString());
|
| 31 |
+
}
|
| 32 |
+
};
|
| 33 |
+
|
| 34 |
+
updateTime();
|
| 35 |
+
const timer = setInterval(updateTime, 1000);
|
| 36 |
+
return () => clearInterval(timer);
|
| 37 |
+
}, []);
|
| 38 |
+
|
| 39 |
+
const handleInquirySubmit = (e: React.FormEvent) => {
|
| 40 |
+
e.preventDefault();
|
| 41 |
+
if (!inquiryText.trim()) return;
|
| 42 |
+
|
| 43 |
+
// Direct redirection to WhatsApp web chat prefilled with customer inquiry
|
| 44 |
+
const encoded = encodeURIComponent(`Hi Haider Brothers Traders, I am interested in: ${inquiryText}`);
|
| 45 |
+
window.open(`${BUSINESS_INFO.whatsappUrl}?text=${encoded}`, "_blank");
|
| 46 |
+
setInquirySuccess(true);
|
| 47 |
+
setInquiryText("");
|
| 48 |
+
};
|
| 49 |
+
|
| 50 |
+
return (
|
| 51 |
+
<section
|
| 52 |
+
id="contact"
|
| 53 |
+
className={`py-24 relative overflow-hidden transition-colors duration-300 ${
|
| 54 |
+
darkMode ? "bg-brand-dark text-white" : "bg-slate-50 text-slate-900"
|
| 55 |
+
}`}
|
| 56 |
+
>
|
| 57 |
+
{/* Visual lighting accents */}
|
| 58 |
+
<div className="absolute right-0 top-1/2 w-80 h-80 rounded-full bg-red-600/10 blur-[130px] pointer-events-none"></div>
|
| 59 |
+
|
| 60 |
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
| 61 |
+
<div className="text-center max-w-2xl mx-auto mb-16">
|
| 62 |
+
<div className="inline-flex items-center space-x-2 bg-brand-orange/10 px-3.5 py-1.5 rounded-full text-brand-orange text-xs font-mono tracking-wider uppercase mb-3">
|
| 63 |
+
<MapPin className="w-3.5 h-3.5 animate-bounce" />
|
| 64 |
+
<span>Faisal Town Lahore Office</span>
|
| 65 |
+
</div>
|
| 66 |
+
<h2 className="text-3xl sm:text-4xl font-display font-extrabold tracking-tight">
|
| 67 |
+
Stop By Or Get Instant Support
|
| 68 |
+
</h2>
|
| 69 |
+
<p className={`text-sm mt-3 ${darkMode ? "text-slate-400" : "text-slate-600"}`}>
|
| 70 |
+
Located at the trade center of Faisal Town block C1, Lahore. Ready to assist you 24/7 with professional tyre fitting, alignment tuning, or roadside emergency fixes.
|
| 71 |
+
</p>
|
| 72 |
+
</div>
|
| 73 |
+
|
| 74 |
+
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-stretch">
|
| 75 |
+
|
| 76 |
+
{/* Left Column: Coordinates & Real Contact bento Cards (lg:col-span-5) */}
|
| 77 |
+
<div className="lg:col-span-5 flex flex-col justify-between gap-4">
|
| 78 |
+
|
| 79 |
+
{/* Live Indicator Card */}
|
| 80 |
+
<div className={`p-6 rounded-3xl border transition-all duration-300 ${
|
| 81 |
+
darkMode ? "glassmorphism" : "glassmorphism-light shadow-md"
|
| 82 |
+
}`}>
|
| 83 |
+
<div className="flex items-center justify-between">
|
| 84 |
+
<span className="text-[10px] font-mono tracking-widest uppercase text-slate-500">Live Showroom Status</span>
|
| 85 |
+
<div className="flex items-center space-x-1.5 bg-green-500/10 px-2.5 py-1 rounded-full border border-green-500/20 text-green-500">
|
| 86 |
+
<span className="w-2 h-2 rounded-full bg-green-500 animate-ping"></span>
|
| 87 |
+
<span className="text-[10px] font-bold font-mono">24/7 OPEN NOW</span>
|
| 88 |
+
</div>
|
| 89 |
+
</div>
|
| 90 |
+
|
| 91 |
+
<div className="mt-6 flex items-baseline space-x-2">
|
| 92 |
+
<span className="text-2xl sm:text-3xl font-mono font-extrabold tracking-tight text-brand-orange">
|
| 93 |
+
{lahoreTime || "Loading..."}
|
| 94 |
+
</span>
|
| 95 |
+
<span className={`text-xs ${darkMode ? "text-slate-400" : "text-slate-500"}`}>Lahore PKT Time</span>
|
| 96 |
+
</div>
|
| 97 |
+
<p className={`text-xs mt-3 leading-relaxed ${darkMode ? "text-slate-400" : "text-slate-500"}`}>
|
| 98 |
+
Midnight puncture repair? Highway wheel balance issue? Our crews never sleep. Call first or drive in at any hour.
|
| 99 |
+
</p>
|
| 100 |
+
</div>
|
| 101 |
+
|
| 102 |
+
{/* Quick Connection Options Card */}
|
| 103 |
+
<div className={`p-6 rounded-3xl border ${
|
| 104 |
+
darkMode ? "bg-white/5 border-white/5" : "bg-slate-50 border-slate-100 shadow-sm"
|
| 105 |
+
} space-y-4`}>
|
| 106 |
+
{/* Telephone */}
|
| 107 |
+
<a
|
| 108 |
+
href={`tel:${BUSINESS_INFO.phoneRaw}`}
|
| 109 |
+
className={`p-4 rounded-2xl border flex items-center space-x-4 transition-all hover:scale-[1.01] ${
|
| 110 |
+
darkMode
|
| 111 |
+
? "bg-black/20 border-white/5 hover:border-brand-orange/40 hover:bg-black/40"
|
| 112 |
+
: "bg-white border-slate-200/60 hover:border-brand-orange/40"
|
| 113 |
+
}`}
|
| 114 |
+
id="contact-phone-link"
|
| 115 |
+
>
|
| 116 |
+
<div className="w-10 h-10 rounded-xl bg-orange-600/10 text-brand-orange flex items-center justify-center shrink-0">
|
| 117 |
+
<Phone className="w-5 h-5" />
|
| 118 |
+
</div>
|
| 119 |
+
<div>
|
| 120 |
+
<span className={`text-[9px] font-mono uppercase block ${darkMode ? "text-slate-500" : "text-slate-400"}`}>Direct Hotline Support</span>
|
| 121 |
+
<span className="font-display font-bold text-sm sm:text-base text-brand-orange">{BUSINESS_INFO.phone}</span>
|
| 122 |
+
</div>
|
| 123 |
+
</a>
|
| 124 |
+
|
| 125 |
+
{/* WhatsApp */}
|
| 126 |
+
<a
|
| 127 |
+
href={`${BUSINESS_INFO.whatsappUrl}?text=Hello%20Haider%20Brothers,%20I'd%20like%20to%20ask%20about%20your%20tyre%20inventory`}
|
| 128 |
+
target="_blank"
|
| 129 |
+
rel="noreferrer referrer"
|
| 130 |
+
className={`p-4 rounded-2xl border flex items-center space-x-4 transition-all hover:scale-[1.01] ${
|
| 131 |
+
darkMode
|
| 132 |
+
? "bg-black/20 border-white/5 hover:border-green-500/45 hover:bg-black/40"
|
| 133 |
+
: "bg-white border-slate-200/60 hover:border-green-500/40"
|
| 134 |
+
}`}
|
| 135 |
+
id="contact-whatsapp-link"
|
| 136 |
+
>
|
| 137 |
+
<div className="w-10 h-10 rounded-xl bg-green-500/10 text-green-500 flex items-center justify-center shrink-0">
|
| 138 |
+
<MessageSquare className="w-5 h-5" />
|
| 139 |
+
</div>
|
| 140 |
+
<div>
|
| 141 |
+
<span className={`text-[9px] font-mono uppercase block ${darkMode ? "text-slate-500" : "text-slate-400"}`}>WhatsApp Instant Quote</span>
|
| 142 |
+
<span className="font-display font-semibold text-sm sm:text-base text-green-500">Secure Live Text Chat</span>
|
| 143 |
+
</div>
|
| 144 |
+
</a>
|
| 145 |
+
|
| 146 |
+
{/* Address */}
|
| 147 |
+
<div className={`p-4 rounded-2xl border flex items-start space-x-4 ${
|
| 148 |
+
darkMode ? "bg-black/20 border-white/5" : "bg-white border-slate-200/60"
|
| 149 |
+
}`}>
|
| 150 |
+
<div className="w-10 h-10 rounded-xl bg-red-500/10 text-red-500 flex items-center justify-center shrink-0 mt-0.5">
|
| 151 |
+
<MapPin className="w-5 h-5" />
|
| 152 |
+
</div>
|
| 153 |
+
<div>
|
| 154 |
+
<span className={`text-[9px] font-mono uppercase block ${darkMode ? "text-slate-400" : "text-slate-400"}`}>Postal Address</span>
|
| 155 |
+
<p className="text-xs font-semibold leading-relaxed mt-1">
|
| 156 |
+
{BUSINESS_INFO.address}
|
| 157 |
+
</p>
|
| 158 |
+
</div>
|
| 159 |
+
</div>
|
| 160 |
+
</div>
|
| 161 |
+
|
| 162 |
+
{/* Live Message Form */}
|
| 163 |
+
<div className={`p-6 rounded-3xl border ${
|
| 164 |
+
darkMode ? "glassmorphism" : "glassmorphism-light shadow-md"
|
| 165 |
+
}`}>
|
| 166 |
+
<h3 className="font-display font-bold text-sm mb-2">Search Stock or Request Quote</h3>
|
| 167 |
+
|
| 168 |
+
<form onSubmit={handleInquirySubmit} className="space-y-3">
|
| 169 |
+
<input
|
| 170 |
+
type="text"
|
| 171 |
+
required
|
| 172 |
+
placeholder="e.g., need 4 tyre price for Pirelli 205/60R16..."
|
| 173 |
+
value={inquiryText}
|
| 174 |
+
onChange={(e) => setInquiryText(e.target.value)}
|
| 175 |
+
className={`w-full p-3 rounded-xl text-xs border focus:ring-2 focus:ring-brand-orange focus:outline-none transition-all ${
|
| 176 |
+
darkMode
|
| 177 |
+
? "bg-black/40 border-white/10 text-white placeholder-slate-500"
|
| 178 |
+
: "bg-white border-slate-200 text-slate-900 placeholder-slate-400"
|
| 179 |
+
}`}
|
| 180 |
+
id="contact-stock-search-input"
|
| 181 |
+
/>
|
| 182 |
+
|
| 183 |
+
<button
|
| 184 |
+
type="submit"
|
| 185 |
+
className="w-full bg-gradient-to-r from-brand-orange to-red-600 text-white py-2.5 rounded-xl text-xs font-bold transition-all hover:-translate-y-0.5 flex items-center justify-center space-x-1"
|
| 186 |
+
id="contact-submit-btn"
|
| 187 |
+
>
|
| 188 |
+
<Send className="w-3.5 h-3.5" />
|
| 189 |
+
<span>Send WhatsApp Message</span>
|
| 190 |
+
</button>
|
| 191 |
+
</form>
|
| 192 |
+
</div>
|
| 193 |
+
</div>
|
| 194 |
+
|
| 195 |
+
{/* Right Column: Google Maps Location Interactive Container (lg:col-span-7) */}
|
| 196 |
+
<div className={`lg:col-span-7 flex flex-col justify-between rounded-3xl overflow-hidden border shadow-xl min-h-[400px] transition-all duration-300 hover:scale-[1.005] ${
|
| 197 |
+
darkMode
|
| 198 |
+
? "bg-[#111115] border-white/5 hover:border-brand-orange/30 hover:shadow-[0_0_25px_rgba(255,85,0,0.2)]"
|
| 199 |
+
: "bg-white border-slate-200/60 hover:border-brand-orange/30 hover:shadow-2xl"
|
| 200 |
+
}`}>
|
| 201 |
+
{/* Maps Iframe */}
|
| 202 |
+
<div className="relative w-full h-full min-h-[350px] flex-1">
|
| 203 |
+
<iframe
|
| 204 |
+
title="Haider Brothers Traders Google Maps Location"
|
| 205 |
+
src={BUSINESS_INFO.mapIframeUrl}
|
| 206 |
+
width="100%"
|
| 207 |
+
height="100%"
|
| 208 |
+
style={{ border: 0 }}
|
| 209 |
+
allowFullScreen={true}
|
| 210 |
+
loading="lazy"
|
| 211 |
+
referrerPolicy="no-referrer-when-downgrade"
|
| 212 |
+
className="absolute inset-0 w-full h-full grayscale-0 dark:brightness-[0.85] dark:contrast-[1.15] transition-all duration-300"
|
| 213 |
+
/>
|
| 214 |
+
</div>
|
| 215 |
+
|
| 216 |
+
{/* Map footer directions link */}
|
| 217 |
+
<div className={`p-4 sm:p-5 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 border-t ${
|
| 218 |
+
darkMode ? "bg-[#1c1c24] border-white/5" : "bg-white border-slate-100"
|
| 219 |
+
}`}>
|
| 220 |
+
<div>
|
| 221 |
+
<h4 className="font-display font-bold text-sm">Faisal Town C-1 Roadway</h4>
|
| 222 |
+
<p className={`text-xs ${darkMode ? "text-slate-400" : "text-slate-500"}`}>
|
| 223 |
+
Located right on Block C 1, Faisal Town, Lahore, Pakistan.
|
| 224 |
+
</p>
|
| 225 |
+
</div>
|
| 226 |
+
|
| 227 |
+
<a
|
| 228 |
+
href="https://www.google.com/maps/place/Haider+Brothers+Traders/@31.4764496,74.3084687,17z/data=!3m1!4b1!4m6!3m5!1s0x3919030037af462f:0xc479c225327c71d6!8m2!3d31.4764496!4d74.3084687!16s%2Fg%2F11zjmlbgth?entry=ttu&g_ep=EgoyMDI2MDYxMC4wIKXMDSoASAFQAw%3D%3D"
|
| 229 |
+
target="_blank"
|
| 230 |
+
rel="noreferrer referrer"
|
| 231 |
+
className="flex items-center justify-center space-x-2 bg-brand-orange hover:bg-brand-orange-dark text-white px-5 py-3 rounded-xl text-xs font-bold shadow-md shadow-brand-orange/20 transition-all text-center hover:scale-105 active:scale-95"
|
| 232 |
+
id="maps-directions-btn"
|
| 233 |
+
>
|
| 234 |
+
<Compass className="w-4 h-4 shrink-0" />
|
| 235 |
+
<span>Navigate on Google Maps</span>
|
| 236 |
+
</a>
|
| 237 |
+
</div>
|
| 238 |
+
</div>
|
| 239 |
+
</div>
|
| 240 |
+
</div>
|
| 241 |
+
</section>
|
| 242 |
+
);
|
| 243 |
+
}
|
src/components/Logo.tsx
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from "react";
|
| 2 |
+
|
| 3 |
+
interface LogoProps {
|
| 4 |
+
className?: string;
|
| 5 |
+
variant?: "full" | "icon" | "compact";
|
| 6 |
+
darkMode?: boolean;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
export default function Logo({ className = "h-12", variant = "full", darkMode = true }: LogoProps) {
|
| 10 |
+
// Brand Colors matching the logo:
|
| 11 |
+
// Red: Racing red/orange-red (#EB1C24 or brand-orange)
|
| 12 |
+
// Dark text: Slate-900 / white depending on darkMode
|
| 13 |
+
const primaryRed = "#E11D48"; // vibrant racing red
|
| 14 |
+
const textDarkColor = darkMode ? "#FFFFFF" : "#0F172A";
|
| 15 |
+
const grayColor = darkMode ? "#94A3B8" : "#64748B";
|
| 16 |
+
|
| 17 |
+
if (variant === "icon") {
|
| 18 |
+
// A quick, highly-identifiable compact circular/square icon for favicon / tiny spots
|
| 19 |
+
return (
|
| 20 |
+
<svg
|
| 21 |
+
viewBox="0 0 100 100"
|
| 22 |
+
fill="none"
|
| 23 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 24 |
+
className={className}
|
| 25 |
+
>
|
| 26 |
+
{/* Ambient Glow */}
|
| 27 |
+
<circle cx="50" cy="50" r="40" fill="url(#iconGlow)" opacity="0.15" />
|
| 28 |
+
|
| 29 |
+
{/* Tire track arc on top of icon */}
|
| 30 |
+
<path
|
| 31 |
+
d="M20,38 C30,22 70,22 80,38"
|
| 32 |
+
stroke={textDarkColor}
|
| 33 |
+
strokeWidth="4"
|
| 34 |
+
strokeDasharray="4 4"
|
| 35 |
+
strokeLinecap="round"
|
| 36 |
+
opacity="0.8"
|
| 37 |
+
/>
|
| 38 |
+
|
| 39 |
+
{/* Slanted bold letter "H" and "T" */}
|
| 40 |
+
<g transform="skewX(-10) translate(8, 0)">
|
| 41 |
+
<text
|
| 42 |
+
x="20"
|
| 43 |
+
y="65"
|
| 44 |
+
fill={primaryRed}
|
| 45 |
+
fontSize="32"
|
| 46 |
+
fontWeight="900"
|
| 47 |
+
fontFamily="system-ui, -apple-system, sans-serif"
|
| 48 |
+
letterSpacing="-1"
|
| 49 |
+
>
|
| 50 |
+
H
|
| 51 |
+
</text>
|
| 52 |
+
<text
|
| 53 |
+
x="48"
|
| 54 |
+
y="65"
|
| 55 |
+
fill={textDarkColor}
|
| 56 |
+
fontSize="32"
|
| 57 |
+
fontWeight="900"
|
| 58 |
+
fontFamily="system-ui, -apple-system, sans-serif"
|
| 59 |
+
letterSpacing="-1"
|
| 60 |
+
>
|
| 61 |
+
B
|
| 62 |
+
</text>
|
| 63 |
+
</g>
|
| 64 |
+
|
| 65 |
+
{/* Tire wrapping right side */}
|
| 66 |
+
<circle cx="76" cy="55" r="14" stroke={textDarkColor} strokeWidth="4.5" />
|
| 67 |
+
<circle cx="76" cy="55" r="8" stroke={textDarkColor} strokeWidth="2.5" />
|
| 68 |
+
<path d="M76,41 L76,69 M62,55 L90,55" stroke={textDarkColor} strokeWidth="1.5" />
|
| 69 |
+
|
| 70 |
+
<defs>
|
| 71 |
+
<radialGradient id="iconGlow" cx="50%" cy="50%" r="50%">
|
| 72 |
+
<stop offset="0%" stopColor={primaryRed} />
|
| 73 |
+
<stop offset="100%" stopColor="transparent" />
|
| 74 |
+
</radialGradient>
|
| 75 |
+
</defs>
|
| 76 |
+
</svg>
|
| 77 |
+
);
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
// Full HBT Logo matching the shop's physical sign board and uploaded image
|
| 81 |
+
return (
|
| 82 |
+
<div className={`flex items-center select-none ${className}`}>
|
| 83 |
+
<svg
|
| 84 |
+
viewBox="0 0 350 115"
|
| 85 |
+
fill="none"
|
| 86 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 87 |
+
className="w-full h-full"
|
| 88 |
+
>
|
| 89 |
+
{/* BACKGROUND GLOW (Subtle, professional) */}
|
| 90 |
+
<defs>
|
| 91 |
+
<filter id="glow" x="-10%" y="-10%" width="120%" height="120%">
|
| 92 |
+
<feGaussianBlur stdDeviation="3" result="blur" />
|
| 93 |
+
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
| 94 |
+
</filter>
|
| 95 |
+
</defs>
|
| 96 |
+
|
| 97 |
+
{/* 1. TIRE TREAD TRACK ARC (Top header curving down over the letters into the wheel) */}
|
| 98 |
+
<g id="Tire-Tread-Arc" transform="translate(45, 10)">
|
| 99 |
+
{/* Subtle guide curve */}
|
| 100 |
+
<path d="M 5,20 Q 100,-8 212,28" stroke={grayColor} strokeWidth="1" fill="none" opacity="0.15" />
|
| 101 |
+
|
| 102 |
+
{/* Authentic Herringbone/Leaning tread tracks from the uploaded sign */}
|
| 103 |
+
<g fill={textDarkColor} opacity="0.85">
|
| 104 |
+
{/* Left side rising treads */}
|
| 105 |
+
<path d="M 5,22 L 11,14 L 16,16 L 10,24 Z" />
|
| 106 |
+
<path d="M 23,17 L 29,9 L 34,11 L 28,19 Z" />
|
| 107 |
+
<path d="M 42,13 L 48,5 L 53,7 L 47,15 Z" />
|
| 108 |
+
<path d="M 62,10 L 68,2 L 73,4 L 67,12 Z" />
|
| 109 |
+
<path d="M 83,8 L 89,1 L 94,3 L 88,10 Z" />
|
| 110 |
+
|
| 111 |
+
{/* Center-right crowning treads bending towards the tire */}
|
| 112 |
+
<path d="M 105,7 L 111,1 L 116,3 L 110,9 Z" />
|
| 113 |
+
<path d="M 127,8 L 133,3 L 137,6 L 131,11 Z" />
|
| 114 |
+
<path d="M 149,11 L 154,6 L 158,9 L 153,14 Z" />
|
| 115 |
+
<path d="M 170,15 L 175,10 L 179,13 L 174,18 Z" />
|
| 116 |
+
<path d="M 190,21 L 194,16 L 198,19 L 194,24 Z" />
|
| 117 |
+
<path d="M 209,28 L 213,23 L 217,26 L 213,31 Z" />
|
| 118 |
+
</g>
|
| 119 |
+
</g>
|
| 120 |
+
|
| 121 |
+
{/* 2. MAIN LOGO BODY: STYLIZED SLANTED "HBT" */}
|
| 122 |
+
<g id="HBT-Acronym" transform="translate(56, 32)">
|
| 123 |
+
{/* High-fidelity, sharp racing red path for 'H' */}
|
| 124 |
+
<path
|
| 125 |
+
d="M 5,48 L 16,6 H 31 L 27,22 H 49 L 53,6 H 68 L 57,48 H 42 L 46,31 H 24 L 20,48 Z"
|
| 126 |
+
fill={primaryRed}
|
| 127 |
+
/>
|
| 128 |
+
|
| 129 |
+
{/* High-fidelity block 'B' in Racing Red */}
|
| 130 |
+
<path
|
| 131 |
+
d="M 72,48 L 84,6 H 112 C 122,6 128,10 126,17 C 125,21 121,24 116,26 C 122,27 124,31 122,37 C 120,44 113,48 102,48 Z
|
| 132 |
+
M 94,21 H 105 C 109,21 111,19 112,16 C 113,13 111,11 107,11 H 97 Z
|
| 133 |
+
M 90,38 H 101 C 105,38 107,36 108,32 C 109,28 107,26 103,26 H 93 Z"
|
| 134 |
+
fill={primaryRed}
|
| 135 |
+
/>
|
| 136 |
+
|
| 137 |
+
{/* High-fidelity slanted 'T' adjacent and integrated to the wheel */}
|
| 138 |
+
<path
|
| 139 |
+
d="M 124,13 L 126,6 H 168 L 166,13 H 152 L 143,48 H 128 L 137,13 Z"
|
| 140 |
+
fill={primaryRed}
|
| 141 |
+
/>
|
| 142 |
+
|
| 143 |
+
{/* 3. SIMPLIFIED & AUTHENTIC TIRE & RIM SYSTEM (Clean, high-contrast circular wheel matching the real shop brand) */}
|
| 144 |
+
<g id="Wheel-Icon" transform="translate(178, 14)">
|
| 145 |
+
{/* Elegant, clean concentric circular ring in the rim disc */}
|
| 146 |
+
<circle cx="25" cy="25" r="9" stroke={grayColor} strokeWidth="1.5" fill="none" opacity="0.6" />
|
| 147 |
+
</g>
|
| 148 |
+
</g>
|
| 149 |
+
|
| 150 |
+
{/* 4. "HAIDER BROTHERS TRADERS" sub-text at the base */}
|
| 151 |
+
<text
|
| 152 |
+
x="175"
|
| 153 |
+
y="104"
|
| 154 |
+
fill={textDarkColor}
|
| 155 |
+
fontSize="15.5"
|
| 156 |
+
fontWeight="800"
|
| 157 |
+
fontFamily="system-ui, -apple-system, sans-serif"
|
| 158 |
+
letterSpacing="5.8"
|
| 159 |
+
textAnchor="middle"
|
| 160 |
+
id="hbt-signature-text"
|
| 161 |
+
>
|
| 162 |
+
HAIDER BROTHERS TRADERS
|
| 163 |
+
</text>
|
| 164 |
+
|
| 165 |
+
{/* Small trademark label */}
|
| 166 |
+
<text
|
| 167 |
+
x="332"
|
| 168 |
+
y="102"
|
| 169 |
+
fill={grayColor}
|
| 170 |
+
fontSize="7"
|
| 171 |
+
fontWeight="700"
|
| 172 |
+
fontFamily="monospace"
|
| 173 |
+
>
|
| 174 |
+
®
|
| 175 |
+
</text>
|
| 176 |
+
</svg>
|
| 177 |
+
</div>
|
| 178 |
+
);
|
| 179 |
+
}
|
src/components/Navbar.tsx
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useEffect } from "react";
|
| 2 |
+
import { Phone, MessageSquare, Menu, X, Sun, Moon, Sparkles, FolderLock } from "lucide-react";
|
| 3 |
+
import { BUSINESS_INFO } from "../data";
|
| 4 |
+
import Logo from "./Logo";
|
| 5 |
+
|
| 6 |
+
interface NavbarProps {
|
| 7 |
+
darkMode: boolean;
|
| 8 |
+
setDarkMode: (val: boolean) => void;
|
| 9 |
+
currentPage: string;
|
| 10 |
+
setCurrentPage: (page: string) => void;
|
| 11 |
+
isAuthorizedAdmin: boolean;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
export default function Navbar({ darkMode, setDarkMode, currentPage, setCurrentPage, isAuthorizedAdmin }: NavbarProps) {
|
| 15 |
+
const [isOpen, setIsOpen] = useState(false);
|
| 16 |
+
const [scrolled, setScrolled] = useState(false);
|
| 17 |
+
|
| 18 |
+
useEffect(() => {
|
| 19 |
+
const handleScroll = () => {
|
| 20 |
+
setScrolled(window.scrollY > 20);
|
| 21 |
+
};
|
| 22 |
+
window.addEventListener("scroll", handleScroll);
|
| 23 |
+
return () => window.removeEventListener("scroll", handleScroll);
|
| 24 |
+
}, []);
|
| 25 |
+
|
| 26 |
+
const baseNavItems = [
|
| 27 |
+
{ label: "Home", page: "home" },
|
| 28 |
+
{ label: "Catalogue", page: "catalogue" },
|
| 29 |
+
{ label: "Reviews Feed", page: "reviews" },
|
| 30 |
+
{ label: "Showroom", page: "showroom" },
|
| 31 |
+
{ label: "Inventory Manager", page: "admin", isIconic: true }
|
| 32 |
+
];
|
| 33 |
+
|
| 34 |
+
const navItems = isAuthorizedAdmin
|
| 35 |
+
? baseNavItems
|
| 36 |
+
: baseNavItems.filter(item => item.page !== "admin");
|
| 37 |
+
|
| 38 |
+
return (
|
| 39 |
+
<nav
|
| 40 |
+
className={`sticky top-0 left-0 w-full z-50 transition-all duration-300 ${
|
| 41 |
+
scrolled || currentPage !== "home"
|
| 42 |
+
? darkMode
|
| 43 |
+
? "bg-[#121214]/95 backdrop-blur-md border-b border-white/10 py-3 shadow-lg shadow-black/20"
|
| 44 |
+
: "bg-white/95 backdrop-blur-md border-b border-black/5 py-3 shadow-sm"
|
| 45 |
+
: "bg-transparent py-5"
|
| 46 |
+
}`}
|
| 47 |
+
>
|
| 48 |
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
| 49 |
+
<div className="flex items-center justify-between">
|
| 50 |
+
{/* Logo Brand */}
|
| 51 |
+
<button
|
| 52 |
+
onClick={() => { setCurrentPage("home"); window.scrollTo({ top: 0, behavior: "smooth" }); }}
|
| 53 |
+
className="flex items-center group transition-all transform hover:scale-[1.02] active:scale-95 text-left"
|
| 54 |
+
id="navbar-logo-btn"
|
| 55 |
+
>
|
| 56 |
+
<Logo variant="full" darkMode={darkMode} className="h-10 sm:h-12 w-auto" />
|
| 57 |
+
</button>
|
| 58 |
+
|
| 59 |
+
{/* Desktop Navigation Links (Responsive breakpoint updated to lg to prevent overlapping on tablets) */}
|
| 60 |
+
<div className="hidden lg:flex items-center space-x-1 xl:space-x-2">
|
| 61 |
+
{navItems.map((item) => {
|
| 62 |
+
const isActive = currentPage === item.page;
|
| 63 |
+
return (
|
| 64 |
+
<button
|
| 65 |
+
key={item.page}
|
| 66 |
+
onClick={() => { setCurrentPage(item.page); window.scrollTo({ top: 0, behavior: "smooth" }); }}
|
| 67 |
+
className={`px-3 py-2 rounded-lg text-xs xl:text-sm font-medium transition-all relative flex items-center space-x-1 ${
|
| 68 |
+
isActive
|
| 69 |
+
? "text-brand-orange font-bold bg-brand-orange/5"
|
| 70 |
+
: darkMode
|
| 71 |
+
? "text-slate-300 hover:text-white hover:bg-white/5"
|
| 72 |
+
: "text-slate-600 hover:text-slate-900 hover:bg-black/5"
|
| 73 |
+
}`}
|
| 74 |
+
>
|
| 75 |
+
{item.isIconic && <FolderLock className="w-3.5 h-3.5 mr-1 text-brand-orange inline" />}
|
| 76 |
+
<span>{item.label}</span>
|
| 77 |
+
{isActive && (
|
| 78 |
+
<span className="absolute bottom-0 left-2 right-2 h-[2px] bg-brand-orange rounded-full" />
|
| 79 |
+
)}
|
| 80 |
+
</button>
|
| 81 |
+
);
|
| 82 |
+
})}
|
| 83 |
+
</div>
|
| 84 |
+
|
| 85 |
+
{/* Right Action panel for Desktop */}
|
| 86 |
+
<div className="hidden lg:flex items-center space-x-3">
|
| 87 |
+
{/* Theme Toggle Button */}
|
| 88 |
+
<button
|
| 89 |
+
onClick={() => setDarkMode(!darkMode)}
|
| 90 |
+
className={`p-2 rounded-xl transition-all ${
|
| 91 |
+
darkMode
|
| 92 |
+
? "bg-white/5 hover:bg-white/10 text-amber-400"
|
| 93 |
+
: "bg-black/5 hover:bg-black/10 text-slate-800"
|
| 94 |
+
}`}
|
| 95 |
+
title={darkMode ? "Switch to Light Mode" : "Switch to Dark Mode"}
|
| 96 |
+
id="theme-toggle-desktop"
|
| 97 |
+
>
|
| 98 |
+
{darkMode ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
|
| 99 |
+
</button>
|
| 100 |
+
|
| 101 |
+
{/* Emergency Hotline Button */}
|
| 102 |
+
<a
|
| 103 |
+
href={`tel:${BUSINESS_INFO.phoneRaw}`}
|
| 104 |
+
className="flex items-center space-x-1.5 bg-gradient-to-r from-brand-orange to-red-600 text-white px-3.5 py-2 rounded-xl text-xs font-semibold shadow-md shadow-brand-orange/20 hover:shadow-brand-orange/40 hover:-translate-y-0.5 transition-all"
|
| 105 |
+
>
|
| 106 |
+
<Phone className="w-3.5 h-3.5" />
|
| 107 |
+
<span>24/7 Helpline</span>
|
| 108 |
+
</a>
|
| 109 |
+
</div>
|
| 110 |
+
|
| 111 |
+
{/* Mobile Right Controls (Menu & Theme) */}
|
| 112 |
+
<div className="flex lg:hidden items-center space-x-2">
|
| 113 |
+
<button
|
| 114 |
+
onClick={() => setDarkMode(!darkMode)}
|
| 115 |
+
className={`p-1.5 rounded-lg transition-all ${
|
| 116 |
+
darkMode ? "bg-white/5 text-amber-400" : "bg-black/5 text-slate-800"
|
| 117 |
+
}`}
|
| 118 |
+
id="theme-toggle-mobile"
|
| 119 |
+
>
|
| 120 |
+
{darkMode ? <Sun className="w-4.5 h-4.5" /> : <Moon className="w-4.5 h-4.5" />}
|
| 121 |
+
</button>
|
| 122 |
+
|
| 123 |
+
<button
|
| 124 |
+
onClick={() => setIsOpen(!isOpen)}
|
| 125 |
+
className={`p-1.5 rounded-lg transition-all ${
|
| 126 |
+
darkMode ? "bg-white/5 text-white" : "bg-black/5 text-slate-800"
|
| 127 |
+
}`}
|
| 128 |
+
aria-label="Toggle navigation menu"
|
| 129 |
+
id="mobile-menu-btn"
|
| 130 |
+
>
|
| 131 |
+
{isOpen ? <X className="w-5.5 h-5.5" /> : <Menu className="w-5.5 h-5.5" />}
|
| 132 |
+
</button>
|
| 133 |
+
</div>
|
| 134 |
+
</div>
|
| 135 |
+
</div>
|
| 136 |
+
|
| 137 |
+
{/* Mobile Drawer Overlay */}
|
| 138 |
+
{isOpen && (
|
| 139 |
+
<div className="lg:hidden fixed inset-0 top-[60px] z-45 bg-black/60 backdrop-blur-sm">
|
| 140 |
+
<div
|
| 141 |
+
className={`w-4/5 max-w-sm h-[calc(100vh-60px)] p-6 shadow-2xl flex flex-col justify-between transition-transform duration-300 ${
|
| 142 |
+
darkMode ? "bg-[#1c1c24] text-white" : "bg-white text-slate-900"
|
| 143 |
+
}`}
|
| 144 |
+
>
|
| 145 |
+
<div className="space-y-4">
|
| 146 |
+
<span className={`text-[10px] font-mono tracking-widest block pb-2 border-b uppercase ${
|
| 147 |
+
darkMode ? "text-slate-400 border-white/5" : "text-slate-500 border-black/5"
|
| 148 |
+
}`}>
|
| 149 |
+
Browse Showroom Sections
|
| 150 |
+
</span>
|
| 151 |
+
|
| 152 |
+
<div className="flex flex-col space-y-1">
|
| 153 |
+
{navItems.map((item) => {
|
| 154 |
+
const isActive = currentPage === item.page;
|
| 155 |
+
return (
|
| 156 |
+
<button
|
| 157 |
+
key={item.page}
|
| 158 |
+
onClick={() => {
|
| 159 |
+
setCurrentPage(item.page);
|
| 160 |
+
setIsOpen(false);
|
| 161 |
+
window.scrollTo({ top: 0, behavior: "smooth" });
|
| 162 |
+
}}
|
| 163 |
+
className={`px-4 py-3 rounded-xl text-sm font-medium transition-all flex items-center justify-between text-left w-full ${
|
| 164 |
+
isActive
|
| 165 |
+
? "bg-brand-orange/10 text-brand-orange font-bold"
|
| 166 |
+
: darkMode
|
| 167 |
+
? "hover:bg-white/5 text-slate-300"
|
| 168 |
+
: "hover:bg-slate-100 text-slate-700"
|
| 169 |
+
}`}
|
| 170 |
+
>
|
| 171 |
+
<span className="flex items-center">
|
| 172 |
+
{item.isIconic && <FolderLock className="w-4 h-4 mr-2 text-brand-orange" />}
|
| 173 |
+
{item.label}
|
| 174 |
+
</span>
|
| 175 |
+
{isActive && <span className="w-2 h-2 rounded-full bg-brand-orange" />}
|
| 176 |
+
</button>
|
| 177 |
+
);
|
| 178 |
+
})}
|
| 179 |
+
</div>
|
| 180 |
+
</div>
|
| 181 |
+
|
| 182 |
+
<div className="space-y-4 pt-6 border-t border-black/5 dark:border-white/5">
|
| 183 |
+
<div className="flex items-center space-x-2 text-xs font-mono text-slate-500 dark:text-slate-400">
|
| 184 |
+
<Sparkles className="w-4 h-4 text-brand-orange animate-pulse" />
|
| 185 |
+
<span>OPEN 24 HOURS IN FAISAL TOWN</span>
|
| 186 |
+
</div>
|
| 187 |
+
|
| 188 |
+
<a
|
| 189 |
+
href={`tel:${BUSINESS_INFO.phoneRaw}`}
|
| 190 |
+
className="flex items-center justify-center space-x-2 w-full bg-gradient-to-r from-brand-orange to-red-600 text-white py-2.5 rounded-xl text-xs font-semibold shadow-md shadow-brand-orange/30 text-center"
|
| 191 |
+
>
|
| 192 |
+
<Phone className="w-4 h-4" />
|
| 193 |
+
<span>Call Emergency Service</span>
|
| 194 |
+
</a>
|
| 195 |
+
|
| 196 |
+
<a
|
| 197 |
+
href={`${BUSINESS_INFO.whatsappUrl}?text=Hello%20Haider%20Brothers,%20I%20have%20an%20emergency%20tyre%20request`}
|
| 198 |
+
target="_blank"
|
| 199 |
+
rel="noreferrer referrer"
|
| 200 |
+
className="flex items-center justify-center space-x-2 w-full border border-green-500/35 text-green-500 bg-green-500/5 py-2.5 rounded-xl text-xs font-medium"
|
| 201 |
+
>
|
| 202 |
+
<MessageSquare className="w-4 h-4" />
|
| 203 |
+
<span>WhatsApp Live Chat</span>
|
| 204 |
+
</a>
|
| 205 |
+
</div>
|
| 206 |
+
</div>
|
| 207 |
+
</div>
|
| 208 |
+
)}
|
| 209 |
+
</nav>
|
| 210 |
+
);
|
| 211 |
+
}
|
src/components/NewsletterSignup.tsx
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from "react";
|
| 2 |
+
import { Mail, Send, Check, Loader2, Sparkles } from "lucide-react";
|
| 3 |
+
import { subscribeNewsletter } from "../lib/firebaseService";
|
| 4 |
+
|
| 5 |
+
interface NewsletterSignupProps {
|
| 6 |
+
darkMode: boolean;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
export default function NewsletterSignup({ darkMode }: NewsletterSignupProps) {
|
| 10 |
+
const [email, setEmail] = useState("");
|
| 11 |
+
const [loading, setLoading] = useState(false);
|
| 12 |
+
const [status, setStatus] = useState<"idle" | "success" | "error">("idle");
|
| 13 |
+
const [errorMessage, setErrorMessage] = useState("");
|
| 14 |
+
|
| 15 |
+
const handleSubscribe = async (e: React.FormEvent) => {
|
| 16 |
+
e.preventDefault();
|
| 17 |
+
if (!email.trim()) return;
|
| 18 |
+
|
| 19 |
+
setLoading(true);
|
| 20 |
+
setStatus("idle");
|
| 21 |
+
setErrorMessage("");
|
| 22 |
+
|
| 23 |
+
try {
|
| 24 |
+
await subscribeNewsletter(email);
|
| 25 |
+
setStatus("success");
|
| 26 |
+
setEmail("");
|
| 27 |
+
} catch (err: any) {
|
| 28 |
+
console.error("Subscription failed:", err);
|
| 29 |
+
setStatus("error");
|
| 30 |
+
|
| 31 |
+
// Extract a descriptive human error message
|
| 32 |
+
let msg = "Something went wrong. Please check your network or try again.";
|
| 33 |
+
if (err instanceof Error) {
|
| 34 |
+
try {
|
| 35 |
+
const parsed = JSON.parse(err.message);
|
| 36 |
+
if (parsed.error) {
|
| 37 |
+
msg = parsed.error;
|
| 38 |
+
}
|
| 39 |
+
} catch {
|
| 40 |
+
msg = err.message;
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
setErrorMessage(msg);
|
| 44 |
+
} finally {
|
| 45 |
+
setLoading(false);
|
| 46 |
+
}
|
| 47 |
+
};
|
| 48 |
+
|
| 49 |
+
return (
|
| 50 |
+
<div className="space-y-4" id="newsletter-signup-container">
|
| 51 |
+
<div className="space-y-1.5">
|
| 52 |
+
<h4 className={`font-display font-medium text-xs uppercase tracking-widest flex items-center gap-1.5 font-mono ${
|
| 53 |
+
darkMode ? "text-slate-200" : "text-slate-950"
|
| 54 |
+
}`}>
|
| 55 |
+
<Mail className="w-3.5 h-3.5 text-brand-orange" />
|
| 56 |
+
<span>Maintenance Newsletter</span>
|
| 57 |
+
</h4>
|
| 58 |
+
<p className="leading-relaxed text-[11px] text-slate-500">
|
| 59 |
+
Subscribe to receive seasonal tyre maintenance advice, road safety checks, and priority showroom discount vouchers in Lahore.
|
| 60 |
+
</p>
|
| 61 |
+
</div>
|
| 62 |
+
|
| 63 |
+
{status === "success" ? (
|
| 64 |
+
<div
|
| 65 |
+
className={`p-3 rounded-2xl border flex items-start space-x-2.5 transition-all animate-fadeIn ${
|
| 66 |
+
darkMode
|
| 67 |
+
? "bg-emerald-500/10 border-emerald-500/20 text-emerald-400"
|
| 68 |
+
: "bg-emerald-50 border-emerald-200 text-emerald-800"
|
| 69 |
+
}`}
|
| 70 |
+
id="newsletter-success-alert"
|
| 71 |
+
>
|
| 72 |
+
<div className="p-1 rounded-full bg-emerald-500/20 text-emerald-400 mt-0.5 shrink-0">
|
| 73 |
+
<Check className="w-3.5 h-3.5" />
|
| 74 |
+
</div>
|
| 75 |
+
<div>
|
| 76 |
+
<p className="font-bold text-xs">Successfully Subscribed!</p>
|
| 77 |
+
<p className="text-[10px] mt-0.5 opacity-90">
|
| 78 |
+
You're registered to receive maintenance tips. Stay safe on the road!
|
| 79 |
+
</p>
|
| 80 |
+
</div>
|
| 81 |
+
</div>
|
| 82 |
+
) : (
|
| 83 |
+
<form onSubmit={handleSubscribe} className="space-y-2">
|
| 84 |
+
<div className="relative">
|
| 85 |
+
<input
|
| 86 |
+
type="email"
|
| 87 |
+
value={email}
|
| 88 |
+
onChange={(e) => setEmail(e.target.value)}
|
| 89 |
+
placeholder="Enter your email address"
|
| 90 |
+
required
|
| 91 |
+
disabled={loading}
|
| 92 |
+
className={`w-full text-xs px-3.5 py-2.5 pr-10 rounded-xl border font-sans focus:outline-hidden focus:ring-1 focus:ring-brand-orange transition-all duration-200 ${
|
| 93 |
+
darkMode
|
| 94 |
+
? "bg-black/40 border-white/10 text-white placeholder-slate-500 focus:border-brand-orange/40"
|
| 95 |
+
: "bg-white border-slate-200 text-slate-900 placeholder-slate-400 focus:border-brand-orange"
|
| 96 |
+
}`}
|
| 97 |
+
id="newsletter-email-input"
|
| 98 |
+
/>
|
| 99 |
+
<button
|
| 100 |
+
type="submit"
|
| 101 |
+
disabled={loading || !email.trim()}
|
| 102 |
+
className="absolute right-1.5 top-1/2 -translate-y-1/2 p-1.5 rounded-lg bg-brand-orange hover:bg-brand-orange-dark text-white hover:scale-105 active:scale-95 transition-all duration-200 disabled:opacity-50 disabled:pointer-events-none"
|
| 103 |
+
title="Subscribe"
|
| 104 |
+
id="newsletter-submit-btn"
|
| 105 |
+
>
|
| 106 |
+
{loading ? (
|
| 107 |
+
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
| 108 |
+
) : (
|
| 109 |
+
<Send className="w-3.5 h-3.5" />
|
| 110 |
+
)}
|
| 111 |
+
</button>
|
| 112 |
+
</div>
|
| 113 |
+
|
| 114 |
+
{status === "error" && (
|
| 115 |
+
<p className="text-[10px] text-rose-500 font-medium px-1 leading-snug" id="newsletter-error-message">
|
| 116 |
+
⚠️ {errorMessage}
|
| 117 |
+
</p>
|
| 118 |
+
)}
|
| 119 |
+
|
| 120 |
+
<div className="flex items-center space-x-1.5 text-[10px] text-slate-500 font-mono px-1">
|
| 121 |
+
<Sparkles className="w-3 h-3 text-brand-orange/60 shrink-0" />
|
| 122 |
+
<span>Updates twice a month • Opt-out anytime</span>
|
| 123 |
+
</div>
|
| 124 |
+
</form>
|
| 125 |
+
)}
|
| 126 |
+
</div>
|
| 127 |
+
);
|
| 128 |
+
}
|
src/components/ProductDetailsModal.tsx
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { X, MessageSquare, ShieldCheck, CheckCircle2, Phone, Compass } from "lucide-react";
|
| 2 |
+
import { TireProduct, BUSINESS_INFO } from "../data";
|
| 3 |
+
|
| 4 |
+
interface ProductDetailsModalProps {
|
| 5 |
+
tire: TireProduct | null;
|
| 6 |
+
onClose: () => void;
|
| 7 |
+
darkMode: boolean;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export default function ProductDetailsModal({ tire, onClose, darkMode }: ProductDetailsModalProps) {
|
| 11 |
+
if (!tire) return null;
|
| 12 |
+
|
| 13 |
+
const waPreFilledText = `Hello Haider Brothers, I want price & stock for ${tire.name} (Size: ${tire.size})`;
|
| 14 |
+
const encodedText = encodeURIComponent(waPreFilledText);
|
| 15 |
+
const waLink = `${BUSINESS_INFO.whatsappUrl}?text=${encodedText}`;
|
| 16 |
+
|
| 17 |
+
// Calculated physical traits for mock premium display
|
| 18 |
+
const loadSpeedIndex = tire.size.includes("14") ? "88H" : tire.size.includes("15") ? "91H" : tire.size.includes("16") ? "94V" : tire.size.includes("17") ? "98W" : "103W";
|
| 19 |
+
const treadwear = tire.brand.toLowerCase() === "michelin" || tire.brand.toLowerCase() === "continental" ? "540 A A" : "400 A A";
|
| 20 |
+
|
| 21 |
+
return (
|
| 22 |
+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6 overflow-y-auto bg-black/80 backdrop-blur-sm animate-fadeIn">
|
| 23 |
+
{/* Container Card */}
|
| 24 |
+
<div
|
| 25 |
+
className={`relative w-full max-w-3xl rounded-3xl overflow-hidden shadow-2xl transition-all border duration-300 ${
|
| 26 |
+
darkMode ? "bg-[#1c1c24] border-white/10 text-white" : "bg-white border-slate-200 text-slate-900"
|
| 27 |
+
}`}
|
| 28 |
+
>
|
| 29 |
+
{/* Close Button */}
|
| 30 |
+
<button
|
| 31 |
+
onClick={onClose}
|
| 32 |
+
className={`absolute top-4 right-4 z-10 p-2 rounded-xl transition-all ${
|
| 33 |
+
darkMode ? "bg-black/40 hover:bg-black/60 text-white" : "bg-slate-100 hover:bg-slate-200 text-slate-800"
|
| 34 |
+
}`}
|
| 35 |
+
aria-label="Close specifications modal"
|
| 36 |
+
id="close-modal-btn"
|
| 37 |
+
>
|
| 38 |
+
<X className="w-5 h-5" />
|
| 39 |
+
</button>
|
| 40 |
+
|
| 41 |
+
<div className="grid grid-cols-1 md:grid-cols-2">
|
| 42 |
+
{/* Card Media Section */}
|
| 43 |
+
<div className="relative aspect-square md:h-full overflow-hidden bg-slate-950">
|
| 44 |
+
<img
|
| 45 |
+
src={tire.image}
|
| 46 |
+
alt={tire.name}
|
| 47 |
+
className="w-full h-full object-cover"
|
| 48 |
+
referrerPolicy="no-referrer"
|
| 49 |
+
/>
|
| 50 |
+
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent pointer-events-none"></div>
|
| 51 |
+
|
| 52 |
+
{/* Image caption absolute */}
|
| 53 |
+
<div className="absolute bottom-6 left-6 right-6">
|
| 54 |
+
<span className="bg-brand-orange text-white text-[10px] font-mono font-bold px-2.5 py-1 rounded-full uppercase tracking-wider block w-fit mb-2">
|
| 55 |
+
ORIGINAL SPECIFICATIONS
|
| 56 |
+
</span>
|
| 57 |
+
<h3 className="font-display text-2xl font-extrabold text-white leading-tight">
|
| 58 |
+
{tire.name}
|
| 59 |
+
</h3>
|
| 60 |
+
<p className="text-xs text-slate-300 mt-1">Authorized Import & Supply</p>
|
| 61 |
+
</div>
|
| 62 |
+
</div>
|
| 63 |
+
|
| 64 |
+
{/* Details / Spec column */}
|
| 65 |
+
<div className="p-6 sm:p-8 flex flex-col justify-between space-y-6">
|
| 66 |
+
<div className="space-y-4">
|
| 67 |
+
<div className="flex items-center space-x-2">
|
| 68 |
+
<span className="text-brand-orange font-bold text-xs uppercase tracking-widest font-mono">
|
| 69 |
+
{tire.brand} Tyre Details
|
| 70 |
+
</span>
|
| 71 |
+
<span className="w-1 h-1 rounded-full bg-slate-500"></span>
|
| 72 |
+
<span className={`text-xs font-semibold ${
|
| 73 |
+
tire.stock !== undefined && tire.stock < 5
|
| 74 |
+
? "text-rose-500 animate-pulse"
|
| 75 |
+
: "text-emerald-500"
|
| 76 |
+
}`}>
|
| 77 |
+
Lahore Stock: {tire.stock !== undefined ? `${tire.stock} Units Remaining` : "Available (In Stock)"}
|
| 78 |
+
</span>
|
| 79 |
+
</div>
|
| 80 |
+
|
| 81 |
+
<div className="border-b pb-4 border-black/5 dark:border-white/5">
|
| 82 |
+
<p className={`text-xs leading-relaxed ${darkMode ? "text-slate-300" : "text-slate-600"}`}>
|
| 83 |
+
{tire.description} Perfect for drivers focused on safety, longevity, and comfortable ride acoustics on both dual highways and local Lahore residential lanes.
|
| 84 |
+
</p>
|
| 85 |
+
</div>
|
| 86 |
+
|
| 87 |
+
{/* Specs Bento Grid Table */}
|
| 88 |
+
<div className="grid grid-cols-2 gap-3 pt-2">
|
| 89 |
+
<div className={`p-3 rounded-xl border ${darkMode ? "bg-white/5 border-white/5" : "bg-slate-50 border-slate-100"}`}>
|
| 90 |
+
<span className={`text-[10px] uppercase font-mono tracking-wider block ${darkMode ? "text-slate-400" : "text-slate-500"}`}>
|
| 91 |
+
Tyre Size
|
| 92 |
+
</span>
|
| 93 |
+
<span className="font-mono text-sm font-bold text-brand-orange">{tire.size}</span>
|
| 94 |
+
</div>
|
| 95 |
+
|
| 96 |
+
<div className={`p-3 rounded-xl border ${darkMode ? "bg-white/5 border-white/5" : "bg-slate-50 border-slate-100"}`}>
|
| 97 |
+
<span className={`text-[10px] uppercase font-mono tracking-wider block ${darkMode ? "text-slate-400" : "text-slate-500"}`}>
|
| 98 |
+
Load/Speed Rating
|
| 99 |
+
</span>
|
| 100 |
+
<span className="font-mono text-sm font-bold">{loadSpeedIndex}</span>
|
| 101 |
+
</div>
|
| 102 |
+
|
| 103 |
+
<div className={`p-3 rounded-xl border ${darkMode ? "bg-white/5 border-white/5" : "bg-slate-50 border-slate-100"}`}>
|
| 104 |
+
<span className={`text-[10px] uppercase font-mono tracking-wider block ${darkMode ? "text-slate-400" : "text-slate-500"}`}>
|
| 105 |
+
UTQG / Treadwear
|
| 106 |
+
</span>
|
| 107 |
+
<span className="font-mono text-sm font-bold">{treadwear}</span>
|
| 108 |
+
</div>
|
| 109 |
+
|
| 110 |
+
<div className={`p-3 rounded-xl border ${darkMode ? "bg-white/5 border-white/5" : "bg-slate-50 border-slate-100"}`}>
|
| 111 |
+
<span className={`text-[10px] uppercase font-mono tracking-wider block ${darkMode ? "text-slate-400" : "text-slate-500"}`}>
|
| 112 |
+
Installation
|
| 113 |
+
</span>
|
| 114 |
+
<span className="text-green-500 text-xs font-bold font-mono">FREE AT STORE</span>
|
| 115 |
+
</div>
|
| 116 |
+
</div>
|
| 117 |
+
|
| 118 |
+
{/* Warranties flags */}
|
| 119 |
+
<div className="space-y-2 pt-1 font-mono text-[11px]">
|
| 120 |
+
<div className="flex items-center space-x-1.5 text-emerald-500">
|
| 121 |
+
<CheckCircle2 className="w-4 h-4" />
|
| 122 |
+
<span>Official Year Manufacturing Date Warranted</span>
|
| 123 |
+
</div>
|
| 124 |
+
<div className="flex items-center space-x-1.5 text-brand-orange">
|
| 125 |
+
<Compass className="w-4 h-4" />
|
| 126 |
+
<span>Optimized for Pakistan's heat cycles & road asphalt</span>
|
| 127 |
+
</div>
|
| 128 |
+
</div>
|
| 129 |
+
</div>
|
| 130 |
+
|
| 131 |
+
{/* Price & Primary Call-to-actions */}
|
| 132 |
+
<div className="pt-4 border-t border-black/5 dark:border-white/5 space-y-4">
|
| 133 |
+
<div className="flex items-baseline justify-between">
|
| 134 |
+
<span className={`text-xs font-mono tracking-wider ${darkMode ? "text-slate-400" : "text-slate-500"}`}>
|
| 135 |
+
Wholesale Unit Price
|
| 136 |
+
</span>
|
| 137 |
+
<span className="text-brand-orange font-display text-2xl font-black">
|
| 138 |
+
PKR {tire.price.toLocaleString()}
|
| 139 |
+
</span>
|
| 140 |
+
</div>
|
| 141 |
+
|
| 142 |
+
<div className="grid grid-cols-2 gap-3">
|
| 143 |
+
{/* Dial Store hotline */}
|
| 144 |
+
<a
|
| 145 |
+
href={`tel:${BUSINESS_INFO.phoneRaw}`}
|
| 146 |
+
className={`flex items-center justify-center space-x-2 py-3.5 rounded-xl text-xs font-bold transition-all border ${
|
| 147 |
+
darkMode
|
| 148 |
+
? "border-white/10 bg-white/5 hover:bg-white/10 text-white"
|
| 149 |
+
: "border-slate-200 bg-slate-50 hover:bg-slate-100 text-slate-800"
|
| 150 |
+
}`}
|
| 151 |
+
id="modal-phone-btn"
|
| 152 |
+
>
|
| 153 |
+
<Phone className="w-4 h-4 text-brand-orange" />
|
| 154 |
+
<span>Call Store Office</span>
|
| 155 |
+
</a>
|
| 156 |
+
|
| 157 |
+
{/* WhatsApp Chat prefilled */}
|
| 158 |
+
<a
|
| 159 |
+
href={waLink}
|
| 160 |
+
target="_blank"
|
| 161 |
+
rel="noreferrer referrer"
|
| 162 |
+
className="flex items-center justify-center space-x-2 bg-gradient-to-r from-green-600 to-emerald-500 hover:from-green-500 hover:to-emerald-400 text-white py-3.5 rounded-xl text-xs font-bold shadow-md shadow-green-600/10 transition-all"
|
| 163 |
+
id="modal-wa-btn"
|
| 164 |
+
>
|
| 165 |
+
<MessageSquare className="w-4 h-4" />
|
| 166 |
+
<span>WhatsApp Secure Order</span>
|
| 167 |
+
</a>
|
| 168 |
+
</div>
|
| 169 |
+
</div>
|
| 170 |
+
</div>
|
| 171 |
+
</div>
|
| 172 |
+
</div>
|
| 173 |
+
</div>
|
| 174 |
+
);
|
| 175 |
+
}
|
src/components/Promotions.tsx
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useEffect } from "react";
|
| 2 |
+
import { motion, AnimatePresence } from "motion/react";
|
| 3 |
+
import {
|
| 4 |
+
ChevronLeft,
|
| 5 |
+
ChevronRight,
|
| 6 |
+
Sparkles,
|
| 7 |
+
Percent,
|
| 8 |
+
Gift,
|
| 9 |
+
ShieldCheck,
|
| 10 |
+
Flame,
|
| 11 |
+
TrendingUp,
|
| 12 |
+
Calendar,
|
| 13 |
+
AlertCircle
|
| 14 |
+
} from "lucide-react";
|
| 15 |
+
import { BUSINESS_INFO } from "../data";
|
| 16 |
+
|
| 17 |
+
interface PromoOffer {
|
| 18 |
+
id: string;
|
| 19 |
+
title: string;
|
| 20 |
+
subtitle: string;
|
| 21 |
+
tagline: string;
|
| 22 |
+
description: string;
|
| 23 |
+
badge: {
|
| 24 |
+
text: string;
|
| 25 |
+
type: "discount" | "bundle" | "limited" | "exclusive";
|
| 26 |
+
};
|
| 27 |
+
highlightFeatures: string[];
|
| 28 |
+
duration: string;
|
| 29 |
+
actionText: string;
|
| 30 |
+
gradient: string;
|
| 31 |
+
icon: any;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
export default function Promotions({ darkMode }: { darkMode: boolean }) {
|
| 35 |
+
const [currentIndex, setCurrentIndex] = useState<number>(0);
|
| 36 |
+
const [isAutoPlaying, setIsAutoPlaying] = useState<boolean>(true);
|
| 37 |
+
|
| 38 |
+
const PROMO_DATA: PromoOffer[] = [
|
| 39 |
+
{
|
| 40 |
+
id: "monsoon-alignment-bundle",
|
| 41 |
+
title: "Lahore Monsoon Safety Package",
|
| 42 |
+
subtitle: "FREE Computerized 3D Alignment & Wheel Balancing",
|
| 43 |
+
tagline: "Total peace-of-mind during rainy seasons on wet Lahore roads",
|
| 44 |
+
description: "Buy any premium set of 4 Tyres (Yokohama, Michelin, or Continental) and unlock complete computerized German 3D alignment, nitrogen air filling, and four heavy-duty brass safety valves absolutely free of charge.",
|
| 45 |
+
badge: {
|
| 46 |
+
text: "Special Bundle",
|
| 47 |
+
type: "bundle"
|
| 48 |
+
},
|
| 49 |
+
highlightFeatures: [
|
| 50 |
+
"Free 3D Alignment (PKR 3,500 value)",
|
| 51 |
+
"Premium German Tubeless Valves",
|
| 52 |
+
"Computerized Wheel Balancing Included",
|
| 53 |
+
"Lifetime Nitrogen topping-up sessions"
|
| 54 |
+
],
|
| 55 |
+
duration: "Valid until July 31, 2026",
|
| 56 |
+
actionText: "Claim Bundle on WhatsApp",
|
| 57 |
+
gradient: "from-blue-600 via-indigo-900 to-[#0e1017]",
|
| 58 |
+
icon: Gift,
|
| 59 |
+
},
|
| 60 |
+
{
|
| 61 |
+
id: "yokohama-summer-blast",
|
| 62 |
+
title: "Summer Heat-Resistant Tyre Fest",
|
| 63 |
+
subtitle: "Save up to 15% on Eco-Friendly Japanese Yokohama Imports",
|
| 64 |
+
tagline: "Advanced rubber compounds designed for Pakistan's extreme summer asphalt",
|
| 65 |
+
description: "Yokohama BluEarth and Advan series tyres have advanced silica compound protection to withhold high highway friction and temperature spikes during summer without risking tire decompression.",
|
| 66 |
+
badge: {
|
| 67 |
+
text: "Flat Discount",
|
| 68 |
+
type: "discount"
|
| 69 |
+
},
|
| 70 |
+
highlightFeatures: [
|
| 71 |
+
"15% Flat Rebate on selected sizes",
|
| 72 |
+
"Genuine 2026 Import DOT markings",
|
| 73 |
+
"Wet traction Class-A safety ratings",
|
| 74 |
+
"Reduced fuel consumption thread patterns"
|
| 75 |
+
],
|
| 76 |
+
duration: "Limited stock quota",
|
| 77 |
+
actionText: "Check Size Compatibility",
|
| 78 |
+
gradient: "from-brand-orange via-amber-850 to-[#120703]",
|
| 79 |
+
icon: Percent,
|
| 80 |
+
},
|
| 81 |
+
{
|
| 82 |
+
id: "executive-continental-bundle",
|
| 83 |
+
title: "Continental Premium Executive Deal",
|
| 84 |
+
subtitle: "Get PKR 8,000 Instant Voucher on Set of 4 Continental Tyres",
|
| 85 |
+
tagline: "Engineered in Germany, trusted on Pakistani Motorways",
|
| 86 |
+
description: "Elevate your sedan's comfort level. Continental offers exceptional noise reduction and high-speed stability. Install at our Faisal Town showroom to claim your premium service cashback voucher.",
|
| 87 |
+
badge: {
|
| 88 |
+
text: "Exclusive Reward",
|
| 89 |
+
type: "exclusive"
|
| 90 |
+
},
|
| 91 |
+
highlightFeatures: [
|
| 92 |
+
"PKR 8,000 Instant Cash Voucher",
|
| 93 |
+
"Free Professional fitting & balancing",
|
| 94 |
+
"5-Year official structural backup warranty",
|
| 95 |
+
"Complimentary Suspension checkups"
|
| 96 |
+
],
|
| 97 |
+
duration: "Hurry! Active this week only",
|
| 98 |
+
actionText: "Secure Cash Voucher",
|
| 99 |
+
gradient: "from-emerald-600 via-teal-950 to-[#0c1613]",
|
| 100 |
+
icon: Sparkles,
|
| 101 |
+
},
|
| 102 |
+
{
|
| 103 |
+
id: "general-tyre-pakistan",
|
| 104 |
+
title: "General Tyre Durability Drive",
|
| 105 |
+
subtitle: "Flat 8% Off on All Local General Tyre Products",
|
| 106 |
+
tagline: "The absolute rugged champion for everyday Lahore traffic and pothole terrains",
|
| 107 |
+
description: "Specially formulated for Pakistani road conditions. Extreme puncture-resistance and thick reinforcement sidewalls guarantee reliable performance for cabs, sedans, and commercial wagons.",
|
| 108 |
+
badge: {
|
| 109 |
+
text: "High Durability",
|
| 110 |
+
type: "limited"
|
| 111 |
+
},
|
| 112 |
+
highlightFeatures: [
|
| 113 |
+
"Economical Lahore pricing",
|
| 114 |
+
"Reinforced steel belt core protection",
|
| 115 |
+
"Excellent water-channeling grooves",
|
| 116 |
+
"Unbeatable lifetime warranty terms"
|
| 117 |
+
],
|
| 118 |
+
duration: "Offer valid on cash payments",
|
| 119 |
+
actionText: "Inquire stock levels",
|
| 120 |
+
gradient: "from-red-600 via-rose-900 to-[#1e0a0f]",
|
| 121 |
+
icon: ShieldCheck,
|
| 122 |
+
}
|
| 123 |
+
];
|
| 124 |
+
|
| 125 |
+
// Auto-play timer
|
| 126 |
+
useEffect(() => {
|
| 127 |
+
if (!isAutoPlaying) return;
|
| 128 |
+
const interval = setInterval(() => {
|
| 129 |
+
setCurrentIndex((prev) => (prev + 1) % PROMO_DATA.length);
|
| 130 |
+
}, 6000);
|
| 131 |
+
return () => clearInterval(interval);
|
| 132 |
+
}, [isAutoPlaying, PROMO_DATA.length]);
|
| 133 |
+
|
| 134 |
+
const handlePrev = () => {
|
| 135 |
+
setIsAutoPlaying(false);
|
| 136 |
+
setCurrentIndex((prev) => (prev - 1 + PROMO_DATA.length) % PROMO_DATA.length);
|
| 137 |
+
};
|
| 138 |
+
|
| 139 |
+
const handleNext = () => {
|
| 140 |
+
setIsAutoPlaying(false);
|
| 141 |
+
setCurrentIndex((prev) => (prev + 1) % PROMO_DATA.length);
|
| 142 |
+
};
|
| 143 |
+
|
| 144 |
+
const getBadgeStyles = (type: string) => {
|
| 145 |
+
switch (type) {
|
| 146 |
+
case "discount":
|
| 147 |
+
return "bg-amber-500/15 text-amber-400 border border-amber-500/30";
|
| 148 |
+
case "bundle":
|
| 149 |
+
return "bg-blue-500/15 text-blue-400 border border-blue-500/30";
|
| 150 |
+
case "exclusive":
|
| 151 |
+
return "bg-purple-500/15 text-purple-400 border border-purple-500/30";
|
| 152 |
+
default:
|
| 153 |
+
return "bg-red-500/15 text-red-400 border border-red-500/30";
|
| 154 |
+
}
|
| 155 |
+
};
|
| 156 |
+
|
| 157 |
+
const currentPromo = PROMO_DATA[currentIndex];
|
| 158 |
+
const IconComponent = currentPromo.icon;
|
| 159 |
+
|
| 160 |
+
return (
|
| 161 |
+
<section
|
| 162 |
+
id="promotions"
|
| 163 |
+
className="py-16 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"
|
| 164 |
+
onMouseEnter={() => setIsAutoPlaying(false)}
|
| 165 |
+
onMouseLeave={() => setIsAutoPlaying(true)}
|
| 166 |
+
>
|
| 167 |
+
<div className="text-center max-w-2xl mx-auto mb-10">
|
| 168 |
+
<span className="text-xs font-mono tracking-widest uppercase bg-brand-orange/15 text-brand-orange px-3.5 py-1 rounded-full font-extrabold flex items-center justify-center w-fit mx-auto gap-1.5 shadow-sm">
|
| 169 |
+
<Flame className="w-3.5 h-3.5 animate-pulse" />
|
| 170 |
+
<span>Seasonal Promotions & Offers</span>
|
| 171 |
+
</span>
|
| 172 |
+
<h2 className="text-2xl sm:text-4xl font-display font-extrabold tracking-tight mt-3">
|
| 173 |
+
Special Bundle Packages & Discounts
|
| 174 |
+
</h2>
|
| 175 |
+
<p className={`text-sm mt-2 ${darkMode ? "text-slate-400" : "text-slate-600"}`}>
|
| 176 |
+
Exclusively designed packages for Lahore drivers. Enjoy free alignment services, road hazard upgrades, and seasonal cash savings.
|
| 177 |
+
</p>
|
| 178 |
+
</div>
|
| 179 |
+
|
| 180 |
+
{/* Main Carousel Frame */}
|
| 181 |
+
<div
|
| 182 |
+
className={`relative rounded-3xl overflow-hidden border ${
|
| 183 |
+
darkMode ? "border-white/10 bg-black/40 shadow-2xl" : "border-slate-200 bg-white shadow-xl"
|
| 184 |
+
}`}
|
| 185 |
+
id="promotions-carousel-stage"
|
| 186 |
+
>
|
| 187 |
+
<div className="absolute inset-0 bg-linear-to-r from-black/80 to-transparent z-10 pointer-events-none md:block hidden" />
|
| 188 |
+
|
| 189 |
+
{/* Carousel Content Render */}
|
| 190 |
+
<div className="relative min-h-[460px] md:min-h-[400px] flex flex-col justify-center overflow-hidden">
|
| 191 |
+
<AnimatePresence mode="wait">
|
| 192 |
+
<motion.div
|
| 193 |
+
key={currentPromo.id}
|
| 194 |
+
initial={{ opacity: 0, x: 50 }}
|
| 195 |
+
animate={{ opacity: 1, x: 0 }}
|
| 196 |
+
exit={{ opacity: 0, x: -50 }}
|
| 197 |
+
transition={{ duration: 0.5, ease: "easeInOut" }}
|
| 198 |
+
className={`absolute inset-0 w-full h-full bg-gradient-to-r ${currentPromo.gradient} flex flex-col md:flex-row items-stretch`}
|
| 199 |
+
>
|
| 200 |
+
{/* Left Column: Promotion Info */}
|
| 201 |
+
<div className="flex-1 p-6 sm:p-10 md:p-12 flex flex-col justify-between z-20 text-white select-none">
|
| 202 |
+
<div className="space-y-4">
|
| 203 |
+
<div className="flex flex-wrap items-center gap-2.5">
|
| 204 |
+
<span className={`text-[10px] uppercase font-mono tracking-widest px-3 py-1 rounded-full font-bold ${getBadgeStyles(currentPromo.badge.type)}`}>
|
| 205 |
+
{currentPromo.badge.text}
|
| 206 |
+
</span>
|
| 207 |
+
<span className="text-[10px] font-mono text-slate-300 flex items-center gap-1">
|
| 208 |
+
<Calendar className="w-3.5 h-3.5 text-brand-orange" />
|
| 209 |
+
{currentPromo.duration}
|
| 210 |
+
</span>
|
| 211 |
+
</div>
|
| 212 |
+
|
| 213 |
+
<div className="space-y-1">
|
| 214 |
+
<h3 className="text-xl sm:text-2xl md:text-3xl font-display font-black tracking-tight text-white leading-tight">
|
| 215 |
+
{currentPromo.title}
|
| 216 |
+
</h3>
|
| 217 |
+
<h4 className="text-sm sm:text-base font-semibold text-brand-orange font-mono">
|
| 218 |
+
{currentPromo.subtitle}
|
| 219 |
+
</h4>
|
| 220 |
+
</div>
|
| 221 |
+
|
| 222 |
+
<p className="text-xs sm:text-sm text-slate-300 leading-relaxed font-sans max-w-xl">
|
| 223 |
+
{currentPromo.description}
|
| 224 |
+
</p>
|
| 225 |
+
|
| 226 |
+
{/* Highlights Grid */}
|
| 227 |
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2 pt-2">
|
| 228 |
+
{currentPromo.highlightFeatures.map((feat, i) => (
|
| 229 |
+
<div key={i} className="flex items-center space-x-2 text-xs text-slate-200">
|
| 230 |
+
<span className="w-1.5 h-1.5 rounded-full bg-brand-orange shrink-0" />
|
| 231 |
+
<span>{feat}</span>
|
| 232 |
+
</div>
|
| 233 |
+
))}
|
| 234 |
+
</div>
|
| 235 |
+
</div>
|
| 236 |
+
|
| 237 |
+
{/* Bottom Interactive Trigger Area */}
|
| 238 |
+
<div className="pt-6 sm:pt-0 mt-6 md:mt-0 flex flex-col sm:flex-row items-start sm:items-center gap-4">
|
| 239 |
+
<a
|
| 240 |
+
href={`${BUSINESS_INFO.whatsappUrl}?text=${encodeURIComponent(
|
| 241 |
+
`Hello Haider Brothers, I'm interested in the promotion: *${currentPromo.title}* (${currentPromo.subtitle}).`
|
| 242 |
+
)}`}
|
| 243 |
+
target="_blank"
|
| 244 |
+
rel="noreferrer referrer"
|
| 245 |
+
className="bg-brand-orange hover:bg-brand-orange-dark text-white font-bold text-xs px-5 py-3 rounded-xl flex items-center space-x-2 transition-all shadow-lg shadow-brand-orange/25"
|
| 246 |
+
>
|
| 247 |
+
<span>{currentPromo.actionText}</span>
|
| 248 |
+
<ChevronRight className="w-4 h-4" />
|
| 249 |
+
</a>
|
| 250 |
+
<span className="text-[10px] text-slate-400 font-mono italic flex items-center gap-1">
|
| 251 |
+
<AlertCircle className="w-3.5 h-3.5 text-slate-500 shrink-0" />
|
| 252 |
+
Premium standard tire sizes apply. Rates subject to stock.
|
| 253 |
+
</span>
|
| 254 |
+
</div>
|
| 255 |
+
</div>
|
| 256 |
+
|
| 257 |
+
{/* Right Column: High Fidelity Aesthetic Visual Cover art */}
|
| 258 |
+
<div className="w-full md:w-[40%] bg-black/40 md:bg-transparent relative overflow-hidden flex items-center justify-center p-8 md:p-0">
|
| 259 |
+
{/* Glowing aesthetic backdrop circle matching promotion icon */}
|
| 260 |
+
<div className="absolute w-52 h-52 sm:w-64 sm:h-64 rounded-full bg-brand-orange/15 blur-3xl" />
|
| 261 |
+
|
| 262 |
+
<div className="relative text-center flex flex-col items-center justify-center space-y-3 z-20">
|
| 263 |
+
<div className="p-5 rounded-3xl bg-white/5 border border-white/10 shadow-lg backdrop-blur-md">
|
| 264 |
+
<IconComponent className="w-12 h-12 text-brand-orange animate-pulse" />
|
| 265 |
+
</div>
|
| 266 |
+
<span className="text-xs font-mono tracking-widest uppercase text-slate-300 font-bold block">
|
| 267 |
+
Haider Brothers Deals
|
| 268 |
+
</span>
|
| 269 |
+
<p className="text-[10px] text-slate-400 max-w-[200px] text-center mx-auto">
|
| 270 |
+
{currentPromo.tagline}
|
| 271 |
+
</p>
|
| 272 |
+
</div>
|
| 273 |
+
</div>
|
| 274 |
+
</motion.div>
|
| 275 |
+
</AnimatePresence>
|
| 276 |
+
</div>
|
| 277 |
+
|
| 278 |
+
{/* Manual Carousel Chevron Controls */}
|
| 279 |
+
<button
|
| 280 |
+
onClick={handlePrev}
|
| 281 |
+
className="absolute left-4 top-1/2 -translate-y-1/2 z-30 p-2 rounded-xl border border-white/10 bg-black/30 hover:bg-black/50 text-white hover:scale-105 active:scale-95 transition-all text-sm backdrop-blur-md"
|
| 282 |
+
title="Previous slide"
|
| 283 |
+
type="button"
|
| 284 |
+
id="btn-promo-prev"
|
| 285 |
+
>
|
| 286 |
+
<ChevronLeft className="w-4 h-4" />
|
| 287 |
+
</button>
|
| 288 |
+
<button
|
| 289 |
+
onClick={handleNext}
|
| 290 |
+
className="absolute right-4 top-1/2 -translate-y-1/2 z-30 p-2 rounded-xl border border-white/10 bg-black/30 hover:bg-black/50 text-white hover:scale-105 active:scale-95 transition-all text-sm backdrop-blur-md"
|
| 291 |
+
title="Next slide"
|
| 292 |
+
type="button"
|
| 293 |
+
id="btn-promo-next"
|
| 294 |
+
>
|
| 295 |
+
<ChevronRight className="w-4 h-4" />
|
| 296 |
+
</button>
|
| 297 |
+
|
| 298 |
+
{/* Bullet Progress Indicator bar */}
|
| 299 |
+
<div className="absolute bottom-4 left-1/2 -translate-y-0 -translate-x-1/2 z-30 flex items-center space-x-2 bg-black/20 p-1.5 px-3 rounded-full backdrop-blur-sm">
|
| 300 |
+
{PROMO_DATA.map((promo, idx) => (
|
| 301 |
+
<button
|
| 302 |
+
key={promo.id}
|
| 303 |
+
onClick={() => {
|
| 304 |
+
setIsAutoPlaying(false);
|
| 305 |
+
setCurrentIndex(idx);
|
| 306 |
+
}}
|
| 307 |
+
className={`h-2.5 rounded-full transition-all duration-300 ${
|
| 308 |
+
idx === currentIndex ? "w-6 bg-brand-orange" : "w-2.5 bg-slate-500/50"
|
| 309 |
+
}`}
|
| 310 |
+
title={`Go to slide ${idx + 1}`}
|
| 311 |
+
type="button"
|
| 312 |
+
/>
|
| 313 |
+
))}
|
| 314 |
+
</div>
|
| 315 |
+
</div>
|
| 316 |
+
</section>
|
| 317 |
+
);
|
| 318 |
+
}
|
src/components/ReviewsSection.tsx
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from "react";
|
| 2 |
+
import { Star, Sparkles, CheckCircle2 } from "lucide-react";
|
| 3 |
+
|
| 4 |
+
interface ReviewsSectionProps {
|
| 5 |
+
darkMode: boolean;
|
| 6 |
+
reviews: any[];
|
| 7 |
+
onAddReview: (review: { name: string; rating: number; text: string; date: string; initials: string }) => Promise<void>;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export default function ReviewsSection({ darkMode, reviews, onAddReview }: ReviewsSectionProps) {
|
| 11 |
+
const [newReview, setNewReview] = useState({
|
| 12 |
+
name: "",
|
| 13 |
+
text: "",
|
| 14 |
+
rating: 5
|
| 15 |
+
});
|
| 16 |
+
const [formSuccess, setFormSuccess] = useState(false);
|
| 17 |
+
const [hoverRating, setHoverRating] = useState<number | null>(null);
|
| 18 |
+
|
| 19 |
+
const handleSubmit = async (e: React.FormEvent) => {
|
| 20 |
+
e.preventDefault();
|
| 21 |
+
if (!newReview.name.trim() || !newReview.text.trim()) return;
|
| 22 |
+
|
| 23 |
+
// Grab initials
|
| 24 |
+
const initials = newReview.name
|
| 25 |
+
.split(" ")
|
| 26 |
+
.map((n) => n[0])
|
| 27 |
+
.join("")
|
| 28 |
+
.toUpperCase()
|
| 29 |
+
.slice(0, 2) || "U";
|
| 30 |
+
|
| 31 |
+
const newlyCreated = {
|
| 32 |
+
name: newReview.name,
|
| 33 |
+
rating: newReview.rating,
|
| 34 |
+
text: newReview.text,
|
| 35 |
+
date: "Just now",
|
| 36 |
+
initials: initials
|
| 37 |
+
};
|
| 38 |
+
|
| 39 |
+
try {
|
| 40 |
+
await onAddReview(newlyCreated);
|
| 41 |
+
setNewReview({ name: "", text: "", rating: 5 });
|
| 42 |
+
setFormSuccess(true);
|
| 43 |
+
setTimeout(() => setFormSuccess(false), 4500);
|
| 44 |
+
} catch (e) {
|
| 45 |
+
console.error(e);
|
| 46 |
+
}
|
| 47 |
+
};
|
| 48 |
+
|
| 49 |
+
return (
|
| 50 |
+
<section
|
| 51 |
+
id="reviews"
|
| 52 |
+
className={`py-24 relative overflow-hidden transition-colors duration-300 ${
|
| 53 |
+
darkMode ? "bg-brand-dark/95 text-white" : "bg-white text-slate-900"
|
| 54 |
+
}`}
|
| 55 |
+
>
|
| 56 |
+
{/* Visual glowing orbs */}
|
| 57 |
+
<div className="absolute top-1/3 left-0 w-72 h-72 rounded-full bg-brand-orange/10 blur-[90px] pointer-events-none"></div>
|
| 58 |
+
|
| 59 |
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
| 60 |
+
<div className="text-center max-w-2xl mx-auto mb-16">
|
| 61 |
+
<div className="inline-flex items-center space-x-2 bg-brand-orange/10 px-3.5 py-1.5 rounded-full text-brand-orange text-xs font-mono tracking-wider uppercase mb-3">
|
| 62 |
+
<Sparkles className="w-3.5 h-3.5" />
|
| 63 |
+
<span>4.8★ Ratings On Google Maps</span>
|
| 64 |
+
</div>
|
| 65 |
+
<h2 className="text-3xl sm:text-4xl font-display font-extrabold tracking-tight">
|
| 66 |
+
Loved By Lahore's Car Community
|
| 67 |
+
</h2>
|
| 68 |
+
<p className={`text-sm mt-3 ${darkMode ? "text-slate-400" : "text-slate-600"}`}>
|
| 69 |
+
Real testimonials from car, SUV, and commercial truck owners who buy their Michelin, Yokohama, and Autogrip tyres from us.
|
| 70 |
+
</p>
|
| 71 |
+
</div>
|
| 72 |
+
|
| 73 |
+
<div className="grid grid-cols-1 lg:grid-cols-12 gap-10 items-start">
|
| 74 |
+
{/* Left Column: List of Reviews */}
|
| 75 |
+
<div className="lg:col-span-7 space-y-6">
|
| 76 |
+
<div className="grid grid-cols-1 gap-4">
|
| 77 |
+
{reviews.map((review, rIdx) => (
|
| 78 |
+
<div
|
| 79 |
+
key={`${review.name}-${rIdx}`}
|
| 80 |
+
className={`p-6 rounded-2xl border transition-all hover:scale-[1.01] duration-300 ${
|
| 81 |
+
darkMode
|
| 82 |
+
? "bg-[#1c1c24] border-white/5 hover:border-brand-orange/30 shadow-md"
|
| 83 |
+
: "bg-slate-50 border-slate-100 hover:border-brand-orange/30 shadow-sm"
|
| 84 |
+
}`}
|
| 85 |
+
>
|
| 86 |
+
<div className="flex items-start justify-between">
|
| 87 |
+
<div className="flex items-center space-x-3.5">
|
| 88 |
+
<div className="w-10 h-10 rounded-xl bg-orange-600/10 text-brand-orange text-sm font-bold flex items-center justify-center shrink-0">
|
| 89 |
+
{review.initials}
|
| 90 |
+
</div>
|
| 91 |
+
<div>
|
| 92 |
+
<h4 className="font-display font-bold text-sm">{review.name}</h4>
|
| 93 |
+
<span className={`text-[10px] font-mono ${darkMode ? "text-slate-500" : "text-slate-400"}`}>
|
| 94 |
+
Verified User • {review.date}
|
| 95 |
+
</span>
|
| 96 |
+
</div>
|
| 97 |
+
</div>
|
| 98 |
+
|
| 99 |
+
<div className="flex space-x-0.5 text-amber-500">
|
| 100 |
+
{Array.from({ length: 5 }).map((_, sIdx) => (
|
| 101 |
+
<Star
|
| 102 |
+
key={sIdx}
|
| 103 |
+
className={`w-3.5 h-3.5 fill-current ${
|
| 104 |
+
sIdx < Math.floor(review.rating)
|
| 105 |
+
? "text-amber-500"
|
| 106 |
+
: "text-slate-300 dark:text-slate-600"
|
| 107 |
+
}`}
|
| 108 |
+
/>
|
| 109 |
+
))}
|
| 110 |
+
</div>
|
| 111 |
+
</div>
|
| 112 |
+
|
| 113 |
+
<p className={`text-xs sm:text-sm mt-4 italic leading-relaxed ${
|
| 114 |
+
darkMode ? "text-slate-300" : "text-slate-600"
|
| 115 |
+
}`}>
|
| 116 |
+
"{review.text}"
|
| 117 |
+
</p>
|
| 118 |
+
</div>
|
| 119 |
+
))}
|
| 120 |
+
</div>
|
| 121 |
+
</div>
|
| 122 |
+
|
| 123 |
+
{/* Right Column: Custom aggregate and submission form */}
|
| 124 |
+
<div className="lg:col-span-5 space-y-6">
|
| 125 |
+
{/* Aggregate Dashboard breakout */}
|
| 126 |
+
<div className={`p-6 rounded-3xl border ${
|
| 127 |
+
darkMode ? "bg-white/5 border-white/5" : "bg-slate-50 border-slate-100 shadow-sm"
|
| 128 |
+
}`}>
|
| 129 |
+
<h3 className="font-display font-extrabold text-lg mb-4">Google Maps Score</h3>
|
| 130 |
+
<div className="flex items-center space-x-4">
|
| 131 |
+
<span className="text-4xl sm:text-5xl font-black text-brand-orange font-display">4.8</span>
|
| 132 |
+
<div>
|
| 133 |
+
<div className="flex space-x-0.5 text-amber-500">
|
| 134 |
+
<Star className="w-5 h-5 fill-current text-amber-500" />
|
| 135 |
+
<Star className="w-5 h-5 fill-current text-amber-500" />
|
| 136 |
+
<Star className="w-5 h-5 fill-current text-amber-500" />
|
| 137 |
+
<Star className="w-5 h-5 fill-current text-amber-500" />
|
| 138 |
+
<Star className="w-5 h-5 fill-current text-amber-500" />
|
| 139 |
+
</div>
|
| 140 |
+
<span className={`text-xs font-mono block mt-1 ${darkMode ? "text-slate-400" : "text-slate-500"}`}>
|
| 141 |
+
Based on {reviews.length} Genuine Feedback Records
|
| 142 |
+
</span>
|
| 143 |
+
</div>
|
| 144 |
+
</div>
|
| 145 |
+
</div>
|
| 146 |
+
|
| 147 |
+
{/* Leave a review interactive form */}
|
| 148 |
+
<div className={`p-6 sm:p-8 rounded-3xl border relative ${
|
| 149 |
+
darkMode ? "glassmorphism" : "glassmorphism-light shadow-md"
|
| 150 |
+
}`}>
|
| 151 |
+
<h3 className="font-display font-bold text-lg">Leave Your Review</h3>
|
| 152 |
+
<p className={`text-xs mt-1.5 ${darkMode ? "text-slate-400" : "text-slate-500"}`}>
|
| 153 |
+
Submit your experience with Haider Brothers Traders. Your feedback will write directly to our Firestore node.
|
| 154 |
+
</p>
|
| 155 |
+
|
| 156 |
+
{formSuccess && (
|
| 157 |
+
<div className="mt-4 p-3 bg-green-500/10 border border-green-500/35 text-green-500 rounded-xl text-xs flex items-center space-x-2">
|
| 158 |
+
<CheckCircle2 className="w-4 h-4 shrink-0" />
|
| 159 |
+
<span>Review recorded in Firestore database successfully!</span>
|
| 160 |
+
</div>
|
| 161 |
+
)}
|
| 162 |
+
|
| 163 |
+
<form onSubmit={handleSubmit} className="space-y-4 mt-6">
|
| 164 |
+
{/* Star rating selector */}
|
| 165 |
+
<div className="space-y-1">
|
| 166 |
+
<label className={`text-xs font-semibold uppercase font-mono block ${darkMode ? "text-slate-300" : "text-slate-600"}`}>
|
| 167 |
+
Your Rating:
|
| 168 |
+
</label>
|
| 169 |
+
<div className="flex space-x-1">
|
| 170 |
+
{Array.from({ length: 5 }).map((_, idx) => {
|
| 171 |
+
const ratingVal = idx + 1;
|
| 172 |
+
return (
|
| 173 |
+
<button
|
| 174 |
+
key={idx}
|
| 175 |
+
type="button"
|
| 176 |
+
onClick={() => setNewReview({ ...newReview, rating: ratingVal })}
|
| 177 |
+
onMouseEnter={() => setHoverRating(ratingVal)}
|
| 178 |
+
onMouseLeave={() => setHoverRating(null)}
|
| 179 |
+
className="p-1 transition-transform active:scale-95"
|
| 180 |
+
title={`Rate ${ratingVal} Stars`}
|
| 181 |
+
id={`star-btn-${ratingVal}`}
|
| 182 |
+
>
|
| 183 |
+
<Star
|
| 184 |
+
className={`w-6 h-6 ${
|
| 185 |
+
ratingVal <= (hoverRating ?? newReview.rating)
|
| 186 |
+
? "text-amber-500 fill-current"
|
| 187 |
+
: "text-slate-400 dark:text-slate-600"
|
| 188 |
+
}`}
|
| 189 |
+
/>
|
| 190 |
+
</button>
|
| 191 |
+
);
|
| 192 |
+
})}
|
| 193 |
+
</div>
|
| 194 |
+
</div>
|
| 195 |
+
|
| 196 |
+
{/* Name field */}
|
| 197 |
+
<div className="space-y-1">
|
| 198 |
+
<label className={`text-xs font-semibold uppercase font-mono block ${darkMode ? "text-slate-300" : "text-slate-600"}`}>
|
| 199 |
+
Full Name
|
| 200 |
+
</label>
|
| 201 |
+
<input
|
| 202 |
+
type="text"
|
| 203 |
+
required
|
| 204 |
+
placeholder="e.g. Hammad Butt"
|
| 205 |
+
value={newReview.name}
|
| 206 |
+
onChange={(e) => setNewReview({ ...newReview, name: e.target.value })}
|
| 207 |
+
className={`w-full p-3 rounded-xl text-sm border focus:ring-2 focus:ring-brand-orange focus:outline-none transition-all ${
|
| 208 |
+
darkMode
|
| 209 |
+
? "bg-black/40 border-white/10 text-white placeholder-slate-500"
|
| 210 |
+
: "bg-white border-slate-200 text-slate-900 placeholder-slate-400"
|
| 211 |
+
}`}
|
| 212 |
+
id="review-name-input"
|
| 213 |
+
/>
|
| 214 |
+
</div>
|
| 215 |
+
|
| 216 |
+
{/* Feedback text */}
|
| 217 |
+
<div className="space-y-1">
|
| 218 |
+
<label className={`text-xs font-semibold uppercase font-mono block ${darkMode ? "text-slate-300" : "text-slate-600"}`}>
|
| 219 |
+
Your Message
|
| 220 |
+
</label>
|
| 221 |
+
<textarea
|
| 222 |
+
required
|
| 223 |
+
rows={3}
|
| 224 |
+
placeholder="Describe your tire purchase, prices, or alignment support experience..."
|
| 225 |
+
value={newReview.text}
|
| 226 |
+
onChange={(e) => setNewReview({ ...newReview, text: e.target.value })}
|
| 227 |
+
className={`w-full p-3 rounded-xl text-sm border focus:ring-2 focus:ring-brand-orange focus:outline-none transition-all ${
|
| 228 |
+
darkMode
|
| 229 |
+
? "bg-black/40 border-white/10 text-white placeholder-slate-500"
|
| 230 |
+
: "bg-white border-slate-200 text-slate-900 placeholder-slate-400"
|
| 231 |
+
}`}
|
| 232 |
+
id="review-text-input"
|
| 233 |
+
/>
|
| 234 |
+
</div>
|
| 235 |
+
|
| 236 |
+
{/* Submit button */}
|
| 237 |
+
<button
|
| 238 |
+
type="submit"
|
| 239 |
+
className="w-full bg-brand-orange hover:bg-brand-orange-dark text-white font-bold py-3.5 rounded-xl text-sm shadow-md shadow-brand-orange/20 transition-all hover:-translate-y-0.5"
|
| 240 |
+
id="submit-review-btn"
|
| 241 |
+
>
|
| 242 |
+
Post Review Onto Feed
|
| 243 |
+
</button>
|
| 244 |
+
</form>
|
| 245 |
+
</div>
|
| 246 |
+
</div>
|
| 247 |
+
</div>
|
| 248 |
+
</div>
|
| 249 |
+
</section>
|
| 250 |
+
);
|
| 251 |
+
}
|
src/components/TireCard.tsx
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { motion } from "motion/react";
|
| 2 |
+
import { MessageSquare, ShieldCheck, Compass, Eye } from "lucide-react";
|
| 3 |
+
import { TireProduct, BUSINESS_INFO } from "../data";
|
| 4 |
+
|
| 5 |
+
interface TireCardProps {
|
| 6 |
+
tire: TireProduct;
|
| 7 |
+
darkMode: boolean;
|
| 8 |
+
onSelect: (tire: TireProduct) => void;
|
| 9 |
+
key?: string;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export default function TireCard({ tire, darkMode, onSelect }: TireCardProps) {
|
| 13 |
+
// Format WhatsApp Link
|
| 14 |
+
const waPreFilledText = `Hello Haider Brothers, I want price & stock for ${tire.name} (Size: ${tire.size})`;
|
| 15 |
+
const encodedText = encodeURIComponent(waPreFilledText);
|
| 16 |
+
const waLink = `${BUSINESS_INFO.whatsappUrl}?text=${encodedText}`;
|
| 17 |
+
|
| 18 |
+
// Get brand indicator styling
|
| 19 |
+
const isAutogrip = tire.brand.toLowerCase() === "autogrip";
|
| 20 |
+
|
| 21 |
+
return (
|
| 22 |
+
<motion.div
|
| 23 |
+
layout
|
| 24 |
+
initial={{ opacity: 0, scale: 0.95 }}
|
| 25 |
+
animate={{ opacity: 1, scale: 1 }}
|
| 26 |
+
exit={{ opacity: 0, scale: 0.95 }}
|
| 27 |
+
whileHover={{ y: -6, transition: { duration: 0.2 } }}
|
| 28 |
+
transition={{ duration: 0.3 }}
|
| 29 |
+
className={`relative rounded-3xl overflow-hidden flex flex-col justify-between border transition-all duration-300 group ${
|
| 30 |
+
darkMode
|
| 31 |
+
? "bg-[#1c1c24] border-white/5 hover:border-brand-orange/40 shadow-xl shadow-black/20"
|
| 32 |
+
: "bg-white border-slate-100/90 hover:border-brand-orange/40 shadow-md shadow-slate-100"
|
| 33 |
+
}`}
|
| 34 |
+
>
|
| 35 |
+
{/* Glow highlight on hover */}
|
| 36 |
+
<div className="absolute top-0 left-0 right-0 h-[3px] bg-gradient-to-r from-transparent via-brand-orange/0 to-transparent group-hover:via-brand-orange/70 transition-all duration-500"></div>
|
| 37 |
+
|
| 38 |
+
{/* Card Header Media area */}
|
| 39 |
+
<div className="relative aspect-video overflow-hidden bg-slate-900 group">
|
| 40 |
+
{/* Dynamic Image with Lazy Load */}
|
| 41 |
+
<img
|
| 42 |
+
src={tire.image}
|
| 43 |
+
alt={tire.name}
|
| 44 |
+
loading="lazy"
|
| 45 |
+
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500 brightness-95"
|
| 46 |
+
referrerPolicy="no-referrer"
|
| 47 |
+
/>
|
| 48 |
+
|
| 49 |
+
{/* Shadow overlays */}
|
| 50 |
+
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-black/30 pointer-events-none"></div>
|
| 51 |
+
|
| 52 |
+
{/* Top-row Glass badges */}
|
| 53 |
+
<div className="absolute top-3 left-3 right-3 flex justify-between items-center">
|
| 54 |
+
<span className="backdrop-blur-md bg-black/50 text-white border border-white/10 text-[10px] font-mono font-semibold px-2.5 py-1 rounded-full uppercase tracking-wider">
|
| 55 |
+
{tire.badge}
|
| 56 |
+
</span>
|
| 57 |
+
<span className="backdrop-blur-md bg-brand-orange/90 text-white text-[10px] font-mono font-bold px-2.5 py-1 rounded-full uppercase tracking-wider shadow-sm shadow-brand-orange/20">
|
| 58 |
+
{tire.size}
|
| 59 |
+
</span>
|
| 60 |
+
</div>
|
| 61 |
+
|
| 62 |
+
{/* Brand Label block (Bottom-left absolute overlay over media) */}
|
| 63 |
+
<div className="absolute bottom-3 left-3 flex items-center space-x-1.5 backdrop-blur-md bg-black/60 px-2.5 py-1 rounded-lg border border-white/5 text-white">
|
| 64 |
+
<span className="text-xs">⚙️</span>
|
| 65 |
+
<span className={`text-xs font-display font-extrabold tracking-wider uppercase ${isAutogrip ? "text-brand-orange-light" : "text-white"}`}>
|
| 66 |
+
{tire.brand}
|
| 67 |
+
</span>
|
| 68 |
+
</div>
|
| 69 |
+
</div>
|
| 70 |
+
|
| 71 |
+
{/* Card Detail Content */}
|
| 72 |
+
<div className="p-5 flex-1 flex flex-col justify-between space-y-4">
|
| 73 |
+
<div>
|
| 74 |
+
{/* Tire Name */}
|
| 75 |
+
<h4 className={`font-display text-lg font-bold tracking-tight leading-snug group-hover:text-brand-orange transition-colors ${
|
| 76 |
+
darkMode ? "text-white" : "text-slate-900"
|
| 77 |
+
}`}>
|
| 78 |
+
{tire.name}
|
| 79 |
+
</h4>
|
| 80 |
+
|
| 81 |
+
{/* Quick specs / Description */}
|
| 82 |
+
<p className={`text-xs mt-1.5 leading-relaxed line-clamp-2 ${
|
| 83 |
+
darkMode ? "text-slate-400" : "text-slate-500"
|
| 84 |
+
}`}>
|
| 85 |
+
{tire.description}
|
| 86 |
+
</p>
|
| 87 |
+
|
| 88 |
+
{/* Core Feature highlight badge */}
|
| 89 |
+
<div className={`mt-3 flex items-center space-x-1.5 text-xs font-semibold ${
|
| 90 |
+
darkMode ? "text-slate-300" : "text-slate-700"
|
| 91 |
+
}`}>
|
| 92 |
+
<Compass className="w-3.5 h-3.5 text-brand-orange shrink-0" />
|
| 93 |
+
<span className="truncate">{tire.feature}</span>
|
| 94 |
+
</div>
|
| 95 |
+
</div>
|
| 96 |
+
|
| 97 |
+
{/* Price & Action row */}
|
| 98 |
+
<div className="pt-4 border-t border-black/5 dark:border-white/5">
|
| 99 |
+
<div className="flex items-baseline justify-between mb-4">
|
| 100 |
+
<span className={`text-[10px] uppercase font-mono tracking-wider ${darkMode ? "text-slate-400" : "text-slate-500"}`}>
|
| 101 |
+
Dealer Net Price
|
| 102 |
+
</span>
|
| 103 |
+
<span className="text-brand-orange font-display text-xl font-extrabold tracking-tight">
|
| 104 |
+
PKR {tire.price.toLocaleString()}
|
| 105 |
+
</span>
|
| 106 |
+
</div>
|
| 107 |
+
|
| 108 |
+
<div className="grid grid-cols-5 gap-2">
|
| 109 |
+
{/* Quick Specs Quickview Button */}
|
| 110 |
+
<button
|
| 111 |
+
onClick={() => onSelect(tire)}
|
| 112 |
+
className={`col-span-2 flex items-center justify-center p-3 rounded-xl transition-all ${
|
| 113 |
+
darkMode
|
| 114 |
+
? "bg-white/5 hover:bg-white/10 border border-white/5 text-slate-300 hover:text-white"
|
| 115 |
+
: "bg-slate-100 hover:bg-slate-200 border border-slate-200/50 text-slate-700 hover:text-slate-950"
|
| 116 |
+
}`}
|
| 117 |
+
title="View Specifications"
|
| 118 |
+
id={`quickview-${tire.id}`}
|
| 119 |
+
>
|
| 120 |
+
<Eye className="w-4.5 h-4.5" />
|
| 121 |
+
</button>
|
| 122 |
+
|
| 123 |
+
{/* WhatsApp Enquiry Button */}
|
| 124 |
+
<a
|
| 125 |
+
href={waLink}
|
| 126 |
+
target="_blank"
|
| 127 |
+
rel="noreferrer referrer"
|
| 128 |
+
className="col-span-3 flex items-center justify-center space-x-1.5 bg-green-600 hover:bg-green-500 text-white py-3 px-3 rounded-xl text-xs font-bold transition-all hover:shadow-md hover:shadow-green-500/10"
|
| 129 |
+
id={`whatsapp-enquiry-${tire.id}`}
|
| 130 |
+
>
|
| 131 |
+
<MessageSquare className="w-4.5 h-4.5 shrink-0" />
|
| 132 |
+
<span>Quick Enquiry</span>
|
| 133 |
+
</a>
|
| 134 |
+
</div>
|
| 135 |
+
</div>
|
| 136 |
+
</div>
|
| 137 |
+
</motion.div>
|
| 138 |
+
);
|
| 139 |
+
}
|
src/components/VehicleFitmentWidget.tsx
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from "react";
|
| 2 |
+
import { Search, Car, HelpCircle, ArrowRight, MessageSquare, CheckCircle } from "lucide-react";
|
| 3 |
+
import { PAK_CAR_FITMENTS, CarFitment } from "../lib/tireService";
|
| 4 |
+
import { BUSINESS_INFO } from "../data";
|
| 5 |
+
|
| 6 |
+
interface VehicleFitmentWidgetProps {
|
| 7 |
+
darkMode: boolean;
|
| 8 |
+
onFilterSize: (size: string) => void;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
export default function VehicleFitmentWidget({ darkMode, onFilterSize }: VehicleFitmentWidgetProps) {
|
| 12 |
+
const [selectedMake, setSelectedMake] = useState<string>("");
|
| 13 |
+
const [selectedModel, setSelectedModel] = useState<string>("");
|
| 14 |
+
const [selectedVariant, setSelectedVariant] = useState<any>(null);
|
| 15 |
+
const [typedSearch, setTypedSearch] = useState<string>("");
|
| 16 |
+
|
| 17 |
+
// Get models for selected make
|
| 18 |
+
const availableModels = selectedMake
|
| 19 |
+
? PAK_CAR_FITMENTS.filter((f) => f.make.toLowerCase() === selectedMake.toLowerCase())
|
| 20 |
+
: [];
|
| 21 |
+
|
| 22 |
+
// Handle Make changes
|
| 23 |
+
const handleMakeChange = (make: string) => {
|
| 24 |
+
setSelectedMake(make);
|
| 25 |
+
setSelectedModel("");
|
| 26 |
+
setSelectedVariant(null);
|
| 27 |
+
};
|
| 28 |
+
|
| 29 |
+
// Handle Model changes
|
| 30 |
+
const handleModelChange = (modelName: string) => {
|
| 31 |
+
setSelectedModel(modelName);
|
| 32 |
+
const fitment = availableModels.find((m) => m.model.toLowerCase() === modelName.toLowerCase());
|
| 33 |
+
if (fitment && fitment.variants.length > 0) {
|
| 34 |
+
setSelectedVariant(fitment.variants[0]);
|
| 35 |
+
} else {
|
| 36 |
+
setSelectedVariant(null);
|
| 37 |
+
}
|
| 38 |
+
};
|
| 39 |
+
|
| 40 |
+
// Quick auto-search matching
|
| 41 |
+
const filteredSearchFitments = typedSearch.trim()
|
| 42 |
+
? PAK_CAR_FITMENTS.flatMap((fit) =>
|
| 43 |
+
fit.variants.map((v) => ({
|
| 44 |
+
make: fit.make,
|
| 45 |
+
model: fit.model,
|
| 46 |
+
variantName: v.name,
|
| 47 |
+
engine: v.engine,
|
| 48 |
+
size: v.recommendedSize
|
| 49 |
+
}))
|
| 50 |
+
).filter((item) =>
|
| 51 |
+
`${item.make} ${item.model} ${item.variantName}`.toLowerCase().includes(typedSearch.toLowerCase())
|
| 52 |
+
).slice(0, 4)
|
| 53 |
+
: [];
|
| 54 |
+
|
| 55 |
+
const handleSelectQuickMatch = (item: any) => {
|
| 56 |
+
setSelectedMake(item.make);
|
| 57 |
+
setSelectedModel(item.model);
|
| 58 |
+
const fitObj = PAK_CAR_FITMENTS.find(f => f.make === item.make && f.model === item.model);
|
| 59 |
+
const varObj = fitObj?.variants.find(v => v.name === item.variantName);
|
| 60 |
+
setSelectedVariant(varObj || null);
|
| 61 |
+
setTypedSearch("");
|
| 62 |
+
};
|
| 63 |
+
|
| 64 |
+
return (
|
| 65 |
+
<div className={`p-6 sm:p-8 rounded-3xl border transition-all duration-300 hover:scale-[1.005] hover:border-brand-orange/30 ${
|
| 66 |
+
darkMode ? "glassmorphism box-glow hover:shadow-[0_0_25px_rgba(255,85,0,0.25)]" : "glassmorphism-light shadow-xl hover:shadow-2xl"
|
| 67 |
+
}`}>
|
| 68 |
+
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6">
|
| 69 |
+
<div>
|
| 70 |
+
<span className="text-xs font-mono font-bold tracking-widest text-brand-orange uppercase">
|
| 71 |
+
⚡ Fitment Recommendations Guide
|
| 72 |
+
</span>
|
| 73 |
+
<h3 className="text-xl sm:text-2xl font-display font-bold mt-1 tracking-tight">
|
| 74 |
+
Find Recommended Tyre Size by Vehicle model
|
| 75 |
+
</h3>
|
| 76 |
+
<p className={`text-xs mt-1 ${darkMode ? "text-slate-400" : "text-slate-500"}`}>
|
| 77 |
+
Configure your car's profile to extract the certified OEM tire dimensions immediately.
|
| 78 |
+
</p>
|
| 79 |
+
</div>
|
| 80 |
+
|
| 81 |
+
<div className="flex items-center space-x-2 text-xs font-mono bg-brand-orange/5 text-brand-orange px-3 py-1.5 rounded-xl border border-brand-orange/10 self-start md:self-auto">
|
| 82 |
+
<Car className="w-3.5 h-3.5" />
|
| 83 |
+
<span>Pakistan Specs Database</span>
|
| 84 |
+
</div>
|
| 85 |
+
</div>
|
| 86 |
+
|
| 87 |
+
{/* Interactive Tabs */}
|
| 88 |
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
| 89 |
+
{/* Left Side: Selectors */}
|
| 90 |
+
<div className="space-y-4">
|
| 91 |
+
{/* Quick Search Input */}
|
| 92 |
+
<div className="relative">
|
| 93 |
+
<span className={`text-xs font-mono font-semibold block mb-1 px-1 ${darkMode ? "text-slate-300" : "text-slate-600"}`}>
|
| 94 |
+
Search Car Name (e.g. 'Corolla', 'Civic', 'Alto')
|
| 95 |
+
</span>
|
| 96 |
+
<div className="relative">
|
| 97 |
+
<input
|
| 98 |
+
type="text"
|
| 99 |
+
placeholder="Type car name..."
|
| 100 |
+
value={typedSearch}
|
| 101 |
+
onChange={(e) => setTypedSearch(e.target.value)}
|
| 102 |
+
className={`w-full p-3 pl-10 rounded-xl text-xs border focus:ring-1 focus:ring-brand-orange focus:outline-none ${
|
| 103 |
+
darkMode ? "bg-black/40 border-white/10 text-white placeholder-slate-500" : "bg-white border-slate-200 text-slate-800 placeholder-slate-400"
|
| 104 |
+
}`}
|
| 105 |
+
id="car-search-input"
|
| 106 |
+
/>
|
| 107 |
+
<Search className="w-4 h-4 text-slate-400 absolute left-3.5 top-1/2 -translate-y-1/2" />
|
| 108 |
+
</div>
|
| 109 |
+
|
| 110 |
+
{/* Auto-suggest results */}
|
| 111 |
+
{filteredSearchFitments.length > 0 && (
|
| 112 |
+
<div className={`absolute left-0 right-0 mt-1.5 rounded-xl border p-2 z-10 shadow-2xl ${
|
| 113 |
+
darkMode ? "bg-[#1c1c24] border-white/10" : "bg-white border-slate-200"
|
| 114 |
+
}`}>
|
| 115 |
+
{filteredSearchFitments.map((fit, idx) => (
|
| 116 |
+
<button
|
| 117 |
+
key={idx}
|
| 118 |
+
type="button"
|
| 119 |
+
onClick={() => handleSelectQuickMatch(fit)}
|
| 120 |
+
className={`w-full text-left px-3 py-2 rounded-lg text-xs flex justify-between items-center transition-all ${
|
| 121 |
+
darkMode ? "hover:bg-white/5 text-slate-200" : "hover:bg-slate-50 text-slate-800"
|
| 122 |
+
}`}
|
| 123 |
+
>
|
| 124 |
+
<div>
|
| 125 |
+
<span className="font-bold text-brand-orange">{fit.make}</span> {fit.model} <span className="text-slate-400">({fit.variantName})</span>
|
| 126 |
+
</div>
|
| 127 |
+
<span className="font-mono text-[10px] bg-slate-500/10 px-2 py-0.5 rounded text-brand-orange font-bold">
|
| 128 |
+
{fit.size}
|
| 129 |
+
</span>
|
| 130 |
+
</button>
|
| 131 |
+
))}
|
| 132 |
+
</div>
|
| 133 |
+
)}
|
| 134 |
+
</div>
|
| 135 |
+
|
| 136 |
+
<div className="grid grid-cols-2 gap-4">
|
| 137 |
+
{/* Make Selector */}
|
| 138 |
+
<div>
|
| 139 |
+
<label className={`text-xs font-mono font-semibold block mb-1 px-1 ${darkMode ? "text-slate-300" : "text-slate-600"}`}>
|
| 140 |
+
Select Brand (Make)
|
| 141 |
+
</label>
|
| 142 |
+
<select
|
| 143 |
+
value={selectedMake}
|
| 144 |
+
onChange={(e) => handleMakeChange(e.target.value)}
|
| 145 |
+
className={`w-full p-3 rounded-xl text-xs border focus:ring-1 focus:ring-brand-orange focus:outline-none transition-all ${
|
| 146 |
+
darkMode ? "bg-black/40 border-white/10 text-white" : "bg-white border-slate-200 text-slate-800"
|
| 147 |
+
}`}
|
| 148 |
+
id="vehicle-make-select"
|
| 149 |
+
>
|
| 150 |
+
<option value="">-- Choose Make --</option>
|
| 151 |
+
<option value="toyota">Toyota Pakistan</option>
|
| 152 |
+
<option value="honda">Honda Atlas</option>
|
| 153 |
+
<option value="suzuki">Pak Suzuki</option>
|
| 154 |
+
<option value="kia">KIA Lucky Motors</option>
|
| 155 |
+
<option value="hyundai">Hyundai Nishat</option>
|
| 156 |
+
<option value="changan">Changan Pakistan</option>
|
| 157 |
+
<option value="mg">MG Motors</option>
|
| 158 |
+
<option value="haval">Haval Pakistan</option>
|
| 159 |
+
<option value="proton">Proton Pakistan</option>
|
| 160 |
+
<option value="dfsk">DFSK Motors</option>
|
| 161 |
+
</select>
|
| 162 |
+
</div>
|
| 163 |
+
|
| 164 |
+
{/* Model Selector */}
|
| 165 |
+
<div>
|
| 166 |
+
<label className={`text-xs font-mono font-semibold block mb-1 px-1 ${darkMode ? "text-slate-300" : "text-slate-600"}`}>
|
| 167 |
+
Select Model
|
| 168 |
+
</label>
|
| 169 |
+
<select
|
| 170 |
+
value={selectedModel}
|
| 171 |
+
disabled={!selectedMake}
|
| 172 |
+
onChange={(e) => handleModelChange(e.target.value)}
|
| 173 |
+
className={`w-full p-3 rounded-xl text-xs border focus:ring-1 focus:ring-brand-orange focus:outline-none transition-all disabled:opacity-50 ${
|
| 174 |
+
darkMode ? "bg-black/40 border-white/10 text-white" : "bg-white border-slate-200 text-slate-800"
|
| 175 |
+
}`}
|
| 176 |
+
id="vehicle-model-select"
|
| 177 |
+
>
|
| 178 |
+
<option value="">-- Choose Model --</option>
|
| 179 |
+
{Array.from(new Set(availableModels.map((m) => m.model))).map((model) => (
|
| 180 |
+
<option key={model} value={model.toLowerCase()}>
|
| 181 |
+
{model}
|
| 182 |
+
</option>
|
| 183 |
+
))}
|
| 184 |
+
</select>
|
| 185 |
+
</div>
|
| 186 |
+
</div>
|
| 187 |
+
|
| 188 |
+
{/* Variant Selector */}
|
| 189 |
+
{selectedModel && (
|
| 190 |
+
<div>
|
| 191 |
+
<label className={`text-xs font-mono font-semibold block mb-1 px-1 ${darkMode ? "text-slate-300" : "text-slate-600"}`}>
|
| 192 |
+
Select Car Variant / Engine Specifications
|
| 193 |
+
</label>
|
| 194 |
+
<div className="flex flex-wrap gap-2">
|
| 195 |
+
{availableModels
|
| 196 |
+
.find((m) => m.model.toLowerCase() === selectedModel.toLowerCase())
|
| 197 |
+
?.variants.map((v) => (
|
| 198 |
+
<button
|
| 199 |
+
key={v.name}
|
| 200 |
+
onClick={() => setSelectedVariant(v)}
|
| 201 |
+
className={`px-3 py-2 rounded-xl text-xs font-medium border transition-all ${
|
| 202 |
+
selectedVariant?.name === v.name
|
| 203 |
+
? "border-brand-orange text-brand-orange bg-brand-orange/10 font-bold"
|
| 204 |
+
: darkMode
|
| 205 |
+
? "border-white/5 bg-white/5 text-slate-300 hover:border-white/20"
|
| 206 |
+
: "border-slate-200 bg-white text-slate-700 hover:border-slate-300"
|
| 207 |
+
}`}
|
| 208 |
+
id={`variant-btn-${v.name.replace(/\s+/g, '-').toLowerCase()}`}
|
| 209 |
+
>
|
| 210 |
+
{v.name} <span className="text-[10px] opacity-75">({v.engine})</span>
|
| 211 |
+
</button>
|
| 212 |
+
))}
|
| 213 |
+
</div>
|
| 214 |
+
</div>
|
| 215 |
+
)}
|
| 216 |
+
</div>
|
| 217 |
+
|
| 218 |
+
{/* Right Side: Recommendation Results Card container */}
|
| 219 |
+
<div className="flex flex-col justify-between h-full">
|
| 220 |
+
{selectedVariant ? (
|
| 221 |
+
<div className={`p-5 rounded-2xl border flex flex-col justify-between h-full bg-linear-to-br transition-all duration-300 ${
|
| 222 |
+
darkMode ? "bg-white/5 border-white/10 text-white" : "bg-slate-100/70 border-slate-200 text-slate-900"
|
| 223 |
+
}`}>
|
| 224 |
+
<div>
|
| 225 |
+
<div className="flex justify-between items-start mb-4">
|
| 226 |
+
<div>
|
| 227 |
+
<span className="text-[10px] font-mono tracking-widest text-brand-orange uppercase font-bold">
|
| 228 |
+
PROFILING MATCH FOUND
|
| 229 |
+
</span>
|
| 230 |
+
<h4 className="font-display font-black text-lg sm:text-l tracking-tight mt-0.5 uppercase">
|
| 231 |
+
{selectedMake} {selectedModel}
|
| 232 |
+
</h4>
|
| 233 |
+
<p className={`text-xs ${darkMode ? "text-slate-400" : "text-slate-500"}`}>
|
| 234 |
+
Variant: <span className="font-semibold text-brand-orange">{selectedVariant.name}</span> ({selectedVariant.engine})
|
| 235 |
+
</p>
|
| 236 |
+
</div>
|
| 237 |
+
<span className="p-2 rounded-lg bg-green-500/10 text-green-500">
|
| 238 |
+
<CheckCircle className="w-5 h-5" />
|
| 239 |
+
</span>
|
| 240 |
+
</div>
|
| 241 |
+
|
| 242 |
+
<div className={`p-4 rounded-xl text-center border my-3 ${
|
| 243 |
+
darkMode ? "bg-black/30 border-white/5" : "bg-white border-slate-200"
|
| 244 |
+
}`}>
|
| 245 |
+
<span className={`text-[11px] font-mono block ${darkMode ? "text-slate-400" : "text-slate-500"}`}>
|
| 246 |
+
RECOMMENDED TYRE SIZE
|
| 247 |
+
</span>
|
| 248 |
+
<span className="font-display font-black text-2xl sm:text-3xl tracking-wider text-brand-orange block my-1">
|
| 249 |
+
{selectedVariant.recommendedSize}
|
| 250 |
+
</span>
|
| 251 |
+
<span className={`text-[10px] block leading-normal ${darkMode ? "text-slate-500" : "text-slate-400"}`}>
|
| 252 |
+
Spec matches the exact manufacturing profile for road pressure distribution & safety rules of Lahore.
|
| 253 |
+
</span>
|
| 254 |
+
</div>
|
| 255 |
+
</div>
|
| 256 |
+
|
| 257 |
+
{/* Practical CTAs */}
|
| 258 |
+
<div className="flex flex-col sm:flex-row gap-2 mt-4">
|
| 259 |
+
<button
|
| 260 |
+
onClick={() => onFilterSize(selectedVariant.recommendedSize)}
|
| 261 |
+
className="flex-1 bg-gradient-to-r from-brand-orange to-red-600 hover:from-brand-orange hover:to-red-700 text-white font-bold p-2.5 rounded-xl text-xs flex items-center justify-center space-x-1 shadow-md hover:shadow-lg transition-all"
|
| 262 |
+
id="fitment-search-match-btn"
|
| 263 |
+
>
|
| 264 |
+
<span>Filter Catalogue for this Size</span>
|
| 265 |
+
<ArrowRight className="w-3.5 h-3.5" />
|
| 266 |
+
</button>
|
| 267 |
+
|
| 268 |
+
<a
|
| 269 |
+
href={`${BUSINESS_INFO.whatsappUrl}?text=Asalam-o-ALaykum%20Haider%20Brothers%20Traders,%20I'm%20looking%20for%20tyres%20in%20specification%20size%20${selectedVariant.recommendedSize}%20for%20my%20${selectedMake}%20${selectedModel}%20${selectedVariant.name}%20car.%20What%20brands%20are%20available%20now?`}
|
| 270 |
+
target="_blank"
|
| 271 |
+
rel="noreferrer referrer"
|
| 272 |
+
className="bg-green-600 hover:bg-green-700 text-white font-bold px-3 py-2.5 rounded-xl text-xs flex items-center justify-center space-x-1 shadow-md transition-all shrink-0"
|
| 273 |
+
>
|
| 274 |
+
<MessageSquare className="w-4 h-4" />
|
| 275 |
+
<span>WhatsApp Query</span>
|
| 276 |
+
</a>
|
| 277 |
+
</div>
|
| 278 |
+
</div>
|
| 279 |
+
) : (
|
| 280 |
+
<div className={`p-10 rounded-2xl border border-dashed flex flex-col items-center justify-center text-center h-full ${
|
| 281 |
+
darkMode ? "bg-white/5 border-white/10" : "bg-slate-100/50 border-slate-200"
|
| 282 |
+
}`}>
|
| 283 |
+
<span className="text-4xl animate-pulse block mb-3">🚗</span>
|
| 284 |
+
<h4 className={`font-display font-bold text-sm ${darkMode ? "text-slate-300" : "text-slate-700"}`}>
|
| 285 |
+
Select Car to Extract Fitments
|
| 286 |
+
</h4>
|
| 287 |
+
<p className={`text-xs max-w-[280px] mt-2 leading-relaxed ${darkMode ? "text-slate-500" : "text-slate-400"}`}>
|
| 288 |
+
Select brand make/model variables on the left, or query the car name to load the customized profile recommendations instantly.
|
| 289 |
+
</p>
|
| 290 |
+
</div>
|
| 291 |
+
)}
|
| 292 |
+
</div>
|
| 293 |
+
</div>
|
| 294 |
+
</div>
|
| 295 |
+
);
|
| 296 |
+
}
|
src/data.ts
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export interface TireProduct {
|
| 2 |
+
id: string;
|
| 3 |
+
name: string;
|
| 4 |
+
brand: string;
|
| 5 |
+
size: string;
|
| 6 |
+
feature: string;
|
| 7 |
+
price: number;
|
| 8 |
+
badge: string;
|
| 9 |
+
segment: 'hot' | 'new' | 'famous';
|
| 10 |
+
image: string;
|
| 11 |
+
description: string;
|
| 12 |
+
stock?: number;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export interface Review {
|
| 16 |
+
name: string;
|
| 17 |
+
rating: number;
|
| 18 |
+
text: string;
|
| 19 |
+
date: string;
|
| 20 |
+
initials: string;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
export const BUSINESS_INFO = {
|
| 24 |
+
name: "Haider Brothers Traders",
|
| 25 |
+
rating: 4.8,
|
| 26 |
+
reviewsCount: 5,
|
| 27 |
+
phone: "+92 302 4594403",
|
| 28 |
+
phoneRaw: "+923024594403",
|
| 29 |
+
whatsappUrl: "https://wa.me/923024594403",
|
| 30 |
+
address: "36-C-1, Maulana Shaukat Ali Road, Block C-1 Faisal Town, Lahore, Punjab 54700, Pakistan",
|
| 31 |
+
locationCode: "F8G5+H9, Lahore",
|
| 32 |
+
hours: "Open 24 hours (24/7)",
|
| 33 |
+
mapIframeUrl: "https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3402.1385474320295!2d74.30589377519962!3d31.476454149202534!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x3919030037af462f%3A0xc479c225327c71d6!2sHaider%20Brothers%20Traders!5e0!3m2!1sen!2spk!4v1718500000000!5m2!1sen!2spk"
|
| 34 |
+
};
|
| 35 |
+
|
| 36 |
+
export const BRANDS = [
|
| 37 |
+
{ name: "Autogrip", hasDirectAccent: true },
|
| 38 |
+
{ name: "Michelin", hasDirectAccent: false },
|
| 39 |
+
{ name: "Bridgestone", hasDirectAccent: false },
|
| 40 |
+
{ name: "Pirelli", hasDirectAccent: false },
|
| 41 |
+
{ name: "Goodyear", hasDirectAccent: false },
|
| 42 |
+
{ name: "Dunlop", hasDirectAccent: false },
|
| 43 |
+
{ name: "General Tire", hasDirectAccent: false },
|
| 44 |
+
{ name: "Continental", hasDirectAccent: false },
|
| 45 |
+
{ name: "Yokohama", hasDirectAccent: false },
|
| 46 |
+
{ name: "Hankook", hasDirectAccent: false }
|
| 47 |
+
];
|
| 48 |
+
|
| 49 |
+
export const REVIEWS: Review[] = [
|
| 50 |
+
{
|
| 51 |
+
name: "gitsitmarketing",
|
| 52 |
+
rating: 5,
|
| 53 |
+
text: "Excellent service and quality of tires. My experience was good.",
|
| 54 |
+
date: "1 month ago",
|
| 55 |
+
initials: "GM"
|
| 56 |
+
},
|
| 57 |
+
{
|
| 58 |
+
name: "M Bilal",
|
| 59 |
+
rating: 5,
|
| 60 |
+
text: "My experience was great with haider brother traders. They provided me good service and high quality product.",
|
| 61 |
+
date: "2 months ago",
|
| 62 |
+
initials: "MB"
|
| 63 |
+
},
|
| 64 |
+
{
|
| 65 |
+
name: "Muhammad Ramzan",
|
| 66 |
+
rating: 5,
|
| 67 |
+
text: "Great quality and service.",
|
| 68 |
+
date: "3 months ago",
|
| 69 |
+
initials: "MR"
|
| 70 |
+
},
|
| 71 |
+
{
|
| 72 |
+
name: "Zain Ul Abideen",
|
| 73 |
+
rating: 4.5,
|
| 74 |
+
text: "Very reliable place for 24/7 emergencies. Broke down late at night, and they fixed my alignment and tires quickly.",
|
| 75 |
+
date: "4 months ago",
|
| 76 |
+
initials: "ZA"
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
name: "Ahmad Raza",
|
| 80 |
+
rating: 5,
|
| 81 |
+
text: "Genuine products at reasonable wholesale-level rates in Lahore. Highly recommended!",
|
| 82 |
+
date: "5 months ago",
|
| 83 |
+
initials: "AR"
|
| 84 |
+
}
|
| 85 |
+
];
|
| 86 |
+
|
| 87 |
+
export const TIRE_PRODUCTS: TireProduct[] = [
|
| 88 |
+
// --- HOT SELLING (9 products) ---
|
| 89 |
+
{
|
| 90 |
+
id: "hot-1",
|
| 91 |
+
name: "Autogrip F107 Comfort",
|
| 92 |
+
brand: "Autogrip",
|
| 93 |
+
size: "185/65R14",
|
| 94 |
+
feature: "All-season wet stability & 185/65R14 smooth ride",
|
| 95 |
+
price: 12500,
|
| 96 |
+
badge: "Hot Selling",
|
| 97 |
+
segment: "hot",
|
| 98 |
+
image: "/src/assets/images/comfort_tire_1781509452553.jpg",
|
| 99 |
+
description: "Budget-friendly comfort tyre optimized for classic sedan and hatchback daily driving. Low rolling noise, enhanced tread compound, and great drainage to prevent aquaplaning in Lahore's heavy monsoon seasons. Highly compatible with Corolla and older Civic variants."
|
| 100 |
+
},
|
| 101 |
+
{
|
| 102 |
+
id: "hot-2",
|
| 103 |
+
name: "Dunlop SP Touring T1",
|
| 104 |
+
brand: "Dunlop",
|
| 105 |
+
size: "175/65R15",
|
| 106 |
+
feature: "Extra-durable rubber & premium fuel saver - 175/65R15",
|
| 107 |
+
price: 14800,
|
| 108 |
+
badge: "Best Seller",
|
| 109 |
+
segment: "hot",
|
| 110 |
+
image: "/src/assets/images/comfort_tire_1781509452553.jpg",
|
| 111 |
+
description: "Highly stable and optimized touring compound engineered to withstand rough tarmac, heatwaves, and long mileage. Outstanding choice for Toyota Yaris, Honda City, and Suzuki Swift owners in Pakistan who value long-lasting performance and maximum fuel economy."
|
| 112 |
+
},
|
| 113 |
+
{
|
| 114 |
+
id: "hot-3",
|
| 115 |
+
name: "Yokohama BluEarth-Es ES32",
|
| 116 |
+
brand: "Yokohama",
|
| 117 |
+
size: "195/65R15",
|
| 118 |
+
feature: "Premium silica compound & low noise - 195/65R15",
|
| 119 |
+
price: 21500,
|
| 120 |
+
badge: "Premium Hot",
|
| 121 |
+
segment: "hot",
|
| 122 |
+
image: "/src/assets/images/comfort_tire_1781509452553.jpg",
|
| 123 |
+
description: "Premium JDM quiet tire with state-of-the-art nanocomposite silica compound. Delivers superior wet/dry road grip, luxurious shock-absorption over Lahore potholes, and unmatched stability at high speeds for modern Honda Civic, Toyota Corolla, and luxury hybrid vehicles."
|
| 124 |
+
},
|
| 125 |
+
{
|
| 126 |
+
id: "hot-4",
|
| 127 |
+
name: "General Tire Euro Glide",
|
| 128 |
+
brand: "General Tire",
|
| 129 |
+
size: "165/70R14",
|
| 130 |
+
feature: "Locally optimized rugged tread - 165/70R14",
|
| 131 |
+
price: 9800,
|
| 132 |
+
badge: "Budget King",
|
| 133 |
+
segment: "hot",
|
| 134 |
+
image: "/src/assets/images/comfort_tire_1781509452553.jpg",
|
| 135 |
+
description: "Genuine local heavy-duty tire manufactured explicitly for Pakistani hot climates and uneven street grids. Strong side casing provides extreme puncture protection that is loved by Suzuki Cultus, Wagon R, and JDM imported hatchback drivers in Punjab."
|
| 136 |
+
},
|
| 137 |
+
{
|
| 138 |
+
id: "hot-5",
|
| 139 |
+
name: "Bridgestone Ecopia EP150",
|
| 140 |
+
brand: "Bridgestone",
|
| 141 |
+
size: "185/65R15",
|
| 142 |
+
feature: "Ultra eco-friendly, fuel-efficiency design - 185/65R15",
|
| 143 |
+
price: 18200,
|
| 144 |
+
badge: "Hot Selling",
|
| 145 |
+
segment: "hot",
|
| 146 |
+
image: "/src/assets/images/comfort_tire_1781509452553.jpg",
|
| 147 |
+
description: "Advanced low-rolling technology reduces carbon footprint while saving up to 6% in fuel fuel-consumption on daily commutes. Solid wet braking performance makes this Ecopia series a prime product for Toyota Yaris, Honda City, and Suzuki Swift."
|
| 148 |
+
},
|
| 149 |
+
{
|
| 150 |
+
id: "hot-6",
|
| 151 |
+
name: "Mirage MR-162 Utility",
|
| 152 |
+
brand: "General Tire", // Classifying Mirage as premium general import or general tier
|
| 153 |
+
size: "145/80R13",
|
| 154 |
+
feature: "Affordable Alto spec, long durability - 145/80R13",
|
| 155 |
+
price: 8300,
|
| 156 |
+
badge: "Alto Spec",
|
| 157 |
+
segment: "hot",
|
| 158 |
+
image: "/src/assets/images/comfort_tire_1781509452553.jpg",
|
| 159 |
+
description: "Extremely affordable imported tire featuring deep treads and extra-thick sidewalls designed to survive bad terrain. Specifically matches the highly popular Suzuki Alto, Daihatsu Mira, and generic 660cc imported Japanese kei cars in Lahore."
|
| 160 |
+
},
|
| 161 |
+
{
|
| 162 |
+
id: "hot-7",
|
| 163 |
+
name: "Dunlop ECO EC201 Eco-Drive",
|
| 164 |
+
brand: "Dunlop",
|
| 165 |
+
size: "145/80R13",
|
| 166 |
+
feature: "Premium Alto comfort & safety grip - 145/80R13",
|
| 167 |
+
price: 10500,
|
| 168 |
+
badge: "Alto Premium",
|
| 169 |
+
segment: "hot",
|
| 170 |
+
image: "/src/assets/images/comfort_tire_1781509452553.jpg",
|
| 171 |
+
description: "The ultimate premium radial upgrade for the Suzuki Alto and Wagon R community. Features ultra-quiet rolling technology, outstanding grip on dry asphalt, and superior control under emergency braking at tight city crossings."
|
| 172 |
+
},
|
| 173 |
+
{
|
| 174 |
+
id: "hot-8",
|
| 175 |
+
name: "Yokohama Advan dB V552",
|
| 176 |
+
brand: "Yokohama",
|
| 177 |
+
size: "215/55R17",
|
| 178 |
+
feature: "Extreme noise reduction & supreme cushion - 215/55R17",
|
| 179 |
+
price: 38000,
|
| 180 |
+
badge: "Ultra Luxury",
|
| 181 |
+
segment: "hot",
|
| 182 |
+
image: "/src/assets/images/sport_tire_1781509490802.jpg",
|
| 183 |
+
description: "Incredibly quiet premium tyre designed for executive luxury. Uses Yokohama's signature chemical compound to cancel asphalt resonance. Provides a cloud-like ride for Honda Civic Oriel/RS, Toyota Corolla Altis Grande, and imported Honda Vezel models."
|
| 184 |
+
},
|
| 185 |
+
{
|
| 186 |
+
id: "hot-9",
|
| 187 |
+
name: "Michelin Primacy 4 Premium",
|
| 188 |
+
brand: "Michelin",
|
| 189 |
+
size: "215/60R16",
|
| 190 |
+
feature: "World-class quiet touring & wet defense - 215/60R16",
|
| 191 |
+
price: 44500,
|
| 192 |
+
badge: "Elite Safety",
|
| 193 |
+
segment: "hot",
|
| 194 |
+
image: "/src/assets/images/comfort_tire_1781509452553.jpg",
|
| 195 |
+
description: "Industry-standard for safety and comfort. Even after extensive mileage, its patented tire pattern drains water with consistent grip. Excellent match for high-profile luxury sedans like Honda Accord, Toyota Camry, and customized JDM vehicles."
|
| 196 |
+
},
|
| 197 |
+
|
| 198 |
+
// --- NEW BRANDS & ARRIVALS (8 products) ---
|
| 199 |
+
{
|
| 200 |
+
id: "new-1",
|
| 201 |
+
name: "Autogrip SUV A606 Sport",
|
| 202 |
+
brand: "Autogrip",
|
| 203 |
+
size: "235/60R18",
|
| 204 |
+
feature: "Rugged road tracking & heavy SUV compound - 235/60R18",
|
| 205 |
+
price: 22000,
|
| 206 |
+
badge: "New Arrival",
|
| 207 |
+
segment: "new",
|
| 208 |
+
image: "/src/assets/images/suv_tire_1781509472580.jpg",
|
| 209 |
+
description: "Rugged block styling and dual steel belts for midsize crossovers. Delivers long tread life, off-road traction, and stable high-speed tracking on Ring Road Lahore. Excellent fit for KIA Sportage, Hyundai Tucson, and Proton X70."
|
| 210 |
+
},
|
| 211 |
+
{
|
| 212 |
+
id: "new-2",
|
| 213 |
+
name: "Yokohama Geolandar A/T G015",
|
| 214 |
+
brand: "Yokohama",
|
| 215 |
+
size: "265/65R17",
|
| 216 |
+
feature: "Extreme double-shield all-terrain - 265/65R17",
|
| 217 |
+
price: 46000,
|
| 218 |
+
badge: "New Arrival",
|
| 219 |
+
segment: "new",
|
| 220 |
+
image: "/src/assets/images/suv_tire_1781509472580.jpg",
|
| 221 |
+
description: "Designed for full-sized off-road SUVs. Offers severe snow-rated traction blocks and triple-polymer compound to resist extreme cuts/chipping on rough gravel and inter-city routes. Ideal for Toyota Hilux Revo, Toyota Fortuner, and Prado owners."
|
| 222 |
+
},
|
| 223 |
+
{
|
| 224 |
+
id: "new-3",
|
| 225 |
+
name: "Bridgestone Dueler H/P Sport",
|
| 226 |
+
brand: "Bridgestone",
|
| 227 |
+
size: "225/55R18",
|
| 228 |
+
feature: "High-horsepower highway response - 225/55R18",
|
| 229 |
+
price: 39500,
|
| 230 |
+
badge: "Premium SUV",
|
| 231 |
+
segment: "new",
|
| 232 |
+
image: "/src/assets/images/suv_tire_1781509472580.jpg",
|
| 233 |
+
description: "Outstanding summer tire built to extract extreme cornering response and stable wet traction for premium crossover profiles. Perfect OEM spec for KIA Sportage, Hyundai Tucson, and MG HS sport package upgrades."
|
| 234 |
+
},
|
| 235 |
+
{
|
| 236 |
+
id: "new-4",
|
| 237 |
+
name: "Michelin Pilot Sport 5 Precision",
|
| 238 |
+
brand: "Michelin",
|
| 239 |
+
size: "225/45R17",
|
| 240 |
+
feature: "Max sport handling & quick track response - 225/45R17",
|
| 241 |
+
price: 35000,
|
| 242 |
+
badge: "New Arrival",
|
| 243 |
+
segment: "new",
|
| 244 |
+
image: "/src/assets/images/sport_tire_1781509490802.jpg",
|
| 245 |
+
description: "Experience incredible luxury precise control. Features Dynamic Response technology for high cornering precision and immediate dry track response. Perfect premium upgrade for modern luxury sedans and modified sports custom vehicles."
|
| 246 |
+
},
|
| 247 |
+
{
|
| 248 |
+
id: "new-5",
|
| 249 |
+
name: "Continental PremiumContact 7 Pro",
|
| 250 |
+
brand: "Continental",
|
| 251 |
+
size: "195/55R15",
|
| 252 |
+
feature: "Adaptive contact patch & top handling - 195/55R15",
|
| 253 |
+
price: 19500,
|
| 254 |
+
badge: "German Tech",
|
| 255 |
+
segment: "new",
|
| 256 |
+
image: "/src/assets/images/comfort_tire_1781509452553.jpg",
|
| 257 |
+
description: "Adaptive tread design matches itself dynamically to wet or dry conditions for steady, confident braking. Brings elite European safety limits, low vibration, and precise handling to local JDM imports and sport sedans."
|
| 258 |
+
},
|
| 259 |
+
{
|
| 260 |
+
id: "new-6",
|
| 261 |
+
name: "General Tire Grabber AT3",
|
| 262 |
+
brand: "General Tire",
|
| 263 |
+
size: "265/65R17",
|
| 264 |
+
feature: "All-terrain dual action traction - 265/65R17",
|
| 265 |
+
price: 28500,
|
| 266 |
+
badge: "New Arrival",
|
| 267 |
+
segment: "new",
|
| 268 |
+
image: "/src/assets/images/suv_tire_1781509472580.jpg",
|
| 269 |
+
description: "High-traction all-terrain tyre with aggressive self-cleaning blocks. Developed specifically for SUV and pickup truck enthusiasts looking for tough off-road capability combined with stable highway dynamics inside Pakistan."
|
| 270 |
+
},
|
| 271 |
+
{
|
| 272 |
+
id: "new-7",
|
| 273 |
+
name: "Pirelli Cinturato P7 Verde",
|
| 274 |
+
brand: "Pirelli",
|
| 275 |
+
size: "205/60R16",
|
| 276 |
+
feature: "Eco-impact compound & silent highway ride - 205/60R16",
|
| 277 |
+
price: 24500,
|
| 278 |
+
badge: "Italian Craft",
|
| 279 |
+
segment: "new",
|
| 280 |
+
image: "/src/assets/images/comfort_tire_1781509452553.jpg",
|
| 281 |
+
description: "High-end touring tire featuring hybrid polymer materials. Reduced tire weight and rolling resistance lead to immense fuel consumption cuts and lower road vibrations. Universally fits Honda Civic, Toyota Corolla, and imported JDM hatchbacks."
|
| 282 |
+
},
|
| 283 |
+
{
|
| 284 |
+
id: "new-8",
|
| 285 |
+
name: "Autogrip EcoSaver Radial",
|
| 286 |
+
brand: "Autogrip",
|
| 287 |
+
size: "165/65R14",
|
| 288 |
+
feature: "Fuel conscious hatchback radial - 165/65R14",
|
| 289 |
+
price: 11200,
|
| 290 |
+
badge: "New Arrival",
|
| 291 |
+
segment: "new",
|
| 292 |
+
image: "/src/assets/images/comfort_tire_1781509452553.jpg",
|
| 293 |
+
description: "Optimized radial tire designed with a robust eco-compound that evenly distributes vehicle weight. Extremely budget-friendly, delivering a quieter ride and reliable long treadwear for Suzuki Wagon R and imported Japanese JDM hatchbacks."
|
| 294 |
+
},
|
| 295 |
+
|
| 296 |
+
// --- BESTSELLING GLOBAL / FAMOUS (8 products) ---
|
| 297 |
+
{
|
| 298 |
+
id: "fam-1",
|
| 299 |
+
name: "Dunlop Grandtrek AT20 Premium",
|
| 300 |
+
brand: "Dunlop",
|
| 301 |
+
size: "265/60R18",
|
| 302 |
+
feature: "4x4 executive tire & heavy highway duty - 265/60R18",
|
| 303 |
+
price: 48000,
|
| 304 |
+
badge: "Famous",
|
| 305 |
+
segment: "famous",
|
| 306 |
+
image: "/src/assets/images/suv_tire_1781509472580.jpg",
|
| 307 |
+
description: "Wholesale-level standard luxury SUV highway tire. Combines quiet road manners with high structural integrity capable of carrying extreme loads. Highly popular for premium Toyota Fortuner, Land Cruiser Prado, and high-end 4x4 trucks."
|
| 308 |
+
},
|
| 309 |
+
{
|
| 310 |
+
id: "fam-2",
|
| 311 |
+
name: "General Tire Euro Star Radial",
|
| 312 |
+
brand: "General Tire",
|
| 313 |
+
size: "175/70R13",
|
| 314 |
+
feature: "Classic local design, unbeatable cost - 175/70R13",
|
| 315 |
+
price: 8900,
|
| 316 |
+
badge: "Famous Classic",
|
| 317 |
+
segment: "famous",
|
| 318 |
+
image: "/src/assets/images/comfort_tire_1781509452553.jpg",
|
| 319 |
+
description: "The most famous local tire model of Pakistan. Engineered specifically to survive deep potholes and scorching heat. Highly cost-effective and dependable daily workhorse for Suzuki Mehran, Cultus, Wagon R, and small commercial taxis in Lahore."
|
| 320 |
+
},
|
| 321 |
+
{
|
| 322 |
+
id: "fam-3",
|
| 323 |
+
name: "Yokohama Geolandar CV G058",
|
| 324 |
+
brand: "Yokohama",
|
| 325 |
+
size: "215/70R16",
|
| 326 |
+
feature: "Grand crossover luxury silent touring - 215/70R16",
|
| 327 |
+
price: 26000,
|
| 328 |
+
badge: "Famous",
|
| 329 |
+
segment: "famous",
|
| 330 |
+
image: "/src/assets/images/suv_tire_1781509472580.jpg",
|
| 331 |
+
description: "Engineered specifically to transform midsize crossover rides. Provides supreme dry/wet braking safety with Yokohama's proprietary micro-silica compound to drastically reduce interior cabin vibrations and low frequency hums. Fits Toyota RAV4, Kia Sportage base, and Tucson imports."
|
| 332 |
+
},
|
| 333 |
+
{
|
| 334 |
+
id: "fam-4",
|
| 335 |
+
name: "Pirelli P Zero High Performance",
|
| 336 |
+
brand: "Pirelli",
|
| 337 |
+
size: "245/40R19",
|
| 338 |
+
feature: "Precision sports tracking & F1 DNA - 245/40R19",
|
| 339 |
+
price: 58000,
|
| 340 |
+
badge: "Elite Track",
|
| 341 |
+
segment: "famous",
|
| 342 |
+
image: "/src/assets/images/sport_tire_1781509490802.jpg",
|
| 343 |
+
description: "Legendary sports tire built using genuine Formula 1 racetrack heritage materials. Implements severe cornering stiffeners and specialized compound ratios for incredible braking under extreme speed. Ideal for Mercedes C-Class, E-Class, Audi A6, and sports cars."
|
| 344 |
+
},
|
| 345 |
+
{
|
| 346 |
+
id: "fam-5",
|
| 347 |
+
name: "Dunlop Sport Maxx RT2 Premium",
|
| 348 |
+
brand: "Dunlop",
|
| 349 |
+
size: "225/40R18",
|
| 350 |
+
feature: "Motorsport grip & short braking - 225/40R18",
|
| 351 |
+
price: 32000,
|
| 352 |
+
badge: "Famous Sport",
|
| 353 |
+
segment: "famous",
|
| 354 |
+
image: "/src/assets/images/sport_tire_1781509490802.jpg",
|
| 355 |
+
description: "Bridges the gap between street driving and track-level responsiveness. Dual compound notches adapt dynamically to high cornering heat to preserve high traction limits. Excellent choice for custom Civic upgrades, Audi A3, and Mercedes models."
|
| 356 |
+
},
|
| 357 |
+
{
|
| 358 |
+
id: "fam-6",
|
| 359 |
+
name: "Goodyear Eagle F1 Asymmetric 6",
|
| 360 |
+
brand: "Goodyear",
|
| 361 |
+
size: "235/35R19",
|
| 362 |
+
feature: "Asphalt adaptation & high force braking - 235/35R19",
|
| 363 |
+
price: 42000,
|
| 364 |
+
badge: "Famous",
|
| 365 |
+
segment: "famous",
|
| 366 |
+
image: "/src/assets/images/sport_tire_1781509490802.jpg",
|
| 367 |
+
description: "Highly adaptive contact profile reacts instantly to rapid weight shifts. Patented carbon black compounds offer outstanding asphalt feedback and top-level wet braking metrics. Perfectly matches premium sports packages and high-performance sedans."
|
| 368 |
+
},
|
| 369 |
+
{
|
| 370 |
+
id: "fam-7",
|
| 371 |
+
name: "Hankook Ventus S1 evo3 Elite",
|
| 372 |
+
brand: "Hankook",
|
| 373 |
+
size: "225/40R18",
|
| 374 |
+
feature: "OE-grade precise tracking - 225/40R18",
|
| 375 |
+
price: 30000,
|
| 376 |
+
badge: "Famous",
|
| 377 |
+
segment: "famous",
|
| 378 |
+
image: "/src/assets/images/sport_tire_1781509490802.jpg",
|
| 379 |
+
description: "Original Equipment (OE) selected by Audi, BMW, and Mercedes-Benz. Employs advanced aramid reinforcement belts to prevent tread expansion, maximizing speed stability and minimizing road vibration noise. Elite high-speed passenger car tire."
|
| 380 |
+
},
|
| 381 |
+
{
|
| 382 |
+
id: "fam-8",
|
| 383 |
+
name: "Autogrip Winter Grip W609 Radial",
|
| 384 |
+
brand: "Autogrip",
|
| 385 |
+
size: "195/65R15",
|
| 386 |
+
feature: "Snow/mud traction & deep siped wet protection",
|
| 387 |
+
price: 14000,
|
| 388 |
+
badge: "Famous",
|
| 389 |
+
segment: "famous",
|
| 390 |
+
image: "/src/assets/images/winter_tire_1781509513584.jpg",
|
| 391 |
+
description: "Featuring deep self-cleaning channels and microscopic sipes, this model delivers superior road traction during wet rainy days and heavy sludge. Highly recommended for winters and mountainous tourism in Murree, Swat, or Gilgit. Matches Corolla/Civic sizing."
|
| 392 |
+
}
|
| 393 |
+
];
|
src/index.css
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;450;550;600;700&family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
|
| 2 |
+
@import "tailwindcss";
|
| 3 |
+
|
| 4 |
+
@theme {
|
| 5 |
+
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
| 6 |
+
--font-display: "Space Grotesk", sans-serif;
|
| 7 |
+
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, monospace;
|
| 8 |
+
|
| 9 |
+
--color-brand-orange: #ff5500;
|
| 10 |
+
--color-brand-dark: #121214;
|
| 11 |
+
--color-brand-card: #1c1c24;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
/* Custom styles for smooth scroll & glow effects */
|
| 15 |
+
html {
|
| 16 |
+
scroll-behavior: smooth;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
body {
|
| 20 |
+
font-family: var(--font-sans);
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
.text-glow {
|
| 24 |
+
text-shadow: 0 0 10px rgba(255, 85, 0, 0.4);
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.box-glow {
|
| 28 |
+
box-shadow: 0 0 20px rgba(255, 85, 0, 0.15);
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
.glassmorphism {
|
| 32 |
+
background: rgba(28, 28, 36, 0.7);
|
| 33 |
+
backdrop-filter: blur(12px);
|
| 34 |
+
-webkit-backdrop-filter: blur(12px);
|
| 35 |
+
border: 1px solid rgba(255, 255, 255, 0.06);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
.glassmorphism-light {
|
| 39 |
+
background: rgba(255, 255, 255, 0.75);
|
| 40 |
+
backdrop-filter: blur(12px);
|
| 41 |
+
-webkit-backdrop-filter: blur(12px);
|
| 42 |
+
border: 1px solid rgba(0, 0, 0, 0.05);
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
/* Custom styled scrollbar */
|
| 46 |
+
::-webkit-scrollbar {
|
| 47 |
+
width: 8px;
|
| 48 |
+
}
|
| 49 |
+
::-webkit-scrollbar-track {
|
| 50 |
+
background: #121214;
|
| 51 |
+
}
|
| 52 |
+
::-webkit-scrollbar-thumb {
|
| 53 |
+
background: #2a2a35;
|
| 54 |
+
border-radius: 4px;
|
| 55 |
+
}
|
| 56 |
+
::-webkit-scrollbar-thumb:hover {
|
| 57 |
+
background: #ff5500;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
@keyframes marquee {
|
| 61 |
+
0% { transform: translateX(0); }
|
| 62 |
+
100% { transform: translateX(-50%); }
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
.animate-marquee-scroll {
|
| 66 |
+
display: flex;
|
| 67 |
+
width: max-content;
|
| 68 |
+
animation: marquee 40s linear infinite;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.animate-spin-slow {
|
| 72 |
+
animation: spin 20s linear infinite;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
@keyframes spin {
|
| 76 |
+
0% { transform: rotate(0deg); }
|
| 77 |
+
100% { transform: rotate(360deg); }
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
@keyframes wiggle {
|
| 81 |
+
0%, 100% { transform: rotate(0deg); }
|
| 82 |
+
25% { transform: rotate(-8deg); }
|
| 83 |
+
75% { transform: rotate(8deg); }
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.animate-wiggle {
|
| 87 |
+
animation: wiggle 1s ease-in-out infinite;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
.animate-pulse-slow {
|
| 91 |
+
animation: pulse 4s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
| 92 |
+
}
|
src/lib/firebase.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { initializeApp } from "firebase/app";
|
| 2 |
+
import { getAuth } from "firebase/auth";
|
| 3 |
+
import { getFirestore, doc, getDocFromServer } from "firebase/firestore";
|
| 4 |
+
import { getStorage } from "firebase/storage";
|
| 5 |
+
import firebaseConfig from "../../firebase-applet-config.json";
|
| 6 |
+
|
| 7 |
+
/*
|
| 8 |
+
===================================================================
|
| 9 |
+
FIREBASE CONFIGURATION GUIDE (bahaduralimunnabhai@gmail.com)
|
| 10 |
+
===================================================================
|
| 11 |
+
To connect this application to your custom Firebase project:
|
| 12 |
+
|
| 13 |
+
1. Go to Firebase Console (https://console.firebase.google.com/)
|
| 14 |
+
2. Create or select your project.
|
| 15 |
+
3. Under Project Settings -> General, add a Web App.
|
| 16 |
+
4. Copy the config object and paste it below, replacing this configuration:
|
| 17 |
+
|
| 18 |
+
const manualConfig = {
|
| 19 |
+
apiKey: "YOUR_API_KEY",
|
| 20 |
+
authDomain: "YOUR_AUTH_DOMAIN",
|
| 21 |
+
projectId: "YOUR_PROJECT_ID",
|
| 22 |
+
storageBucket: "YOUR_STORAGE_BUCKET",
|
| 23 |
+
messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
|
| 24 |
+
appId: "YOUR_APP_ID"
|
| 25 |
+
};
|
| 26 |
+
===================================================================
|
| 27 |
+
*/
|
| 28 |
+
|
| 29 |
+
// Use provisioned dynamic config from setup by default
|
| 30 |
+
const app = initializeApp(firebaseConfig);
|
| 31 |
+
|
| 32 |
+
// Initialize Firestore, Auth, and Storage services
|
| 33 |
+
export const db = getFirestore(app, firebaseConfig.firestoreDatabaseId);
|
| 34 |
+
export const auth = getAuth(app);
|
| 35 |
+
export const storage = getStorage(app);
|
| 36 |
+
|
| 37 |
+
// Connectivity check as requested by system verification guidelines
|
| 38 |
+
export async function testConnection() {
|
| 39 |
+
try {
|
| 40 |
+
await getDocFromServer(doc(db, "test", "connection"));
|
| 41 |
+
console.log("Firebase Connection verified successfully.");
|
| 42 |
+
} catch (error) {
|
| 43 |
+
if (error instanceof Error && error.message.includes("offline")) {
|
| 44 |
+
console.error("Firebase is offline. Please check your network or configuration.");
|
| 45 |
+
} else {
|
| 46 |
+
console.log("Firebase server connectivity confirmed.", error);
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
// Fallback error handler following guidelines
|
| 52 |
+
export enum OperationType {
|
| 53 |
+
CREATE = "create",
|
| 54 |
+
UPDATE = "update",
|
| 55 |
+
DELETE = "delete",
|
| 56 |
+
LIST = "list",
|
| 57 |
+
GET = "get",
|
| 58 |
+
WRITE = "write",
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
export interface FirestoreErrorInfo {
|
| 62 |
+
error: string;
|
| 63 |
+
operationType: OperationType;
|
| 64 |
+
path: string | null;
|
| 65 |
+
authInfo: {
|
| 66 |
+
userId?: string | null;
|
| 67 |
+
email?: string | null;
|
| 68 |
+
emailVerified?: boolean | null;
|
| 69 |
+
};
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
export function handleFirestoreError(error: unknown, operationType: OperationType, path: string | null) {
|
| 73 |
+
const errInfo: FirestoreErrorInfo = {
|
| 74 |
+
error: error instanceof Error ? error.message : String(error),
|
| 75 |
+
authInfo: {
|
| 76 |
+
userId: auth.currentUser?.uid || null,
|
| 77 |
+
email: auth.currentUser?.email || null,
|
| 78 |
+
emailVerified: auth.currentUser?.emailVerified || null,
|
| 79 |
+
},
|
| 80 |
+
operationType,
|
| 81 |
+
path,
|
| 82 |
+
};
|
| 83 |
+
console.error("Firestore Error Detailed: ", JSON.stringify(errInfo));
|
| 84 |
+
throw new Error(JSON.stringify(errInfo));
|
| 85 |
+
}
|
| 86 |
+
|
src/lib/firebaseService.ts
ADDED
|
@@ -0,0 +1,827 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {
|
| 2 |
+
collection,
|
| 3 |
+
getDocs,
|
| 4 |
+
addDoc,
|
| 5 |
+
setDoc,
|
| 6 |
+
updateDoc,
|
| 7 |
+
deleteDoc,
|
| 8 |
+
doc,
|
| 9 |
+
query,
|
| 10 |
+
where,
|
| 11 |
+
onSnapshot,
|
| 12 |
+
serverTimestamp,
|
| 13 |
+
Timestamp,
|
| 14 |
+
writeBatch
|
| 15 |
+
} from "firebase/firestore";
|
| 16 |
+
import {
|
| 17 |
+
signInWithEmailAndPassword,
|
| 18 |
+
signOut,
|
| 19 |
+
onAuthStateChanged,
|
| 20 |
+
createUserWithEmailAndPassword,
|
| 21 |
+
User
|
| 22 |
+
} from "firebase/auth";
|
| 23 |
+
import {
|
| 24 |
+
ref,
|
| 25 |
+
uploadBytes,
|
| 26 |
+
getDownloadURL,
|
| 27 |
+
deleteObject
|
| 28 |
+
} from "firebase/storage";
|
| 29 |
+
import { db, auth, storage, handleFirestoreError, OperationType } from "./firebase";
|
| 30 |
+
|
| 31 |
+
// Collection Constant
|
| 32 |
+
const COLLECTION_NAME = "products";
|
| 33 |
+
|
| 34 |
+
// Fully compatible type interface satisfying both the user requested structure and frontend components
|
| 35 |
+
export interface ProductDocument {
|
| 36 |
+
id: string;
|
| 37 |
+
name: string;
|
| 38 |
+
brand: string;
|
| 39 |
+
size: string;
|
| 40 |
+
price: number;
|
| 41 |
+
category: "hot-selling" | "new-brands" | "famous";
|
| 42 |
+
segment?: "hot" | "new" | "famous"; // UI fallback field
|
| 43 |
+
badge?: string; // UI fallback field
|
| 44 |
+
stock: number;
|
| 45 |
+
feature: string;
|
| 46 |
+
imageUrl: string;
|
| 47 |
+
image?: string; // UI fallback field
|
| 48 |
+
description?: string; // UI fallback field
|
| 49 |
+
createdAt?: Timestamp | Date | any;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// ---------------------------------------------------------
|
| 53 |
+
// 9. ERROR HANDLING & NETWORK DETECTOR
|
| 54 |
+
// ---------------------------------------------------------
|
| 55 |
+
|
| 56 |
+
/**
|
| 57 |
+
* Checks if the system browser environment has active internet connectivity
|
| 58 |
+
*/
|
| 59 |
+
export function isBrowserOnline(): boolean {
|
| 60 |
+
return typeof navigator !== "undefined" ? navigator.onLine : true;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
/**
|
| 64 |
+
* Registers a callback for changes in native internet connectivity state
|
| 65 |
+
*/
|
| 66 |
+
export function registerNetworkStateListener(callback: (online: boolean) => void): () => void {
|
| 67 |
+
if (typeof window === "undefined") return () => {};
|
| 68 |
+
|
| 69 |
+
const handleOnline = () => callback(true);
|
| 70 |
+
const handleOffline = () => callback(false);
|
| 71 |
+
|
| 72 |
+
window.addEventListener("online", handleOnline);
|
| 73 |
+
window.addEventListener("offline", handleOffline);
|
| 74 |
+
|
| 75 |
+
return () => {
|
| 76 |
+
window.removeEventListener("online", handleOnline);
|
| 77 |
+
window.removeEventListener("offline", handleOffline);
|
| 78 |
+
};
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
// ---------------------------------------------------------
|
| 82 |
+
// 3. CRUD FUNCTIONS (TypeScript / JavaScript Module)
|
| 83 |
+
// ---------------------------------------------------------
|
| 84 |
+
|
| 85 |
+
/**
|
| 86 |
+
* Fills helper values into the model object so that it remains completely compatible
|
| 87 |
+
* across the vintage UI segments & new strict collection layouts.
|
| 88 |
+
*/
|
| 89 |
+
function fillCompatibilityFields(data: any): any {
|
| 90 |
+
const result = { ...data };
|
| 91 |
+
|
| 92 |
+
// Set alternative images
|
| 93 |
+
if (result.imageUrl && !result.image) {
|
| 94 |
+
result.image = result.imageUrl;
|
| 95 |
+
}
|
| 96 |
+
if (result.image && !result.imageUrl) {
|
| 97 |
+
result.imageUrl = result.image;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
// Set category/segment mapping
|
| 101 |
+
if (result.category && !result.segment) {
|
| 102 |
+
if (result.category === "hot-selling") result.segment = "hot";
|
| 103 |
+
else if (result.category === "new-brands") result.segment = "new";
|
| 104 |
+
else if (result.category === "famous") result.segment = "famous";
|
| 105 |
+
}
|
| 106 |
+
if (result.segment && !result.category) {
|
| 107 |
+
if (result.segment === "hot") result.category = "hot-selling";
|
| 108 |
+
else if (result.segment === "new") result.category = "new-brands";
|
| 109 |
+
else if (result.segment === "famous") result.category = "famous";
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
// Supply default badges
|
| 113 |
+
if (!result.badge) {
|
| 114 |
+
if (result.category === "hot-selling") result.badge = "Hot Selling";
|
| 115 |
+
else if (result.category === "new-brands") result.badge = "New Arrival";
|
| 116 |
+
else result.badge = "Famous";
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
// Fallback description
|
| 120 |
+
if (!result.description) {
|
| 121 |
+
result.description = `Premium tire profile configured for high grip and safety under high heat-cycle Pakistani road asphalt conditions.`;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
// Type coercions
|
| 125 |
+
if (result.price !== undefined) result.price = Number(result.price);
|
| 126 |
+
if (result.stock !== undefined) result.stock = Number(result.stock);
|
| 127 |
+
|
| 128 |
+
return result;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
/**
|
| 132 |
+
* Adds a new product document to Firestore 'products' collection
|
| 133 |
+
*/
|
| 134 |
+
export async function addProduct(productData: Omit<ProductDocument, "id">): Promise<string> {
|
| 135 |
+
if (!isBrowserOnline()) {
|
| 136 |
+
throw new Error("Unable to add product. You are currently offline.");
|
| 137 |
+
}
|
| 138 |
+
const payload = fillCompatibilityFields(productData);
|
| 139 |
+
try {
|
| 140 |
+
const docRef = await addDoc(collection(db, COLLECTION_NAME), {
|
| 141 |
+
...payload,
|
| 142 |
+
createdAt: serverTimestamp()
|
| 143 |
+
});
|
| 144 |
+
// Set ID on document itself
|
| 145 |
+
await updateDoc(docRef, { id: docRef.id });
|
| 146 |
+
return docRef.id;
|
| 147 |
+
} catch (error) {
|
| 148 |
+
handleFirestoreError(error, OperationType.WRITE, COLLECTION_NAME);
|
| 149 |
+
throw error;
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
/**
|
| 154 |
+
* Fetches all products with real-time snapshot support optionally, or via one-shot call
|
| 155 |
+
*/
|
| 156 |
+
export async function getAllProducts(isRetry: boolean = false): Promise<ProductDocument[]> {
|
| 157 |
+
try {
|
| 158 |
+
const q = query(collection(db, COLLECTION_NAME));
|
| 159 |
+
const querySnapshot = await getDocs(q);
|
| 160 |
+
const results: ProductDocument[] = [];
|
| 161 |
+
querySnapshot.forEach((doc) => {
|
| 162 |
+
const data = doc.data();
|
| 163 |
+
results.push(fillCompatibilityFields({ ...data, id: doc.id }) as ProductDocument);
|
| 164 |
+
});
|
| 165 |
+
|
| 166 |
+
// Check and trigger auto-seeding if collection returns empty
|
| 167 |
+
if (results.length === 0 && !isRetry) {
|
| 168 |
+
console.log("Empty 'products' collection found. Seeding with 12 defaults.");
|
| 169 |
+
await seedDefaultProducts();
|
| 170 |
+
return await getAllProducts(true);
|
| 171 |
+
}
|
| 172 |
+
return results;
|
| 173 |
+
} catch (error) {
|
| 174 |
+
handleFirestoreError(error, OperationType.GET, COLLECTION_NAME);
|
| 175 |
+
throw error;
|
| 176 |
+
}
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
/**
|
| 180 |
+
* Updates an existing product document
|
| 181 |
+
*/
|
| 182 |
+
export async function updateProduct(productId: string, updatedData: Partial<ProductDocument>): Promise<void> {
|
| 183 |
+
if (!isBrowserOnline()) {
|
| 184 |
+
throw new Error("Unable to update product. You are currently offline.");
|
| 185 |
+
}
|
| 186 |
+
const payload = fillCompatibilityFields(updatedData);
|
| 187 |
+
try {
|
| 188 |
+
const docRef = doc(db, COLLECTION_NAME, productId);
|
| 189 |
+
await updateDoc(docRef, payload);
|
| 190 |
+
} catch (error) {
|
| 191 |
+
handleFirestoreError(error, OperationType.WRITE, `${COLLECTION_NAME}/${productId}`);
|
| 192 |
+
throw error;
|
| 193 |
+
}
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
/**
|
| 197 |
+
* Deletes a product from the database
|
| 198 |
+
*/
|
| 199 |
+
export async function deleteProduct(productId: string): Promise<void> {
|
| 200 |
+
if (!isBrowserOnline()) {
|
| 201 |
+
throw new Error("Unable to delete product. You are currently offline.");
|
| 202 |
+
}
|
| 203 |
+
try {
|
| 204 |
+
await deleteDoc(doc(db, COLLECTION_NAME, productId));
|
| 205 |
+
} catch (error) {
|
| 206 |
+
handleFirestoreError(error, OperationType.DELETE, `${COLLECTION_NAME}/${productId}`);
|
| 207 |
+
throw error;
|
| 208 |
+
}
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
/**
|
| 212 |
+
* Increases or decreases all prices in the inventory database by a given percentage.
|
| 213 |
+
* Example: bulkUpdatePrices(10) increases all rates by 10%, while bulkUpdatePrices(-5) cuts them by 5%.
|
| 214 |
+
*/
|
| 215 |
+
export async function bulkUpdatePrices(percentage: number): Promise<void> {
|
| 216 |
+
if (!isBrowserOnline()) {
|
| 217 |
+
throw new Error("No network connectivity. Bulk operations are disabled.");
|
| 218 |
+
}
|
| 219 |
+
try {
|
| 220 |
+
const q = query(collection(db, COLLECTION_NAME));
|
| 221 |
+
const querySnapshot = await getDocs(q);
|
| 222 |
+
|
| 223 |
+
const batch = writeBatch(db);
|
| 224 |
+
let count = 0;
|
| 225 |
+
|
| 226 |
+
querySnapshot.forEach((document) => {
|
| 227 |
+
const data = document.data() as ProductDocument;
|
| 228 |
+
const originalPrice = Number(data.price) || 0;
|
| 229 |
+
const multiplier = 1 + (percentage / 100);
|
| 230 |
+
const newPrice = Math.round(originalPrice * multiplier);
|
| 231 |
+
|
| 232 |
+
batch.update(doc(db, COLLECTION_NAME, document.id), { price: newPrice });
|
| 233 |
+
count++;
|
| 234 |
+
});
|
| 235 |
+
|
| 236 |
+
if (count > 0) {
|
| 237 |
+
await batch.commit();
|
| 238 |
+
console.log(`Successfully completed bulk price update on ${count} products.`);
|
| 239 |
+
}
|
| 240 |
+
} catch (error) {
|
| 241 |
+
handleFirestoreError(error, OperationType.WRITE, `${COLLECTION_NAME}/bulkUpdate`);
|
| 242 |
+
throw error;
|
| 243 |
+
}
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
/**
|
| 247 |
+
* Returns products with stock levels strictly below the threshold value
|
| 248 |
+
*/
|
| 249 |
+
export async function getLowStockProducts(threshold: number = 5): Promise<ProductDocument[]> {
|
| 250 |
+
try {
|
| 251 |
+
const q = query(collection(db, COLLECTION_NAME), where("stock", "<", threshold));
|
| 252 |
+
const querySnapshot = await getDocs(q);
|
| 253 |
+
const results: ProductDocument[] = [];
|
| 254 |
+
querySnapshot.forEach((doc) => {
|
| 255 |
+
results.push(fillCompatibilityFields({ ...doc.data(), id: doc.id }));
|
| 256 |
+
});
|
| 257 |
+
return results;
|
| 258 |
+
} catch (error) {
|
| 259 |
+
handleFirestoreError(error, OperationType.GET, COLLECTION_NAME);
|
| 260 |
+
throw error;
|
| 261 |
+
}
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
/**
|
| 265 |
+
* Searches the collection for products with matching names or manufacturer brands
|
| 266 |
+
*/
|
| 267 |
+
export async function searchProducts(searchQuery: string): Promise<ProductDocument[]> {
|
| 268 |
+
// Firestore does not natively support multi-field partial matching easily in client SDK,
|
| 269 |
+
// so we fetch all and apply high-performance filter locally to ensure maximum reliability and speed.
|
| 270 |
+
try {
|
| 271 |
+
const all = await getAllProducts();
|
| 272 |
+
const queryLower = searchQuery.toLowerCase().trim();
|
| 273 |
+
if (!queryLower) return all;
|
| 274 |
+
|
| 275 |
+
return all.filter(p =>
|
| 276 |
+
p.name.toLowerCase().includes(queryLower) ||
|
| 277 |
+
p.brand.toLowerCase().includes(queryLower) ||
|
| 278 |
+
p.size.toLowerCase().includes(queryLower)
|
| 279 |
+
);
|
| 280 |
+
} catch (error) {
|
| 281 |
+
throw error;
|
| 282 |
+
}
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
/**
|
| 286 |
+
* Filter products specifically by database category classification
|
| 287 |
+
*/
|
| 288 |
+
export async function filterByCategory(category: "hot-selling" | "new-brands" | "famous"): Promise<ProductDocument[]> {
|
| 289 |
+
try {
|
| 290 |
+
const q = query(collection(db, COLLECTION_NAME), where("category", "==", category));
|
| 291 |
+
const querySnapshot = await getDocs(q);
|
| 292 |
+
const results: ProductDocument[] = [];
|
| 293 |
+
querySnapshot.forEach((doc) => {
|
| 294 |
+
results.push(fillCompatibilityFields({ ...doc.data(), id: doc.id }));
|
| 295 |
+
});
|
| 296 |
+
return results;
|
| 297 |
+
} catch (error) {
|
| 298 |
+
handleFirestoreError(error, OperationType.GET, COLLECTION_NAME);
|
| 299 |
+
throw error;
|
| 300 |
+
}
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
// ---------------------------------------------------------
|
| 304 |
+
// 4. AUTHENTICATION FUNCTIONS
|
| 305 |
+
// ---------------------------------------------------------
|
| 306 |
+
|
| 307 |
+
/**
|
| 308 |
+
* Authenticates an administrator using standard email and password
|
| 309 |
+
*/
|
| 310 |
+
export async function loginAdmin(email: string, password: string): Promise<User> {
|
| 311 |
+
try {
|
| 312 |
+
const userCredential = await signInWithEmailAndPassword(auth, email, password);
|
| 313 |
+
return userCredential.user;
|
| 314 |
+
} catch (error: any) {
|
| 315 |
+
console.error("Login attempt failed:", error);
|
| 316 |
+
throw new Error(error.message || "Invalid authentication credentials.");
|
| 317 |
+
}
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
/**
|
| 321 |
+
* Standard security signout for administrators
|
| 322 |
+
*/
|
| 323 |
+
export async function logoutAdmin(): Promise<void> {
|
| 324 |
+
try {
|
| 325 |
+
await signOut(auth);
|
| 326 |
+
} catch (error: any) {
|
| 327 |
+
throw new Error(error.message || "Signout failed.");
|
| 328 |
+
}
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
/**
|
| 332 |
+
* Attaches a state change listener tracking authentications
|
| 333 |
+
*/
|
| 334 |
+
export function checkAuthState(callback: (user: User | null) => void): () => void {
|
| 335 |
+
return onAuthStateChanged(auth, (user) => {
|
| 336 |
+
callback(user);
|
| 337 |
+
});
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
/**
|
| 341 |
+
* Utility to register fresh admin personnel accounts
|
| 342 |
+
*/
|
| 343 |
+
export async function createAdminUser(email: string, password: string): Promise<User> {
|
| 344 |
+
try {
|
| 345 |
+
const userCredential = await createUserWithEmailAndPassword(auth, email, password);
|
| 346 |
+
return userCredential.user;
|
| 347 |
+
} catch (error: any) {
|
| 348 |
+
throw new Error(error.message || "Failed to create administrator user.");
|
| 349 |
+
}
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
// ---------------------------------------------------------
|
| 353 |
+
// 5. STORAGE FUNCTIONS
|
| 354 |
+
// ---------------------------------------------------------
|
| 355 |
+
|
| 356 |
+
/**
|
| 357 |
+
* Uploads a tire image binary file to the Firebase Storage bucket
|
| 358 |
+
*/
|
| 359 |
+
export async function uploadProductImage(file: File, productName: string): Promise<string> {
|
| 360 |
+
if (!isBrowserOnline()) {
|
| 361 |
+
throw new Error("Unable to upload image. Network is currently offline.");
|
| 362 |
+
}
|
| 363 |
+
try {
|
| 364 |
+
const cleanName = productName.replace(/[^a-zA-Z0-9]/g, "_").toLowerCase();
|
| 365 |
+
const timestamp = Date.now();
|
| 366 |
+
const storageRef = ref(storage, `product-images/${cleanName}_${timestamp}`);
|
| 367 |
+
|
| 368 |
+
const snapshot = await uploadBytes(storageRef, file);
|
| 369 |
+
const downloadUrl = await getDownloadURL(snapshot.ref);
|
| 370 |
+
return downloadUrl;
|
| 371 |
+
} catch (error: any) {
|
| 372 |
+
console.error("Storage image upload failure:", error);
|
| 373 |
+
throw new Error(error.message || "Failed to store product asset image.");
|
| 374 |
+
}
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
/**
|
| 378 |
+
* Deletes standard file objects from the Firebase storage bucket using their download addresses
|
| 379 |
+
*/
|
| 380 |
+
export async function deleteProductImage(imageUrl: string): Promise<void> {
|
| 381 |
+
if (!isBrowserOnline()) {
|
| 382 |
+
throw new Error("Unable to deleted image. Offline mode.");
|
| 383 |
+
}
|
| 384 |
+
try {
|
| 385 |
+
// Extract file reference path from firebase download URI
|
| 386 |
+
const decodedUrl = decodeURIComponent(imageUrl);
|
| 387 |
+
const relativePathStart = decodedUrl.indexOf("/o/") + 3;
|
| 388 |
+
const relativePathEnd = decodedUrl.indexOf("?alt=");
|
| 389 |
+
|
| 390 |
+
if (relativePathStart > 2 && relativePathEnd > relativePathStart) {
|
| 391 |
+
const filePath = decodedUrl.substring(relativePathStart, relativePathEnd);
|
| 392 |
+
const fileRef = ref(storage, filePath);
|
| 393 |
+
await deleteObject(fileRef);
|
| 394 |
+
}
|
| 395 |
+
} catch (error: any) {
|
| 396 |
+
console.warn("Storage item cleanup ignored (possibly using an external image link):", error.message);
|
| 397 |
+
}
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
// ---------------------------------------------------------
|
| 401 |
+
// 7. SEED DATA FUNCTION
|
| 402 |
+
// ---------------------------------------------------------
|
| 403 |
+
|
| 404 |
+
// Exhaustive real Pakistan market default dataset encompassing exactly 12 products
|
| 405 |
+
export const DEFAULT_TIR_PRODUCTS_SEED: Omit<ProductDocument, "id">[] = [
|
| 406 |
+
// --- HOT SELLING ---
|
| 407 |
+
{
|
| 408 |
+
name: "Autogrip F107 Comfort Grip",
|
| 409 |
+
brand: "Autogrip",
|
| 410 |
+
size: "185/65R14",
|
| 411 |
+
price: 12500,
|
| 412 |
+
category: "hot-selling",
|
| 413 |
+
stock: 15,
|
| 414 |
+
feature: "All-season wet grip & stability - 185/65R14",
|
| 415 |
+
imageUrl: "/src/assets/images/comfort_tire_1781509452553.jpg",
|
| 416 |
+
description: "Designed for premium everyday passenger cars. Delivers superior compound performance in all seasons with low road noise levels. Size: 185/65R14."
|
| 417 |
+
},
|
| 418 |
+
{
|
| 419 |
+
name: "Autogrip SUV A606 Rugged",
|
| 420 |
+
brand: "Autogrip",
|
| 421 |
+
size: "235/60R18",
|
| 422 |
+
price: 22000,
|
| 423 |
+
category: "hot-selling",
|
| 424 |
+
stock: 4, // low stock flag
|
| 425 |
+
feature: "Off-road rugged tread design - 235/60R18",
|
| 426 |
+
imageUrl: "/src/assets/images/suv_tire_1781509472580.jpg",
|
| 427 |
+
description: "Aggressive tread blocks and heavy-duty compound for SUVs. Provides high durability on rugged tracks, gravel, and highway driving alike. Size: 235/60R18."
|
| 428 |
+
},
|
| 429 |
+
{
|
| 430 |
+
name: "General Tire Grabber AT3",
|
| 431 |
+
brand: "General Tire",
|
| 432 |
+
size: "265/65R17",
|
| 433 |
+
price: 28500,
|
| 434 |
+
category: "hot-selling",
|
| 435 |
+
stock: 8,
|
| 436 |
+
feature: "All-terrain robust capability - 265/65R17",
|
| 437 |
+
imageUrl: "/src/assets/images/suv_tire_1781509472580.jpg",
|
| 438 |
+
description: "Developed to meet the needs of SUV, pick-up truck and off-road vehicle drivers who want high command and high-traction performance. Size: 265/65R17."
|
| 439 |
+
},
|
| 440 |
+
{
|
| 441 |
+
name: "Bridgestone Turanza T005",
|
| 442 |
+
brand: "Bridgestone",
|
| 443 |
+
size: "205/55R16",
|
| 444 |
+
price: 21000,
|
| 445 |
+
category: "hot-selling",
|
| 446 |
+
stock: 12,
|
| 447 |
+
feature: "Premium comfort & low noise rating - 205/55R16",
|
| 448 |
+
imageUrl: "/src/assets/images/sport_tire_1781509490802.jpg",
|
| 449 |
+
description: "Flagship luxury touring tire providing superior control in wet and dry conditions, with fuel efficiency optimization. Size: 205/55R16."
|
| 450 |
+
},
|
| 451 |
+
|
| 452 |
+
// --- NEW BRANDS / NEW ARRIVALS ---
|
| 453 |
+
{
|
| 454 |
+
name: "Michelin Pilot Sport 5",
|
| 455 |
+
brand: "Michelin",
|
| 456 |
+
size: "225/45R17",
|
| 457 |
+
price: 35000,
|
| 458 |
+
category: "new-brands",
|
| 459 |
+
stock: 3, // low stock flag
|
| 460 |
+
feature: "Ultra-high performance & control - 225/45R17",
|
| 461 |
+
imageUrl: "/src/assets/images/sport_tire_1781509490802.jpg",
|
| 462 |
+
description: "Live each driving moment to the fullest. Dynamic response technology ensures high precision handling and short braking distances. Size: 225/45R17."
|
| 463 |
+
},
|
| 464 |
+
{
|
| 465 |
+
name: "Pirelli Cinturato P7 Eco",
|
| 466 |
+
brand: "Pirelli",
|
| 467 |
+
size: "205/60R16",
|
| 468 |
+
price: 24500,
|
| 469 |
+
category: "new-brands",
|
| 470 |
+
stock: 7,
|
| 471 |
+
feature: "Eco-friendly, low rolling resistance - 205/60R16",
|
| 472 |
+
imageUrl: "/src/assets/images/comfort_tire_1781509452553.jpg",
|
| 473 |
+
description: "Green performance luxury tire developed for modern cars. High safety levels, low fuel consumption, and eco-friendly raw materials. Size: 205/60R16."
|
| 474 |
+
},
|
| 475 |
+
{
|
| 476 |
+
name: "Yokohama Geolandar CV G058",
|
| 477 |
+
brand: "Yokohama",
|
| 478 |
+
size: "215/70R16",
|
| 479 |
+
price: 26000,
|
| 480 |
+
category: "new-brands",
|
| 481 |
+
stock: 9,
|
| 482 |
+
feature: "Crossover/SUV quiet touring - 215/70R16",
|
| 483 |
+
imageUrl: "/src/assets/images/suv_tire_1781509472580.jpg",
|
| 484 |
+
description: "Formidable wet grip with advanced CV silica compound. Designed to deliver confident handling and long wear life for modern crossovers. Size: 215/70R16."
|
| 485 |
+
},
|
| 486 |
+
{
|
| 487 |
+
name: "Continental PremiumContact 7",
|
| 488 |
+
brand: "Continental",
|
| 489 |
+
size: "195/55R15",
|
| 490 |
+
price: 19500,
|
| 491 |
+
category: "new-brands",
|
| 492 |
+
stock: 14,
|
| 493 |
+
feature: "Outstanding safety & wet braking - 195/55R15",
|
| 494 |
+
imageUrl: "/src/assets/images/comfort_tire_1781509452553.jpg",
|
| 495 |
+
description: "Experience absolute driving comfort and peace of mind. Outstanding safety on wet roads and superior wet braking efficiency. Size: 195/55R15."
|
| 496 |
+
},
|
| 497 |
+
|
| 498 |
+
// --- BESTSELLING GLOBAL / FAMOUS Products ---
|
| 499 |
+
{
|
| 500 |
+
name: "Dunlop Sport Maxx RT2",
|
| 501 |
+
brand: "Dunlop",
|
| 502 |
+
size: "225/40R18",
|
| 503 |
+
price: 32000,
|
| 504 |
+
category: "famous",
|
| 505 |
+
stock: 2, // low stock flag
|
| 506 |
+
feature: "Ultra-high performance sport traction - 225/40R18",
|
| 507 |
+
imageUrl: "/src/assets/images/sport_tire_1781509490802.jpg",
|
| 508 |
+
description: "Champion grip and braking performance. Motorsport-derived compound offers supreme steering precision and curve cornering. Size: 225/40R18."
|
| 509 |
+
},
|
| 510 |
+
{
|
| 511 |
+
name: "Goodyear Eagle F1 Asymmetric 6",
|
| 512 |
+
brand: "Goodyear",
|
| 513 |
+
size: "235/35R19",
|
| 514 |
+
price: 42000,
|
| 515 |
+
category: "famous",
|
| 516 |
+
stock: 11,
|
| 517 |
+
feature: "Max grip, sports performance - 235/35R19",
|
| 518 |
+
imageUrl: "/src/assets/images/sport_tire_1781509490802.jpg",
|
| 519 |
+
description: "Ready for anything. Adaptable contact patch ensures extreme asphalt feedback. Excellent performance in both wet and dry tracks. Size: 235/35R19."
|
| 520 |
+
},
|
| 521 |
+
{
|
| 522 |
+
name: "Hankook Ventus S1 evo3 Elite",
|
| 523 |
+
brand: "Hankook",
|
| 524 |
+
size: "225/40R18",
|
| 525 |
+
price: 30000,
|
| 526 |
+
category: "famous",
|
| 527 |
+
stock: 6,
|
| 528 |
+
feature: "Luxury sports OE high response rating - 225/40R18",
|
| 529 |
+
imageUrl: "/src/assets/images/sport_tire_1781509490802.jpg",
|
| 530 |
+
description: "Original Equipment (OE) for BMW, Audi, and Mercedes. Highlights high response speed, high durability, and lower rolling noise. Size: 225/40R18."
|
| 531 |
+
},
|
| 532 |
+
{
|
| 533 |
+
name: "Autogrip Winter Grip W609 Extreme",
|
| 534 |
+
brand: "Autogrip",
|
| 535 |
+
size: "195/65R15",
|
| 536 |
+
price: 14000,
|
| 537 |
+
category: "famous",
|
| 538 |
+
stock: 5,
|
| 539 |
+
feature: "Extreme snow & water grip traction - 195/65R15",
|
| 540 |
+
imageUrl: "/src/assets/images/winter_tire_1781509513584.jpg",
|
| 541 |
+
description: "Heavy-siped tread design that grips perfectly during heavy rain, wet mud, and cold winter situations. Ensures reliable road security. Size: 195/65R15."
|
| 542 |
+
}
|
| 543 |
+
];
|
| 544 |
+
|
| 545 |
+
/**
|
| 546 |
+
* Checks if the products collection is empty and fills it with 12 premium tyres if needed
|
| 547 |
+
*/
|
| 548 |
+
export async function seedDefaultProducts(): Promise<void> {
|
| 549 |
+
try {
|
| 550 |
+
const q = query(collection(db, COLLECTION_NAME));
|
| 551 |
+
const querySnapshot = await getDocs(q);
|
| 552 |
+
|
| 553 |
+
if (!querySnapshot.empty) {
|
| 554 |
+
console.log("Database 'products' collection is already populated. Seeding aborted.");
|
| 555 |
+
return;
|
| 556 |
+
}
|
| 557 |
+
|
| 558 |
+
console.log("Initiating premium 12 product configurations cloud seed operation.");
|
| 559 |
+
|
| 560 |
+
// Batch write to optimize bandwidth
|
| 561 |
+
const batch = writeBatch(db);
|
| 562 |
+
|
| 563 |
+
for (let i = 0; i < DEFAULT_TIR_PRODUCTS_SEED.length; i++) {
|
| 564 |
+
const item = DEFAULT_TIR_PRODUCTS_SEED[i];
|
| 565 |
+
const cleanBrand = item.brand.toLowerCase().replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-");
|
| 566 |
+
const customId = `seed-${cleanBrand}-${i + 1}`;
|
| 567 |
+
const payload = fillCompatibilityFields({
|
| 568 |
+
...item,
|
| 569 |
+
id: customId,
|
| 570 |
+
createdAt: new Date()
|
| 571 |
+
});
|
| 572 |
+
|
| 573 |
+
const docRef = doc(db, COLLECTION_NAME, customId);
|
| 574 |
+
batch.set(docRef, payload);
|
| 575 |
+
}
|
| 576 |
+
|
| 577 |
+
await batch.commit();
|
| 578 |
+
console.log("Database seeded successfully with exactly 12 default tires!");
|
| 579 |
+
} catch (error) {
|
| 580 |
+
console.error("Failure while running cloud seeding:", error);
|
| 581 |
+
}
|
| 582 |
+
}
|
| 583 |
+
|
| 584 |
+
// ---------------------------------------------------------
|
| 585 |
+
// 8. REAL-TIME LISTENERS
|
| 586 |
+
// ---------------------------------------------------------
|
| 587 |
+
|
| 588 |
+
/**
|
| 589 |
+
* Sets up a live listener on the main collections. Realizes reactive UI state
|
| 590 |
+
* updates on item modifications.
|
| 591 |
+
*/
|
| 592 |
+
export function onProductsSnapshot(callback: (products: ProductDocument[]) => void): () => void {
|
| 593 |
+
const q = query(collection(db, COLLECTION_NAME));
|
| 594 |
+
|
| 595 |
+
return onSnapshot(q, (snapshot) => {
|
| 596 |
+
const results: ProductDocument[] = [];
|
| 597 |
+
snapshot.forEach((doc) => {
|
| 598 |
+
const data = doc.data();
|
| 599 |
+
results.push(fillCompatibilityFields({ ...data, id: doc.id }) as ProductDocument);
|
| 600 |
+
});
|
| 601 |
+
callback(results);
|
| 602 |
+
}, (error) => {
|
| 603 |
+
handleFirestoreError(error, OperationType.LIST, COLLECTION_NAME);
|
| 604 |
+
});
|
| 605 |
+
}
|
| 606 |
+
|
| 607 |
+
// ---------------------------------------------------------
|
| 608 |
+
// 10. EXPORT / IMPORT FUNCTIONS
|
| 609 |
+
// ---------------------------------------------------------
|
| 610 |
+
|
| 611 |
+
/**
|
| 612 |
+
* Downloads the complete active database catalog as a structured JSON file instantly from the browser
|
| 613 |
+
*/
|
| 614 |
+
export async function exportToJSON(): Promise<void> {
|
| 615 |
+
try {
|
| 616 |
+
const allProducts = await getAllProducts();
|
| 617 |
+
const dataString = JSON.stringify(allProducts, null, 2);
|
| 618 |
+
|
| 619 |
+
// Trigger localized file download in client runtime
|
| 620 |
+
const blob = new Blob([dataString], { type: "application/json" });
|
| 621 |
+
const fileAnchor = document.createElement("a");
|
| 622 |
+
fileAnchor.href = URL.createObjectURL(blob);
|
| 623 |
+
fileAnchor.download = `haider_brothers_products_backup_${Date.now()}.json`;
|
| 624 |
+
document.body.appendChild(fileAnchor);
|
| 625 |
+
fileAnchor.click();
|
| 626 |
+
document.body.removeChild(fileAnchor);
|
| 627 |
+
} catch (error) {
|
| 628 |
+
console.error("Export file creation error: ", error);
|
| 629 |
+
alert("Export operation encountered failures. Check network status.");
|
| 630 |
+
}
|
| 631 |
+
}
|
| 632 |
+
|
| 633 |
+
/**
|
| 634 |
+
* Uploads a user supplied JSON backup file, checks for duplicates, and uploads novel configurations
|
| 635 |
+
*/
|
| 636 |
+
export async function importFromJSON(jsonFile: File): Promise<{ imported: number; duplicates: number }> {
|
| 637 |
+
if (!isBrowserOnline()) {
|
| 638 |
+
throw new Error("Unable to import product index while offline.");
|
| 639 |
+
}
|
| 640 |
+
return new Promise((resolve, reject) => {
|
| 641 |
+
const reader = new FileReader();
|
| 642 |
+
reader.onload = async (event) => {
|
| 643 |
+
try {
|
| 644 |
+
const fileContent = event.target?.result;
|
| 645 |
+
if (typeof fileContent !== "string") {
|
| 646 |
+
throw new Error("Invalid file readings.");
|
| 647 |
+
}
|
| 648 |
+
|
| 649 |
+
const parsedData = JSON.parse(fileContent);
|
| 650 |
+
const productsToImport: any[] = Array.isArray(parsedData) ? parsedData : [parsedData];
|
| 651 |
+
|
| 652 |
+
// Fetch existing configurations to handle duplicate prevention by names and size matching
|
| 653 |
+
const existingProducts = await getAllProducts();
|
| 654 |
+
|
| 655 |
+
let importCount = 0;
|
| 656 |
+
let duplicateCount = 0;
|
| 657 |
+
|
| 658 |
+
const batch = writeBatch(db);
|
| 659 |
+
|
| 660 |
+
for (const item of productsToImport) {
|
| 661 |
+
// Normalize names & sizes check for solid uniqueness check
|
| 662 |
+
const isDuplicate = existingProducts.some(ep =>
|
| 663 |
+
ep.name.toLowerCase().trim() === item.name?.toLowerCase().trim() &&
|
| 664 |
+
ep.size.toLowerCase().trim() === item.size?.toLowerCase().trim()
|
| 665 |
+
);
|
| 666 |
+
|
| 667 |
+
if (isDuplicate) {
|
| 668 |
+
duplicateCount++;
|
| 669 |
+
continue;
|
| 670 |
+
}
|
| 671 |
+
|
| 672 |
+
const uniqueId = item.id || `imported-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
|
| 673 |
+
const payload = fillCompatibilityFields({
|
| 674 |
+
...item,
|
| 675 |
+
id: uniqueId,
|
| 676 |
+
createdAt: new Date()
|
| 677 |
+
});
|
| 678 |
+
|
| 679 |
+
const docRef = doc(db, COLLECTION_NAME, uniqueId);
|
| 680 |
+
batch.set(docRef, payload);
|
| 681 |
+
importCount++;
|
| 682 |
+
}
|
| 683 |
+
|
| 684 |
+
if (importCount > 0) {
|
| 685 |
+
await batch.commit();
|
| 686 |
+
}
|
| 687 |
+
|
| 688 |
+
resolve({ imported: importCount, duplicates: duplicateCount });
|
| 689 |
+
} catch (error: any) {
|
| 690 |
+
console.error("Failed to parse or save backup file:", error);
|
| 691 |
+
reject(new Error(error.message || "Invalid JSON or database security rules violation during upload."));
|
| 692 |
+
}
|
| 693 |
+
};
|
| 694 |
+
reader.onerror = () => reject(new Error("File read error."));
|
| 695 |
+
reader.readAsText(jsonFile);
|
| 696 |
+
});
|
| 697 |
+
}
|
| 698 |
+
|
| 699 |
+
// ---------------------------------------------------------
|
| 700 |
+
// 11. VISITOR TRACKING & TRAFFIC METRICS
|
| 701 |
+
// ---------------------------------------------------------
|
| 702 |
+
|
| 703 |
+
function getOrCreateSessionId(): string {
|
| 704 |
+
if (typeof window === "undefined" || !window.sessionStorage) {
|
| 705 |
+
return "session-fallback";
|
| 706 |
+
}
|
| 707 |
+
let sid = window.sessionStorage.getItem("hb_visitor_sid");
|
| 708 |
+
if (!sid) {
|
| 709 |
+
sid = "sid-" + Math.random().toString(36).substring(2, 15) + "-" + Date.now();
|
| 710 |
+
window.sessionStorage.setItem("hb_visitor_sid", sid);
|
| 711 |
+
}
|
| 712 |
+
return sid;
|
| 713 |
+
}
|
| 714 |
+
|
| 715 |
+
/**
|
| 716 |
+
* Logs a standard visitor page view event into the Firestore visitors collection
|
| 717 |
+
*/
|
| 718 |
+
export async function logVisitorInfo(pagePath: string): Promise<void> {
|
| 719 |
+
if (typeof window === "undefined") return;
|
| 720 |
+
try {
|
| 721 |
+
const sessionId = getOrCreateSessionId();
|
| 722 |
+
const userAgent = window.navigator.userAgent || "Unknown UA";
|
| 723 |
+
const language = window.navigator.language || "en";
|
| 724 |
+
|
| 725 |
+
// Quick OS signature parsing
|
| 726 |
+
let platform = "Other / Web";
|
| 727 |
+
const ua = userAgent.toLowerCase();
|
| 728 |
+
if (ua.includes("windows")) platform = "Windows";
|
| 729 |
+
else if (ua.includes("macintosh") || ua.includes("mac os")) platform = "macOS";
|
| 730 |
+
else if (ua.includes("android")) platform = "Android";
|
| 731 |
+
else if (ua.includes("iphone") || ua.includes("ipad")) platform = "iOS";
|
| 732 |
+
else if (ua.includes("linux")) platform = "Linux";
|
| 733 |
+
|
| 734 |
+
// Build unique document key
|
| 735 |
+
const customId = `vis-${sessionId.substring(4, 16)}-${Date.now()}`;
|
| 736 |
+
const docRef = doc(db, "visitors", customId);
|
| 737 |
+
|
| 738 |
+
await setDoc(docRef, {
|
| 739 |
+
sessionId,
|
| 740 |
+
userAgent: userAgent.substring(0, 500),
|
| 741 |
+
platform,
|
| 742 |
+
language,
|
| 743 |
+
pagePath,
|
| 744 |
+
timestamp: serverTimestamp()
|
| 745 |
+
});
|
| 746 |
+
} catch (error) {
|
| 747 |
+
// Fail silently so analytics tracking never disrupts primary showroom client flow
|
| 748 |
+
console.warn("Analytics logging bypassed:", error);
|
| 749 |
+
}
|
| 750 |
+
}
|
| 751 |
+
|
| 752 |
+
export interface VisitorRecord {
|
| 753 |
+
id: string;
|
| 754 |
+
sessionId: string;
|
| 755 |
+
userAgent: string;
|
| 756 |
+
platform: string;
|
| 757 |
+
language: string;
|
| 758 |
+
pagePath: string;
|
| 759 |
+
timestamp: any;
|
| 760 |
+
}
|
| 761 |
+
|
| 762 |
+
/**
|
| 763 |
+
* Retrieves the sequential log of website visitation reports
|
| 764 |
+
*/
|
| 765 |
+
export async function getVisitorLogs(): Promise<VisitorRecord[]> {
|
| 766 |
+
try {
|
| 767 |
+
const q = query(collection(db, "visitors"));
|
| 768 |
+
const querySnapshot = await getDocs(q);
|
| 769 |
+
const results: VisitorRecord[] = [];
|
| 770 |
+
querySnapshot.forEach((doc) => {
|
| 771 |
+
const data = doc.data();
|
| 772 |
+
results.push({
|
| 773 |
+
id: doc.id,
|
| 774 |
+
sessionId: data.sessionId || "",
|
| 775 |
+
userAgent: data.userAgent || "",
|
| 776 |
+
platform: data.platform || "",
|
| 777 |
+
language: data.language || "",
|
| 778 |
+
pagePath: data.pagePath || "",
|
| 779 |
+
timestamp: data.timestamp
|
| 780 |
+
});
|
| 781 |
+
});
|
| 782 |
+
|
| 783 |
+
// Order chronologically by descending timestamp order
|
| 784 |
+
results.sort((a, b) => {
|
| 785 |
+
const tA = a.timestamp?.seconds || 0;
|
| 786 |
+
const tB = b.timestamp?.seconds || 0;
|
| 787 |
+
return tB - tA;
|
| 788 |
+
});
|
| 789 |
+
|
| 790 |
+
return results;
|
| 791 |
+
} catch (error) {
|
| 792 |
+
handleFirestoreError(error, OperationType.GET, "visitors");
|
| 793 |
+
throw error;
|
| 794 |
+
}
|
| 795 |
+
}
|
| 796 |
+
|
| 797 |
+
/**
|
| 798 |
+
* Adds a new email address to the newsletter subscribers list in Firestore.
|
| 799 |
+
*/
|
| 800 |
+
export async function subscribeNewsletter(email: string): Promise<void> {
|
| 801 |
+
const collectionName = "subscribers";
|
| 802 |
+
if (!isBrowserOnline()) {
|
| 803 |
+
throw new Error("Unable to subscribe. You are currently offline.");
|
| 804 |
+
}
|
| 805 |
+
|
| 806 |
+
const cleanEmail = email.trim().toLowerCase();
|
| 807 |
+
const emailRegex = /^[^@]+@[^@]+\.[^@]+$/;
|
| 808 |
+
if (!emailRegex.test(cleanEmail)) {
|
| 809 |
+
throw new Error("Please present a valid email format.");
|
| 810 |
+
}
|
| 811 |
+
|
| 812 |
+
try {
|
| 813 |
+
// Build a secure document ID based on the email (replaces special characters) matching the rules regex limit.
|
| 814 |
+
const customId = `sub-${cleanEmail.replace(/[^a-zA-Z0-9_-]/g, "_")}`.substring(0, 128);
|
| 815 |
+
const docRef = doc(db, collectionName, customId);
|
| 816 |
+
|
| 817 |
+
await setDoc(docRef, {
|
| 818 |
+
email: cleanEmail,
|
| 819 |
+
createdAt: serverTimestamp()
|
| 820 |
+
});
|
| 821 |
+
} catch (error) {
|
| 822 |
+
handleFirestoreError(error, OperationType.WRITE, collectionName);
|
| 823 |
+
throw error;
|
| 824 |
+
}
|
| 825 |
+
}
|
| 826 |
+
|
| 827 |
+
|
src/lib/tireService.ts
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Bridge file forwarding products data queries to our clean Firebase Service
|
| 2 |
+
import { TireProduct } from "../data";
|
| 3 |
+
import {
|
| 4 |
+
getAllProducts,
|
| 5 |
+
addProduct,
|
| 6 |
+
updateProduct,
|
| 7 |
+
deleteProduct,
|
| 8 |
+
ProductDocument,
|
| 9 |
+
DEFAULT_TIR_PRODUCTS_SEED
|
| 10 |
+
} from "./firebaseService";
|
| 11 |
+
import {
|
| 12 |
+
collection,
|
| 13 |
+
getDocs,
|
| 14 |
+
addDoc
|
| 15 |
+
} from "firebase/firestore";
|
| 16 |
+
import { db, handleFirestoreError, OperationType } from "./firebase";
|
| 17 |
+
|
| 18 |
+
export interface CustomReview {
|
| 19 |
+
id?: string;
|
| 20 |
+
name: string;
|
| 21 |
+
rating: number;
|
| 22 |
+
text: string;
|
| 23 |
+
date: string;
|
| 24 |
+
initials: string;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
// Map compatibility fields from FirebaseService Product to TireProduct
|
| 28 |
+
function mapToTireProduct(p: ProductDocument): TireProduct {
|
| 29 |
+
return {
|
| 30 |
+
id: p.id,
|
| 31 |
+
name: p.name,
|
| 32 |
+
brand: p.brand,
|
| 33 |
+
size: p.size,
|
| 34 |
+
feature: p.feature,
|
| 35 |
+
price: p.price,
|
| 36 |
+
badge: p.badge || (p.category === "hot-selling" ? "Hot Selling" : p.category === "new-brands" ? "New Arrival" : "Famous"),
|
| 37 |
+
segment: p.segment || (p.category === "hot-selling" ? "hot" : p.category === "new-brands" ? "new" : "famous"),
|
| 38 |
+
image: p.image || p.imageUrl,
|
| 39 |
+
description: p.description || `Premium quality tire from ${p.brand}.`
|
| 40 |
+
};
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
// 1. Fetch Tyres from Firestore (migrated to the new 'products' collection)
|
| 44 |
+
export async function getTireProducts(): Promise<TireProduct[]> {
|
| 45 |
+
try {
|
| 46 |
+
const list = await getAllProducts();
|
| 47 |
+
return list.map(mapToTireProduct);
|
| 48 |
+
} catch (error) {
|
| 49 |
+
console.warn("Could not load from Firestore 'products', falling back.", error);
|
| 50 |
+
return SEED_TYRE_PRODUCTS;
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
// 2. Add or Edit Tyre in Firestore (migrated to the new 'products' collection)
|
| 55 |
+
export async function saveTireProduct(tire: TireProduct): Promise<void> {
|
| 56 |
+
const categoryMapped = tire.segment === "hot" ? "hot-selling" : tire.segment === "new" ? "new-brands" : "famous";
|
| 57 |
+
|
| 58 |
+
const payload: Partial<ProductDocument> = {
|
| 59 |
+
name: tire.name,
|
| 60 |
+
brand: tire.brand,
|
| 61 |
+
size: tire.size,
|
| 62 |
+
feature: tire.feature,
|
| 63 |
+
price: Number(tire.price),
|
| 64 |
+
category: categoryMapped as any,
|
| 65 |
+
segment: tire.segment,
|
| 66 |
+
badge: tire.badge,
|
| 67 |
+
imageUrl: tire.image,
|
| 68 |
+
image: tire.image,
|
| 69 |
+
stock: 10, // default stock allocation
|
| 70 |
+
description: tire.description
|
| 71 |
+
};
|
| 72 |
+
|
| 73 |
+
try {
|
| 74 |
+
// If tire ID starts with custom- or looks like a temp, we check if it is existing in firestore
|
| 75 |
+
const existing = await getTireProducts();
|
| 76 |
+
const match = existing.find(e => e.id === tire.id);
|
| 77 |
+
|
| 78 |
+
if (match) {
|
| 79 |
+
await updateProduct(tire.id, payload);
|
| 80 |
+
} else {
|
| 81 |
+
await addProduct({
|
| 82 |
+
...payload,
|
| 83 |
+
name: tire.name,
|
| 84 |
+
brand: tire.brand,
|
| 85 |
+
size: tire.size,
|
| 86 |
+
feature: tire.feature,
|
| 87 |
+
price: Number(tire.price),
|
| 88 |
+
category: categoryMapped as any,
|
| 89 |
+
stock: 10,
|
| 90 |
+
imageUrl: tire.image
|
| 91 |
+
} as ProductDocument);
|
| 92 |
+
}
|
| 93 |
+
} catch (error) {
|
| 94 |
+
handleFirestoreError(error, OperationType.WRITE, `products/${tire.id}`);
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
// 3. Delete Tyre from Firestore 'products'
|
| 99 |
+
export async function deleteTireProduct(tireId: string): Promise<void> {
|
| 100 |
+
try {
|
| 101 |
+
await deleteProduct(tireId);
|
| 102 |
+
} catch (error) {
|
| 103 |
+
handleFirestoreError(error, OperationType.DELETE, `products/${tireId}`);
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
// 4. Fetch Reviews from Firestore
|
| 108 |
+
export async function getReviews(): Promise<CustomReview[]> {
|
| 109 |
+
const collectionName = "reviews";
|
| 110 |
+
try {
|
| 111 |
+
const list: CustomReview[] = [];
|
| 112 |
+
const querySnapshot = await getDocs(collection(db, collectionName));
|
| 113 |
+
querySnapshot.forEach((doc) => {
|
| 114 |
+
list.push({ id: doc.id, ...doc.data() } as CustomReview);
|
| 115 |
+
});
|
| 116 |
+
return list;
|
| 117 |
+
} catch (error) {
|
| 118 |
+
console.warn("Could not retrieve custom reviews from Firestore.", error);
|
| 119 |
+
return [];
|
| 120 |
+
}
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
// 5. Add Review to Firestore
|
| 124 |
+
export async function addReview(review: Omit<CustomReview, "id">): Promise<string> {
|
| 125 |
+
const collectionName = "reviews";
|
| 126 |
+
try {
|
| 127 |
+
const docRef = await addDoc(collection(db, collectionName), {
|
| 128 |
+
name: review.name,
|
| 129 |
+
rating: Number(review.rating),
|
| 130 |
+
text: review.text,
|
| 131 |
+
date: review.date,
|
| 132 |
+
initials: review.initials,
|
| 133 |
+
});
|
| 134 |
+
return docRef.id;
|
| 135 |
+
} catch (error) {
|
| 136 |
+
handleFirestoreError(error, OperationType.WRITE, collectionName);
|
| 137 |
+
return "error";
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
// Fallback local seed data
|
| 142 |
+
export const SEED_TYRE_PRODUCTS: TireProduct[] = DEFAULT_TIR_PRODUCTS_SEED.map((p, i) => ({
|
| 143 |
+
id: `seed-${p.brand.toLowerCase()}-${i + 1}`,
|
| 144 |
+
name: p.name,
|
| 145 |
+
brand: p.brand,
|
| 146 |
+
size: p.size,
|
| 147 |
+
feature: p.feature,
|
| 148 |
+
price: p.price,
|
| 149 |
+
badge: p.category === "hot-selling" ? "Hot Selling" : p.category === "new-brands" ? "New Arrival" : "Famous",
|
| 150 |
+
segment: p.category === "hot-selling" ? "hot" : p.category === "new-brands" ? "new" : "famous",
|
| 151 |
+
image: p.imageUrl,
|
| 152 |
+
description: p.description || ""
|
| 153 |
+
}));
|
| 154 |
+
|
| 155 |
+
// Pak-Specific Fitments specifications model structure
|
| 156 |
+
export interface CarFitment {
|
| 157 |
+
make: string;
|
| 158 |
+
model: string;
|
| 159 |
+
variants: {
|
| 160 |
+
name: string;
|
| 161 |
+
recommendedSize: string;
|
| 162 |
+
engine: string;
|
| 163 |
+
pressure: string;
|
| 164 |
+
}[];
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
export const PAK_CAR_FITMENTS: CarFitment[] = [
|
| 168 |
+
{
|
| 169 |
+
make: "Toyota",
|
| 170 |
+
model: "Corolla",
|
| 171 |
+
variants: [
|
| 172 |
+
{ name: "Altis Grande", recommendedSize: "195/65R15", engine: "1800cc", pressure: "32 PSI" },
|
| 173 |
+
{ name: "GLI / XLI", recommendedSize: "185/65R15", engine: "1300cc", pressure: "30 PSI" },
|
| 174 |
+
{ name: "Altis 1.6", recommendedSize: "195/65R15", engine: "1600cc", pressure: "32 PSI" }
|
| 175 |
+
]
|
| 176 |
+
},
|
| 177 |
+
{
|
| 178 |
+
make: "Toyota",
|
| 179 |
+
model: "Yaris",
|
| 180 |
+
variants: [
|
| 181 |
+
{ name: "ATIV X CVT", recommendedSize: "185/60R15", engine: "1500cc", pressure: "32 PSI" },
|
| 182 |
+
{ name: "GLI MT / CVT", recommendedSize: "185/60R15", engine: "1300cc", pressure: "30 PSI" }
|
| 183 |
+
]
|
| 184 |
+
},
|
| 185 |
+
{
|
| 186 |
+
make: "Toyota",
|
| 187 |
+
model: "Fortuner",
|
| 188 |
+
variants: [
|
| 189 |
+
{ name: "Sigma 4 Diesel", recommendedSize: "265/60R18", engine: "2800cc", pressure: "32 PSI" },
|
| 190 |
+
{ name: "2.7 G Petrol", recommendedSize: "265/65R17", engine: "2700cc", pressure: "30 PSI" }
|
| 191 |
+
]
|
| 192 |
+
},
|
| 193 |
+
{
|
| 194 |
+
make: "Toyota",
|
| 195 |
+
model: "Hilux Revo",
|
| 196 |
+
variants: [
|
| 197 |
+
{ name: "Revo V", recommendedSize: "265/60R18", engine: "2800cc", pressure: "32 PSI" },
|
| 198 |
+
{ name: "Revo G / Single Cabin", recommendedSize: "265/65R17", engine: "2800cc", pressure: "32 PSI" }
|
| 199 |
+
]
|
| 200 |
+
},
|
| 201 |
+
{
|
| 202 |
+
make: "Toyota",
|
| 203 |
+
model: "Vitz",
|
| 204 |
+
variants: [
|
| 205 |
+
{ name: "Hybrid / F", recommendedSize: "165/70R14", engine: "1000cc / 1500cc", pressure: "30 PSI" },
|
| 206 |
+
{ name: "Jewela / U", recommendedSize: "185/60R15", engine: "1300cc", pressure: "30 PSI" }
|
| 207 |
+
]
|
| 208 |
+
},
|
| 209 |
+
{
|
| 210 |
+
make: "Honda",
|
| 211 |
+
model: "Civic",
|
| 212 |
+
variants: [
|
| 213 |
+
{ name: "Oriel", recommendedSize: "215/55R16", engine: "1800cc", pressure: "32 PSI" },
|
| 214 |
+
{ name: "RS Turbo", recommendedSize: "215/50R17", engine: "1500cc", pressure: "32 PSI" }
|
| 215 |
+
]
|
| 216 |
+
},
|
| 217 |
+
{
|
| 218 |
+
make: "Honda",
|
| 219 |
+
model: "City",
|
| 220 |
+
variants: [
|
| 221 |
+
{ name: "1.5L Aspire", recommendedSize: "185/55R16", engine: "1500cc", pressure: "32 PSI" },
|
| 222 |
+
{ name: "1.2L i-VTEC", recommendedSize: "175/65R15", engine: "1200cc", pressure: "30 PSI" }
|
| 223 |
+
]
|
| 224 |
+
},
|
| 225 |
+
{
|
| 226 |
+
make: "Honda",
|
| 227 |
+
model: "BR-V",
|
| 228 |
+
variants: [
|
| 229 |
+
{ name: "i-VTEC S", recommendedSize: "195/60R16", engine: "1500cc", pressure: "32 PSI" }
|
| 230 |
+
]
|
| 231 |
+
},
|
| 232 |
+
{
|
| 233 |
+
make: "Suzuki",
|
| 234 |
+
model: "Alto",
|
| 235 |
+
variants: [
|
| 236 |
+
{ name: "VXL / AGS", recommendedSize: "145/80R13", engine: "660cc", pressure: "30 PSI" },
|
| 237 |
+
{ name: "VXR / VX", recommendedSize: "145/80R13", engine: "660cc", pressure: "28 PSI" }
|
| 238 |
+
]
|
| 239 |
+
},
|
| 240 |
+
{
|
| 241 |
+
make: "Suzuki",
|
| 242 |
+
model: "Cultus",
|
| 243 |
+
variants: [
|
| 244 |
+
{ name: "VXL / AGS", recommendedSize: "165/70R14", engine: "1000cc", pressure: "30 PSI" },
|
| 245 |
+
{ name: "VXR", recommendedSize: "165/70R14", engine: "1000cc", pressure: "30 PSI" }
|
| 246 |
+
]
|
| 247 |
+
},
|
| 248 |
+
{
|
| 249 |
+
make: "Suzuki",
|
| 250 |
+
model: "Swift",
|
| 251 |
+
variants: [
|
| 252 |
+
{ name: "GLX CVT", recommendedSize: "185/55R16", engine: "1200cc", pressure: "32 PSI" },
|
| 253 |
+
{ name: "GL MT / CVT", recommendedSize: "185/60R15", engine: "1200cc", pressure: "32 PSI" }
|
| 254 |
+
]
|
| 255 |
+
},
|
| 256 |
+
{
|
| 257 |
+
make: "Suzuki",
|
| 258 |
+
model: "Wagon R",
|
| 259 |
+
variants: [
|
| 260 |
+
{ name: "VXL / AGS", recommendedSize: "145/80R13", engine: "1000cc", pressure: "30 PSI" },
|
| 261 |
+
{ name: "VXR", recommendedSize: "145/80R13", engine: "1000cc", pressure: "30 PSI" }
|
| 262 |
+
]
|
| 263 |
+
},
|
| 264 |
+
{
|
| 265 |
+
make: "Suzuki",
|
| 266 |
+
model: "Mehran",
|
| 267 |
+
variants: [
|
| 268 |
+
{ name: "VXR / VX", recommendedSize: "145/70R12", engine: "800cc", pressure: "28 PSI" }
|
| 269 |
+
]
|
| 270 |
+
},
|
| 271 |
+
{
|
| 272 |
+
make: "Kia",
|
| 273 |
+
model: "Sportage",
|
| 274 |
+
variants: [
|
| 275 |
+
{ name: "Alpha", recommendedSize: "225/55R18", engine: "2000cc", pressure: "34 PSI" },
|
| 276 |
+
{ name: "AWD / FWD", recommendedSize: "225/55R18", engine: "2000cc", pressure: "34 PSI" }
|
| 277 |
+
]
|
| 278 |
+
},
|
| 279 |
+
{
|
| 280 |
+
make: "Kia",
|
| 281 |
+
model: "Picanto",
|
| 282 |
+
variants: [
|
| 283 |
+
{ name: "Automatic 1.0", recommendedSize: "165/60R14", engine: "1000cc", pressure: "30 PSI" },
|
| 284 |
+
{ name: "Manual 1.0", recommendedSize: "165/60R14", engine: "1000cc", pressure: "30 PSI" }
|
| 285 |
+
]
|
| 286 |
+
},
|
| 287 |
+
{
|
| 288 |
+
make: "Kia",
|
| 289 |
+
model: "Stonic",
|
| 290 |
+
variants: [
|
| 291 |
+
{ name: "EX Plus", recommendedSize: "195/55R16", engine: "1400cc", pressure: "32 PSI" }
|
| 292 |
+
]
|
| 293 |
+
},
|
| 294 |
+
{
|
| 295 |
+
make: "Hyundai",
|
| 296 |
+
model: "Tucson",
|
| 297 |
+
variants: [
|
| 298 |
+
{ name: "GLS Sport", recommendedSize: "225/55R18", engine: "2000cc", pressure: "33 PSI" },
|
| 299 |
+
{ name: "Ultimate", recommendedSize: "225/55R18", engine: "2000cc", pressure: "33 PSI" }
|
| 300 |
+
]
|
| 301 |
+
},
|
| 302 |
+
{
|
| 303 |
+
make: "Hyundai",
|
| 304 |
+
model: "Elantra",
|
| 305 |
+
variants: [
|
| 306 |
+
{ name: "GLS 2.0", recommendedSize: "205/55R16", engine: "2000cc", pressure: "32 PSI" },
|
| 307 |
+
{ name: "GL 1.6", recommendedSize: "205/55R16", engine: "1600cc", pressure: "32 PSI" }
|
| 308 |
+
]
|
| 309 |
+
},
|
| 310 |
+
{
|
| 311 |
+
make: "Hyundai",
|
| 312 |
+
model: "Sonata",
|
| 313 |
+
variants: [
|
| 314 |
+
{ name: "Sonata 2.5L", recommendedSize: "235/45R18", engine: "2500cc", pressure: "32 PSI" },
|
| 315 |
+
{ name: "Sonata 2.0L", recommendedSize: "215/55R17", engine: "2000cc", pressure: "32 PSI" }
|
| 316 |
+
]
|
| 317 |
+
},
|
| 318 |
+
{
|
| 319 |
+
make: "Changan",
|
| 320 |
+
model: "Alsvin",
|
| 321 |
+
variants: [
|
| 322 |
+
{ name: "Lumiere 1.5", recommendedSize: "185/55R15", engine: "1500cc", pressure: "32 PSI" },
|
| 323 |
+
{ name: "Comfort 1.3 / 1.5", recommendedSize: "185/55R15", engine: "1300cc / 1500cc", pressure: "30 PSI" }
|
| 324 |
+
]
|
| 325 |
+
},
|
| 326 |
+
{
|
| 327 |
+
make: "Changan",
|
| 328 |
+
model: "Oshan X7",
|
| 329 |
+
variants: [
|
| 330 |
+
{ name: "FutureSense", recommendedSize: "225/55R19", engine: "1500cc Turbo", pressure: "34 PSI" },
|
| 331 |
+
{ name: "Comfort", recommendedSize: "225/55R19", engine: "1500cc Turbo", pressure: "34 PSI" }
|
| 332 |
+
]
|
| 333 |
+
},
|
| 334 |
+
{
|
| 335 |
+
make: "MG",
|
| 336 |
+
model: "HS",
|
| 337 |
+
variants: [
|
| 338 |
+
{ name: "Essence / Trophy", recommendedSize: "235/50R18", engine: "1500cc Turbo", pressure: "32 PSI" }
|
| 339 |
+
]
|
| 340 |
+
},
|
| 341 |
+
{
|
| 342 |
+
make: "Haval",
|
| 343 |
+
model: "H6",
|
| 344 |
+
variants: [
|
| 345 |
+
{ name: "HEV (Hybrid)", recommendedSize: "225/55R19", engine: "1500cc HEV", pressure: "33 PSI" },
|
| 346 |
+
{ name: "1.5T / 2.0T AWD", recommendedSize: "225/55R19", engine: "1500cc / 2000cc", pressure: "33 PSI" }
|
| 347 |
+
]
|
| 348 |
+
},
|
| 349 |
+
{
|
| 350 |
+
make: "Haval",
|
| 351 |
+
model: "Jolion",
|
| 352 |
+
variants: [
|
| 353 |
+
{ name: "High Grade", recommendedSize: "225/55R18", engine: "1500cc Turbo", pressure: "32 PSI" }
|
| 354 |
+
]
|
| 355 |
+
},
|
| 356 |
+
{
|
| 357 |
+
make: "Proton",
|
| 358 |
+
model: "Saga",
|
| 359 |
+
variants: [
|
| 360 |
+
{ name: "Ace A/T", recommendedSize: "185/55R15", engine: "1300cc", pressure: "32 PSI" },
|
| 361 |
+
{ name: "Standard M/T / A/T", recommendedSize: "185/60R14", engine: "1300cc", pressure: "30 PSI" }
|
| 362 |
+
]
|
| 363 |
+
},
|
| 364 |
+
{
|
| 365 |
+
make: "DFSK",
|
| 366 |
+
model: "Glory 580",
|
| 367 |
+
variants: [
|
| 368 |
+
{ name: "Glory 580 Pro", recommendedSize: "225/60R17", engine: "1500cc Turbo", pressure: "32 PSI" }
|
| 369 |
+
]
|
| 370 |
+
}
|
| 371 |
+
];
|
| 372 |
+
|
src/main.tsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {StrictMode} from 'react';
|
| 2 |
+
import {createRoot} from 'react-dom/client';
|
| 3 |
+
import App from './App.tsx';
|
| 4 |
+
import './index.css';
|
| 5 |
+
|
| 6 |
+
createRoot(document.getElementById('root')!).render(
|
| 7 |
+
<StrictMode>
|
| 8 |
+
<App />
|
| 9 |
+
</StrictMode>,
|
| 10 |
+
);
|
storage.rules
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
rules_version = '2';
|
| 2 |
+
service firebase.storage {
|
| 3 |
+
match /b/{bucket}/o {
|
| 4 |
+
|
| 5 |
+
// Default-deny policy for entire storage registry
|
| 6 |
+
match /{allPaths=**} {
|
| 7 |
+
allow read, write: if false;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
// Rules for the product-images folder: public read allowed, authenticated writes only
|
| 11 |
+
match /product-images/{imageId} {
|
| 12 |
+
allow read: if true;
|
| 13 |
+
allow write: if request.auth != null
|
| 14 |
+
&& request.resource.size < 5 * 1024 * 1024 // limit size to 5MB
|
| 15 |
+
&& request.resource.contentType.matches('image/.*'); // strictly image MIME types
|
| 16 |
+
allow delete: if request.auth != null;
|
| 17 |
+
}
|
| 18 |
+
}
|
| 19 |
+
}
|
tsconfig.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2022",
|
| 4 |
+
"experimentalDecorators": true,
|
| 5 |
+
"useDefineForClassFields": false,
|
| 6 |
+
"module": "ESNext",
|
| 7 |
+
"lib": [
|
| 8 |
+
"ES2022",
|
| 9 |
+
"DOM",
|
| 10 |
+
"DOM.Iterable"
|
| 11 |
+
],
|
| 12 |
+
"skipLibCheck": true,
|
| 13 |
+
"moduleResolution": "bundler",
|
| 14 |
+
"isolatedModules": true,
|
| 15 |
+
"moduleDetection": "force",
|
| 16 |
+
"allowJs": true,
|
| 17 |
+
"jsx": "react-jsx",
|
| 18 |
+
"paths": {
|
| 19 |
+
"@/*": [
|
| 20 |
+
"./*"
|
| 21 |
+
]
|
| 22 |
+
},
|
| 23 |
+
"allowImportingTsExtensions": true,
|
| 24 |
+
"noEmit": true
|
| 25 |
+
}
|
| 26 |
+
}
|
vite.config.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import tailwindcss from '@tailwindcss/vite';
|
| 2 |
+
import react from '@vitejs/plugin-react';
|
| 3 |
+
import path from 'path';
|
| 4 |
+
import {defineConfig} from 'vite';
|
| 5 |
+
|
| 6 |
+
export default defineConfig(() => {
|
| 7 |
+
return {
|
| 8 |
+
plugins: [react(), tailwindcss()],
|
| 9 |
+
resolve: {
|
| 10 |
+
alias: {
|
| 11 |
+
'@': path.resolve(__dirname, '.'),
|
| 12 |
+
},
|
| 13 |
+
},
|
| 14 |
+
server: {
|
| 15 |
+
// HMR is disabled in AI Studio via DISABLE_HMR env var.
|
| 16 |
+
// Do not modifyâfile watching is disabled to prevent flickering during agent edits.
|
| 17 |
+
hmr: process.env.DISABLE_HMR !== 'true',
|
| 18 |
+
// Disable file watching when DISABLE_HMR is true to save CPU during agent edits.
|
| 19 |
+
watch: process.env.DISABLE_HMR === 'true' ? null : {},
|
| 20 |
+
},
|
| 21 |
+
};
|
| 22 |
+
});
|