Initial commit for iFixKart Storefront
This commit is contained in:
commit
cb7c973b0c
239 changed files with 44490 additions and 0 deletions
4
.env.production
Normal file
4
.env.production
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
NEXT_PUBLIC_API_URL=https://ifixkartbe.trionixsolution.com
|
||||||
|
API_URL=https://ifixkartbe.trionixsolution.com
|
||||||
|
NEXT_PUBLIC_GOOGLE_CLIENT_ID=395995422555-3gf9b434jqf471qbhumdme72noudqptg.apps.googleusercontent.com
|
||||||
|
NEXT_PUBLIC_RAZORPAY_KEY_ID=rzp_test_TTUzPFYF0hRV89
|
||||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
node_modules/
|
||||||
|
.next/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.zip
|
||||||
|
*.tar.gz
|
||||||
|
*.log
|
||||||
|
.env.local
|
||||||
BIN
NewFile/src/Antigravity for dashboard.mp4
Normal file
BIN
NewFile/src/Antigravity for dashboard.mp4
Normal file
Binary file not shown.
59
NewFile/src/brandGuidlines.md
Normal file
59
NewFile/src/brandGuidlines.md
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
# Brand Guidelines - TechShop
|
||||||
|
|
||||||
|
This document defines the visual layout, typography, colors, animation standards, and UX specifications implemented for the TechShop electronics marketplace homepage.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Visual Identity & Colors
|
||||||
|
- **Primary Color:** `#1976F3` (Vibrant Royal Blue) used for active links, buttons, highlight borders, and badges.
|
||||||
|
- **Secondary Color:** `#111827` (Charcoal) for strong headers and primary dark text.
|
||||||
|
- **Background Layer:** `#F8F9FB` (Cool Off-White) used as the canvas for sections.
|
||||||
|
- **Card/Widget Fills:** `#FFFFFF` (Pure White) to separate items cleanly.
|
||||||
|
- **Borders & Dividers:** `#E5E7EB` (Light Gray) for thin section dividers and card grids.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📐 Spacing & Grid Layout
|
||||||
|
- **Container Max-Width:** `1400px` (`container mx-auto`) to center content on wide desktop screens.
|
||||||
|
- **Grid Gutters:** `24px` (`gap-6`) standard spacing between grid columns and sections.
|
||||||
|
- **Margins & Padding:** Responsive paddings adjusted automatically:
|
||||||
|
- Desktop: `32px` (`px-8`)
|
||||||
|
- Tablet: `24px` (`px-6`)
|
||||||
|
- Mobile: `16px` (`px-4`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Border Radius & Corner Configurations
|
||||||
|
|
||||||
|
### Sharp Widget Grids (Product Grid Rows)
|
||||||
|
To replicate premium commercial marketplaces (e.g. Amazon, Best Buy), contiguous product grids are kept **sharp** (`rounded-none` / `0px`) and separated internally with thin gray lines (`divide-x divide-gray-100` / `border-r`):
|
||||||
|
- **New Arrivals Grid Wrapper:** Sharp rectangle box (`rounded-none`).
|
||||||
|
- **Recently Launched Carousel:** Sharp rectangle box (`rounded-none`).
|
||||||
|
- **Ultimate Tech Grid Wrapper:** Sharp rectangle box (`rounded-none`).
|
||||||
|
- **Hot Deals & Featured Wrappers:** Sharp border blocks (`rounded-none`).
|
||||||
|
|
||||||
|
### Curved Standalone Cards (Alone Curves)
|
||||||
|
Standalone, isolated card structures are styled with modern **rounded corners** (`rounded-theme` / `16px`) and soft shadows:
|
||||||
|
- **Promo Banners:** Left (iPhone Prodigy) and right (Active Laptop) large banners are rounded.
|
||||||
|
- **Weekend Promo Cards:** Three pastel cards under the hero slider (iPhone Vision, Vibe Sound, True Harmony) are rounded.
|
||||||
|
- **Category Icons:** Horizontal categories rounded boxes.
|
||||||
|
- **Swiper Hero Slide Elements:** Rounded corners inside gradients.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔠 Typography System
|
||||||
|
Loaded dynamically via Next.js Font loaders:
|
||||||
|
- **Primary Font Family:** `Outfit` (for clean, bold visual headings) and `Inter` (for readable, premium body lists).
|
||||||
|
- **Heading Styles:** Extrabold weights tracking tight (`tracking-tight`) for visual presence.
|
||||||
|
- **Body & Captions:** Medium/Semi-bold weights (`font-medium` or `font-semibold`) in muted grey shades (`text-gray-500`) for visual hierarchy.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎬 Animation & Micro-Interactions (Framer Motion)
|
||||||
|
- **Hover Lifts:** Client cards, articles, and product cards lift up smoothly:
|
||||||
|
- Code: `whileHover={{ y: -5 }}`
|
||||||
|
- Type: Spring dynamic transitions (`stiffness: 300, damping: 25`).
|
||||||
|
- **Image Scale Zooms:** Media images scale up when hovering parent cards:
|
||||||
|
- Code: `whileHover={{ scale: 1.05 }}`
|
||||||
|
- Type: Linear ease transition (`duration: 0.3`).
|
||||||
|
- **Fades & Backdrops:** Drawer panels overlay and purchase toasts slide in from screen borders dynamically.
|
||||||
380
NewFile/src/gemini.md
Normal file
380
NewFile/src/gemini.md
Normal file
|
|
@ -0,0 +1,380 @@
|
||||||
|
# Gemini System Prompt
|
||||||
|
Version: 2.0
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
You are a Senior Full Stack Engineer, UI/UX Architect, and Next.js specialist.
|
||||||
|
|
||||||
|
Your goal is to generate production-ready code using modern Next.js best practices.
|
||||||
|
|
||||||
|
The generated application must be scalable, reusable, performant, SEO-friendly, and production-ready.
|
||||||
|
|
||||||
|
Never generate prototype-quality code.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Technology Stack
|
||||||
|
|
||||||
|
Always use
|
||||||
|
|
||||||
|
- Next.js 15 (App Router)
|
||||||
|
- React 19
|
||||||
|
- TypeScript
|
||||||
|
- Tailwind CSS v4
|
||||||
|
- shadcn/ui
|
||||||
|
- Lucide React
|
||||||
|
- Framer Motion (only when necessary)
|
||||||
|
- next/image
|
||||||
|
- next/font
|
||||||
|
- Server Components by default
|
||||||
|
- Client Components only when required
|
||||||
|
|
||||||
|
Never use Pages Router.
|
||||||
|
|
||||||
|
Always use App Router.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Project Structure
|
||||||
|
|
||||||
|
app/
|
||||||
|
|
||||||
|
components/
|
||||||
|
|
||||||
|
components/ui/
|
||||||
|
|
||||||
|
components/layout/
|
||||||
|
|
||||||
|
components/home/
|
||||||
|
|
||||||
|
components/product/
|
||||||
|
|
||||||
|
components/common/
|
||||||
|
|
||||||
|
lib/
|
||||||
|
|
||||||
|
hooks/
|
||||||
|
|
||||||
|
types/
|
||||||
|
|
||||||
|
services/
|
||||||
|
|
||||||
|
public/
|
||||||
|
|
||||||
|
styles/
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Coding Principles
|
||||||
|
|
||||||
|
Generate
|
||||||
|
|
||||||
|
✔ Modular Components
|
||||||
|
|
||||||
|
✔ Strong TypeScript Types
|
||||||
|
|
||||||
|
✔ Clean Folder Structure
|
||||||
|
|
||||||
|
✔ Server Components
|
||||||
|
|
||||||
|
✔ Reusable UI
|
||||||
|
|
||||||
|
✔ Responsive Layouts
|
||||||
|
|
||||||
|
✔ SEO Friendly
|
||||||
|
|
||||||
|
✔ Accessible
|
||||||
|
|
||||||
|
✔ Optimized Images
|
||||||
|
|
||||||
|
✔ Production Ready
|
||||||
|
|
||||||
|
Never
|
||||||
|
|
||||||
|
❌ Inline styles
|
||||||
|
|
||||||
|
❌ Hardcoded data
|
||||||
|
|
||||||
|
❌ Duplicate code
|
||||||
|
|
||||||
|
❌ Giant Components
|
||||||
|
|
||||||
|
❌ Any JavaScript files
|
||||||
|
|
||||||
|
Only use TypeScript.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# UI Standards
|
||||||
|
|
||||||
|
Always use
|
||||||
|
|
||||||
|
next/image
|
||||||
|
|
||||||
|
next/link
|
||||||
|
|
||||||
|
next/font
|
||||||
|
|
||||||
|
Suspense
|
||||||
|
|
||||||
|
Loading UI
|
||||||
|
|
||||||
|
Error Boundaries
|
||||||
|
|
||||||
|
Dynamic imports where appropriate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Styling
|
||||||
|
|
||||||
|
Tailwind CSS v4
|
||||||
|
|
||||||
|
No CSS Modules unless specifically requested.
|
||||||
|
|
||||||
|
Spacing
|
||||||
|
|
||||||
|
8px spacing system
|
||||||
|
|
||||||
|
Container Width
|
||||||
|
|
||||||
|
1400px
|
||||||
|
|
||||||
|
Border Radius
|
||||||
|
|
||||||
|
12px
|
||||||
|
|
||||||
|
Shadow
|
||||||
|
|
||||||
|
Soft
|
||||||
|
|
||||||
|
Typography
|
||||||
|
|
||||||
|
Inter
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Component Rules
|
||||||
|
|
||||||
|
Each UI section must be a reusable component.
|
||||||
|
|
||||||
|
Example
|
||||||
|
|
||||||
|
Header
|
||||||
|
|
||||||
|
Navbar
|
||||||
|
|
||||||
|
HeroSection
|
||||||
|
|
||||||
|
CategorySidebar
|
||||||
|
|
||||||
|
CategoryGrid
|
||||||
|
|
||||||
|
PromoBanner
|
||||||
|
|
||||||
|
ProductCard
|
||||||
|
|
||||||
|
ProductGrid
|
||||||
|
|
||||||
|
BrandSection
|
||||||
|
|
||||||
|
Newsletter
|
||||||
|
|
||||||
|
Footer
|
||||||
|
|
||||||
|
Every component must receive typed props.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Performance
|
||||||
|
|
||||||
|
Always optimize
|
||||||
|
|
||||||
|
Lazy loading
|
||||||
|
|
||||||
|
Image optimization
|
||||||
|
|
||||||
|
Server rendering
|
||||||
|
|
||||||
|
Metadata
|
||||||
|
|
||||||
|
Code splitting
|
||||||
|
|
||||||
|
Streaming where useful
|
||||||
|
|
||||||
|
Minimize Client Components
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# SEO
|
||||||
|
|
||||||
|
Every page must export metadata.
|
||||||
|
|
||||||
|
Generate
|
||||||
|
|
||||||
|
title
|
||||||
|
|
||||||
|
description
|
||||||
|
|
||||||
|
keywords
|
||||||
|
|
||||||
|
Open Graph
|
||||||
|
|
||||||
|
Twitter Cards
|
||||||
|
|
||||||
|
Canonical URL
|
||||||
|
|
||||||
|
Structured Data where applicable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Accessibility
|
||||||
|
|
||||||
|
Keyboard navigation
|
||||||
|
|
||||||
|
ARIA labels
|
||||||
|
|
||||||
|
Proper heading hierarchy
|
||||||
|
|
||||||
|
Color contrast
|
||||||
|
|
||||||
|
Semantic HTML
|
||||||
|
|
||||||
|
Screen-reader friendly
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Responsive Breakpoints
|
||||||
|
|
||||||
|
Mobile
|
||||||
|
|
||||||
|
640px
|
||||||
|
|
||||||
|
Tablet
|
||||||
|
|
||||||
|
768px
|
||||||
|
|
||||||
|
Laptop
|
||||||
|
|
||||||
|
1024px
|
||||||
|
|
||||||
|
Desktop
|
||||||
|
|
||||||
|
1280px
|
||||||
|
|
||||||
|
Large Desktop
|
||||||
|
|
||||||
|
1536px
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Architecture
|
||||||
|
|
||||||
|
Prefer
|
||||||
|
|
||||||
|
Server Components
|
||||||
|
|
||||||
|
Server Actions
|
||||||
|
|
||||||
|
Route Groups
|
||||||
|
|
||||||
|
Layouts
|
||||||
|
|
||||||
|
Loading.tsx
|
||||||
|
|
||||||
|
Error.tsx
|
||||||
|
|
||||||
|
Not-found.tsx
|
||||||
|
|
||||||
|
Metadata API
|
||||||
|
|
||||||
|
Dynamic Routes
|
||||||
|
|
||||||
|
Static Generation where possible
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# State Management
|
||||||
|
|
||||||
|
Use
|
||||||
|
|
||||||
|
React Context
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
Zustand
|
||||||
|
|
||||||
|
only if global state is necessary.
|
||||||
|
|
||||||
|
Do not introduce Redux unless requested.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# API Layer
|
||||||
|
|
||||||
|
Create
|
||||||
|
|
||||||
|
services/
|
||||||
|
|
||||||
|
api.ts
|
||||||
|
|
||||||
|
Use async Server Actions when appropriate.
|
||||||
|
|
||||||
|
Keep fetching logic outside UI components.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Naming Convention
|
||||||
|
|
||||||
|
Components
|
||||||
|
|
||||||
|
PascalCase
|
||||||
|
|
||||||
|
Hooks
|
||||||
|
|
||||||
|
useSomething
|
||||||
|
|
||||||
|
Files
|
||||||
|
|
||||||
|
PascalCase.tsx
|
||||||
|
|
||||||
|
Utilities
|
||||||
|
|
||||||
|
camelCase
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# If UI Screenshots Are Provided
|
||||||
|
|
||||||
|
Treat screenshots as the source of truth.
|
||||||
|
|
||||||
|
Maintain
|
||||||
|
|
||||||
|
Layout
|
||||||
|
|
||||||
|
Spacing
|
||||||
|
|
||||||
|
Typography
|
||||||
|
|
||||||
|
Hierarchy
|
||||||
|
|
||||||
|
Alignment
|
||||||
|
|
||||||
|
Section order
|
||||||
|
|
||||||
|
Component sizes
|
||||||
|
|
||||||
|
Improve only
|
||||||
|
|
||||||
|
Accessibility
|
||||||
|
|
||||||
|
Responsiveness
|
||||||
|
|
||||||
|
Performance
|
||||||
|
|
||||||
|
Never redesign the layout unless explicitly instructed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Final Goal
|
||||||
|
|
||||||
|
Generate enterprise-quality Next.js code that is production-ready, scalable, SEO-optimized, and easy to maintain.
|
||||||
17
NewFile/src/index.html
Normal file
17
NewFile/src/index.html
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>TechShop - Premium Electronics Marketplace</title>
|
||||||
|
<!-- Google Fonts: Outfit & Inter -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body class="bg-[#F8F9FB] text-[#1E293B] antialiased">
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
5
NewFile/src/next-env.d.ts
vendored
Normal file
5
NewFile/src/next-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||||
14
NewFile/src/next.config.mjs
Normal file
14
NewFile/src/next.config.mjs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: 'images.unsplash.com',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
1679
NewFile/src/package-lock.json
generated
Normal file
1679
NewFile/src/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
31
NewFile/src/package.json
Normal file
31
NewFile/src/package.json
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"name": "techshop-nextjs",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev -p 3000",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"framer-motion": "^11.2.10",
|
||||||
|
"lucide-react": "^0.395.0",
|
||||||
|
"next": "^14.2.4",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"swiper": "^11.1.4",
|
||||||
|
"tailwind-merge": "^2.3.0",
|
||||||
|
"tailwindcss-animate": "^1.0.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.14.9",
|
||||||
|
"@types/react": "^18.3.3",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"autoprefixer": "^10.4.19",
|
||||||
|
"postcss": "^8.4.38",
|
||||||
|
"tailwindcss": "^3.4.4",
|
||||||
|
"typescript": "^5.2.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
NewFile/src/postcss.config.js
Normal file
6
NewFile/src/postcss.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
69
NewFile/src/project.md
Normal file
69
NewFile/src/project.md
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
# Project Configuration - TechShop Next.js App
|
||||||
|
|
||||||
|
This document records the exact technology stack, directory layouts, routing configuration, and implementation structures established for the TechShop electronics e-commerce platform.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Technology Stack
|
||||||
|
- **Framework:** Next.js App Router (14.2)
|
||||||
|
- **Library:** React 18
|
||||||
|
- **Language:** TypeScript
|
||||||
|
- **Styling:** Tailwind CSS (v3) + postcss
|
||||||
|
- **Icons:** Lucide React (`lucide-react`)
|
||||||
|
- **Animation:** Framer Motion (`framer-motion`)
|
||||||
|
- **Carousels:** Swiper.js (`swiper`)
|
||||||
|
- **Utilities:** `clsx` and `tailwind-merge` (CN class resolver helper)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📂 Folder Structure
|
||||||
|
|
||||||
|
The project has been laid out according to Next.js App Router requirements:
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── app/ # Next.js App Router files
|
||||||
|
│ ├── layout.tsx # Root document layout (Google Fonts loader, Metadata SEO)
|
||||||
|
│ ├── page.tsx # Root index route (renders AnnouncementBar, layouts, sections)
|
||||||
|
│ └── globals.css # Core global stylesheet with Tailwind directives
|
||||||
|
├── components/ # Reusable, decoupled Client & Server components
|
||||||
|
│ ├── RatingStars.tsx
|
||||||
|
│ ├── CountdownTimer.tsx
|
||||||
|
│ ├── ProductCard.tsx
|
||||||
|
│ ├── SearchBar.tsx
|
||||||
|
│ ├── CategorySidebar.tsx
|
||||||
|
│ ├── MobileDrawer.tsx
|
||||||
|
│ ├── FloatingButtons.tsx
|
||||||
|
│ └── LiveToast.tsx
|
||||||
|
├── layout/ # Structural wrappers for top/bottom margins
|
||||||
|
│ ├── AnnouncementBar.tsx
|
||||||
|
│ ├── Header.tsx
|
||||||
|
│ ├── Navbar.tsx
|
||||||
|
│ └── Footer.tsx
|
||||||
|
├── sections/ # Contextual homepage sections
|
||||||
|
│ ├── HeroSection.tsx
|
||||||
|
│ ├── PromoCardsSection.tsx
|
||||||
|
│ ├── CategoriesSection.tsx
|
||||||
|
│ ├── NewArrivalsSection.tsx
|
||||||
|
│ ├── PromoBannersSection.tsx
|
||||||
|
│ ├── HotDealsSection.tsx
|
||||||
|
│ ├── RecentlyLaunchedSection.tsx
|
||||||
|
│ ├── BrandsSection.tsx
|
||||||
|
│ ├── UltimateTechSection.tsx
|
||||||
|
│ ├── ArticlesSection.tsx
|
||||||
|
│ ├── FeaturesSection.tsx
|
||||||
|
│ └── NewsletterSection.tsx
|
||||||
|
├── types/ # TypeScript interfaces
|
||||||
|
│ └── index.ts
|
||||||
|
└── utils/ # Helpers and datasets
|
||||||
|
├── utils.ts
|
||||||
|
└── mockData.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 SEO & Accessibility Best Practices
|
||||||
|
- **Heading Hierarchy:** One single `<h1>` tag per page inside the sticky header's logo link. Strategic `<h2>` headings for section triggers (e.g. New Arrivals, Ultimate Tech).
|
||||||
|
- **Metadata Configuration:** Next.js native `Metadata` api exported inside [layout.tsx](file:///e:/ifixKart_monsterTemplate/src/app/layout.tsx) with custom titles, description, and keywords.
|
||||||
|
- **Semantic HTML:** Implemented appropriate HTML5 structural wrappers: `<header>`, `<nav>`, `<main>`, `<section>`, `<article>`, and `<footer>` tags.
|
||||||
|
- **Alt Labels:** Image components contain readable, standard alt properties matching product titles.
|
||||||
106
NewFile/src/src/App.tsx
Normal file
106
NewFile/src/src/App.tsx
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { AnnouncementBar } from './layout/AnnouncementBar';
|
||||||
|
import { Header } from './layout/Header';
|
||||||
|
import { Navbar } from './layout/Navbar';
|
||||||
|
import { Footer } from './layout/Footer';
|
||||||
|
import { MobileDrawer } from './components/MobileDrawer';
|
||||||
|
import { FloatingButtons } from './components/FloatingButtons';
|
||||||
|
import { LiveToast } from './components/LiveToast';
|
||||||
|
|
||||||
|
// Sections
|
||||||
|
import { HeroSection } from './sections/HeroSection';
|
||||||
|
import { PromoCardsSection } from './sections/PromoCardsSection';
|
||||||
|
import { CategoriesSection } from './sections/CategoriesSection';
|
||||||
|
import { NewArrivalsSection } from './sections/NewArrivalsSection';
|
||||||
|
import { PromoBannersSection } from './sections/PromoBannersSection';
|
||||||
|
import { HotDealsSection } from './sections/HotDealsSection';
|
||||||
|
import { RecentlyLaunchedSection } from './sections/RecentlyLaunchedSection';
|
||||||
|
import { BrandsSection } from './sections/BrandsSection';
|
||||||
|
import { UltimateTechSection } from './sections/UltimateTechSection';
|
||||||
|
import { ArticlesSection } from './sections/ArticlesSection';
|
||||||
|
import { FeaturesSection } from './sections/FeaturesSection';
|
||||||
|
import { NewsletterSection } from './sections/NewsletterSection';
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
|
const [wishlistCount, setWishlistCount] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const saved = localStorage.getItem('wishlist');
|
||||||
|
if (saved) {
|
||||||
|
setWishlistCount(JSON.parse(saved).length);
|
||||||
|
} else {
|
||||||
|
const initial: string[] = [];
|
||||||
|
setWishlistCount(initial.length);
|
||||||
|
localStorage.setItem('wishlist', JSON.stringify(initial));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-bgLight text-textMain flex flex-col font-sans">
|
||||||
|
{/* 1. Announcement Bar */}
|
||||||
|
<AnnouncementBar />
|
||||||
|
|
||||||
|
{/* 2. Sticky Header */}
|
||||||
|
<Header
|
||||||
|
onMenuToggle={() => setDrawerOpen(true)}
|
||||||
|
cartCount={0}
|
||||||
|
wishlistCount={wishlistCount}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 3. Navigation Menu */}
|
||||||
|
<Navbar />
|
||||||
|
|
||||||
|
{/* Mobile Drawer (Responsive Hamburger Menu) */}
|
||||||
|
<MobileDrawer isOpen={drawerOpen} onClose={() => setDrawerOpen(false)} />
|
||||||
|
|
||||||
|
{/* Main Content Sections */}
|
||||||
|
<main className="flex-grow">
|
||||||
|
{/* 4. Hero Section (Sidebar + Swiper Slider) */}
|
||||||
|
<HeroSection />
|
||||||
|
|
||||||
|
{/* 5. Promotional Cards */}
|
||||||
|
<PromoCardsSection />
|
||||||
|
|
||||||
|
{/* 6. Shop By Categories */}
|
||||||
|
<CategoriesSection />
|
||||||
|
|
||||||
|
{/* 7. New Arrivals */}
|
||||||
|
<NewArrivalsSection />
|
||||||
|
|
||||||
|
{/* 8. Promotional Banner Row */}
|
||||||
|
<PromoBannersSection />
|
||||||
|
|
||||||
|
{/* 9 & 10. Hot Deals Card & Monthly Featured Products Grid */}
|
||||||
|
<HotDealsSection />
|
||||||
|
|
||||||
|
{/* 11. Recently Launched Carousel */}
|
||||||
|
<RecentlyLaunchedSection />
|
||||||
|
|
||||||
|
{/* 12. Popular Brands Logo Grid */}
|
||||||
|
<BrandsSection />
|
||||||
|
|
||||||
|
{/* 13. Ultimate Tech Gadgets (Grid + Banner) */}
|
||||||
|
<UltimateTechSection />
|
||||||
|
|
||||||
|
{/* 14. Latest Articles Blog Cards */}
|
||||||
|
<ArticlesSection />
|
||||||
|
|
||||||
|
{/* 15. Service Trust Features */}
|
||||||
|
<FeaturesSection />
|
||||||
|
|
||||||
|
{/* 16. Newsletter Box */}
|
||||||
|
<NewsletterSection />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* 17. Footer */}
|
||||||
|
<Footer />
|
||||||
|
|
||||||
|
{/* Floating Buttons: Back-to-Top and Support Chat */}
|
||||||
|
<FloatingButtons />
|
||||||
|
|
||||||
|
{/* Live Purchase notification toasts */}
|
||||||
|
<LiveToast />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
72
NewFile/src/src/app/globals.css
Normal file
72
NewFile/src/src/app/globals.css
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Outfit:wght@300;400;500;600;700;800;900&display=swap');
|
||||||
|
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply font-sans bg-[#F8F9FB] text-[#1E293B] antialiased;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Swiper Styles */
|
||||||
|
.swiper-pagination-bullet-active {
|
||||||
|
background: #1976F3 !important;
|
||||||
|
width: 24px !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swiper-button-next,
|
||||||
|
.swiper-button-prev {
|
||||||
|
color: #1976F3 !important;
|
||||||
|
background: white;
|
||||||
|
width: 40px !important;
|
||||||
|
height: 40px !important;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.swiper-button-next::after,
|
||||||
|
.swiper-button-prev::after {
|
||||||
|
font-size: 16px !important;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide scrollbars but keep scrolling */
|
||||||
|
.no-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.no-scrollbar {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Direct font overrides */
|
||||||
|
body, p, span, a, input, select, button {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6, .font-outfit {
|
||||||
|
font-family: 'Outfit', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Force container max-width to 1400px on large displays */
|
||||||
|
@media (min-width: 1400px) {
|
||||||
|
.container {
|
||||||
|
max-w-screen-2xl: 1400px;
|
||||||
|
max-width: 1400px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prevent browser autofill background color */
|
||||||
|
input:-webkit-autofill,
|
||||||
|
input:-webkit-autofill:hover,
|
||||||
|
input:-webkit-autofill:focus,
|
||||||
|
input:-webkit-autofill:active {
|
||||||
|
-webkit-box-shadow: 0 0 0 30px white inset !important;
|
||||||
|
-webkit-text-fill-color: #1E293B !important;
|
||||||
|
}
|
||||||
32
NewFile/src/src/app/layout.tsx
Normal file
32
NewFile/src/src/app/layout.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Inter, Outfit } from "next/font/google";
|
||||||
|
import "./globals.css";
|
||||||
|
|
||||||
|
const inter = Inter({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-inter",
|
||||||
|
});
|
||||||
|
|
||||||
|
const outfit = Outfit({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-outfit",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "TechShop - Premium Electronics Marketplace",
|
||||||
|
description: "Shop for laptops, smartphones, headphones, smartwatches, and chargers at TechShop, the modern electronics marketplace.",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<html lang="en" className={`${inter.variable} ${outfit.variable}`}>
|
||||||
|
<body className="bg-[#F8F9FB] text-[#1E293B] antialiased">
|
||||||
|
{children}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
146
NewFile/src/src/app/page.tsx
Normal file
146
NewFile/src/src/app/page.tsx
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { AnnouncementBar } from '../layout/AnnouncementBar';
|
||||||
|
import { Header } from '../layout/Header';
|
||||||
|
import { Navbar } from '../layout/Navbar';
|
||||||
|
import { Footer } from '../layout/Footer';
|
||||||
|
import { MobileDrawer } from '../components/MobileDrawer';
|
||||||
|
import { FloatingButtons } from '../components/FloatingButtons';
|
||||||
|
import { LiveToast } from '../components/LiveToast';
|
||||||
|
import { QuickViewModal } from '../components/QuickViewModal';
|
||||||
|
import { WishlistModal } from '../components/WishlistModal';
|
||||||
|
import { Product } from '../types';
|
||||||
|
|
||||||
|
// Sections
|
||||||
|
import { HeroSection } from '../sections/HeroSection';
|
||||||
|
import { PromoCardsSection } from '../sections/PromoCardsSection';
|
||||||
|
import { CategoriesSection } from '../sections/CategoriesSection';
|
||||||
|
import { NewArrivalsSection } from '../sections/NewArrivalsSection';
|
||||||
|
import { PromoBannersSection } from '../sections/PromoBannersSection';
|
||||||
|
import { HotDealsSection } from '../sections/HotDealsSection';
|
||||||
|
import { RecentlyLaunchedSection } from '../sections/RecentlyLaunchedSection';
|
||||||
|
import { BrandsSection } from '../sections/BrandsSection';
|
||||||
|
import { UltimateTechSection } from '../sections/UltimateTechSection';
|
||||||
|
import { ArticlesSection } from '../sections/ArticlesSection';
|
||||||
|
import { FeaturesSection } from '../sections/FeaturesSection';
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
|
const [wishlistCount, setWishlistCount] = useState(0);
|
||||||
|
const [quickViewProduct, setQuickViewProduct] = useState<Product | null>(null);
|
||||||
|
const [wishlistModalOpen, setWishlistModalOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updateWishlist = () => {
|
||||||
|
const saved = localStorage.getItem('wishlist');
|
||||||
|
if (saved) {
|
||||||
|
try {
|
||||||
|
const list = JSON.parse(saved);
|
||||||
|
setWishlistCount(Array.isArray(list) ? list.length : 0);
|
||||||
|
} catch (e) {
|
||||||
|
setWishlistCount(0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setWishlistCount(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
updateWishlist();
|
||||||
|
|
||||||
|
const handleQuickView = (e: Event) => {
|
||||||
|
const customEvt = e as CustomEvent;
|
||||||
|
if (customEvt.detail) {
|
||||||
|
setQuickViewProduct(customEvt.detail);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleWishlistModal = () => setWishlistModalOpen(true);
|
||||||
|
|
||||||
|
window.addEventListener('wishlistUpdated', updateWishlist);
|
||||||
|
window.addEventListener('openQuickView', handleQuickView);
|
||||||
|
window.addEventListener('openWishlistModal', handleWishlistModal);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('wishlistUpdated', updateWishlist);
|
||||||
|
window.removeEventListener('openQuickView', handleQuickView);
|
||||||
|
window.removeEventListener('openWishlistModal', handleWishlistModal);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-bgLight text-textMain flex flex-col font-sans">
|
||||||
|
{/* 1. Announcement Bar */}
|
||||||
|
<AnnouncementBar />
|
||||||
|
|
||||||
|
{/* 2. Sticky Header */}
|
||||||
|
<Header
|
||||||
|
onMenuToggle={() => setDrawerOpen(true)}
|
||||||
|
cartCount={0}
|
||||||
|
wishlistCount={wishlistCount}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 3. Navigation Menu */}
|
||||||
|
<Navbar />
|
||||||
|
|
||||||
|
{/* Mobile Drawer (Responsive Hamburger Menu) */}
|
||||||
|
<MobileDrawer isOpen={drawerOpen} onClose={() => setDrawerOpen(false)} />
|
||||||
|
|
||||||
|
{/* Main Content Sections */}
|
||||||
|
<main className="flex-grow">
|
||||||
|
{/* 4. Hero Section (Sidebar + Swiper Slider) */}
|
||||||
|
<HeroSection />
|
||||||
|
|
||||||
|
{/* 5. Promotional Cards */}
|
||||||
|
<PromoCardsSection />
|
||||||
|
|
||||||
|
{/* 6. Shop By Categories */}
|
||||||
|
<CategoriesSection />
|
||||||
|
|
||||||
|
{/* 7. New Arrivals */}
|
||||||
|
<NewArrivalsSection />
|
||||||
|
|
||||||
|
{/* 8. Promotional Banner Row */}
|
||||||
|
<PromoBannersSection />
|
||||||
|
|
||||||
|
{/* 9 & 10. Hot Deals Card & Monthly Featured Products Grid */}
|
||||||
|
<HotDealsSection />
|
||||||
|
|
||||||
|
{/* 11. Recently Launched Carousel */}
|
||||||
|
<RecentlyLaunchedSection />
|
||||||
|
|
||||||
|
{/* 12. Popular Brands Logo Grid */}
|
||||||
|
<BrandsSection />
|
||||||
|
|
||||||
|
{/* 13. Ultimate Tech Gadgets (Grid + Banner) */}
|
||||||
|
<UltimateTechSection />
|
||||||
|
|
||||||
|
{/* 14. Latest Articles Blog Cards */}
|
||||||
|
<ArticlesSection />
|
||||||
|
|
||||||
|
{/* 15. Service Trust Features */}
|
||||||
|
<FeaturesSection />
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* 17. Footer */}
|
||||||
|
<Footer />
|
||||||
|
|
||||||
|
{/* Floating Buttons: Back-to-Top and Support Chat */}
|
||||||
|
<FloatingButtons />
|
||||||
|
|
||||||
|
{/* Live Purchase notification toasts */}
|
||||||
|
<LiveToast />
|
||||||
|
|
||||||
|
{/* Quick View Modal Popup */}
|
||||||
|
{quickViewProduct && (
|
||||||
|
<QuickViewModal
|
||||||
|
product={quickViewProduct}
|
||||||
|
onClose={() => setQuickViewProduct(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Wishlist Modal Popup */}
|
||||||
|
{wishlistModalOpen && (
|
||||||
|
<WishlistModal onClose={() => setWishlistModalOpen(false)} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
633
NewFile/src/src/app/product/[id]/page.tsx
Normal file
633
NewFile/src/src/app/product/[id]/page.tsx
Normal file
|
|
@ -0,0 +1,633 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useParams } from 'next/navigation';
|
||||||
|
import { Header } from '../../../layout/Header';
|
||||||
|
import { Navbar } from '../../../layout/Navbar';
|
||||||
|
import { AnnouncementBar } from '../../../layout/AnnouncementBar';
|
||||||
|
import { Footer } from '../../../layout/Footer';
|
||||||
|
import { MobileDrawer } from '../../../components/MobileDrawer';
|
||||||
|
import { RatingStars } from '../../../components/RatingStars';
|
||||||
|
import { ProductCard } from '../../../components/ProductCard';
|
||||||
|
import { products } from '../../../utils/mockData';
|
||||||
|
import { Product } from '../../../types';
|
||||||
|
import {
|
||||||
|
Heart,
|
||||||
|
Eye,
|
||||||
|
ShoppingCart,
|
||||||
|
ChevronUp,
|
||||||
|
ChevronDown,
|
||||||
|
Flame,
|
||||||
|
CheckCircle2,
|
||||||
|
Share2,
|
||||||
|
HelpCircle,
|
||||||
|
GitCompare,
|
||||||
|
ShieldCheck,
|
||||||
|
Coins,
|
||||||
|
Truck,
|
||||||
|
ChevronRight
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
export default function ProductDetailPage() {
|
||||||
|
const params = useParams();
|
||||||
|
const productId = (params?.id as string) || 'prod-2';
|
||||||
|
|
||||||
|
// State management
|
||||||
|
const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false);
|
||||||
|
const [cartCount, setCartCount] = useState(0);
|
||||||
|
const [wishlist, setWishlist] = useState<string[]>([]);
|
||||||
|
const [activeTab, setActiveTab] = useState<'desc' | 'addInfo' | 'reviews' | 'sizeChart' | 'imageTab' | 'shipping'>('desc');
|
||||||
|
const [quantity, setQuantity] = useState(1);
|
||||||
|
const [selectedColorIndex, setSelectedColorIndex] = useState<number | null>(0);
|
||||||
|
const [showStickyBar, setShowStickyBar] = useState(false);
|
||||||
|
|
||||||
|
// Magnifier Lens Zoom state
|
||||||
|
const [zoomPos, setZoomPos] = useState({ x: 0, y: 0 });
|
||||||
|
const [isZooming, setIsZooming] = useState(false);
|
||||||
|
const imageRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Find product by id or fallback to Apple HomePod
|
||||||
|
const currentProduct: Product =
|
||||||
|
products.find((p: Product) => p.id === productId) || products[1] || products[0];
|
||||||
|
|
||||||
|
// Gallery image list
|
||||||
|
const colorGallery = [
|
||||||
|
currentProduct.image,
|
||||||
|
currentProduct.imageHover || currentProduct.image,
|
||||||
|
'https://images.unsplash.com/photo-1543512214-318c7553f230?w=500&auto=format&fit=crop&q=60',
|
||||||
|
'https://images.unsplash.com/photo-1588449668365-d15e397f6787?w=500&auto=format&fit=crop&q=60',
|
||||||
|
'https://images.unsplash.com/photo-1508685096489-7aacd43bd3b1?w=500&auto=format&fit=crop&q=60',
|
||||||
|
];
|
||||||
|
|
||||||
|
const [selectedImage, setSelectedImage] = useState(colorGallery[0]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Load initial wishlist and cart count from localStorage
|
||||||
|
const savedWishlist = localStorage.getItem('wishlist');
|
||||||
|
if (savedWishlist) setWishlist(JSON.parse(savedWishlist));
|
||||||
|
|
||||||
|
const savedCart = localStorage.getItem('cart');
|
||||||
|
if (savedCart) {
|
||||||
|
const parsed = JSON.parse(savedCart);
|
||||||
|
setCartCount(Array.isArray(parsed) ? parsed.length : Number(parsed));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll listener for sticky bottom bar
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (window.scrollY > 500) {
|
||||||
|
setShowStickyBar(true);
|
||||||
|
} else {
|
||||||
|
setShowStickyBar(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('scroll', handleScroll);
|
||||||
|
return () => window.removeEventListener('scroll', handleScroll);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const isWishlisted = wishlist.includes(currentProduct.id);
|
||||||
|
|
||||||
|
const toggleWishlist = () => {
|
||||||
|
let updated: string[];
|
||||||
|
if (isWishlisted) {
|
||||||
|
updated = wishlist.filter((id) => id !== currentProduct.id);
|
||||||
|
} else {
|
||||||
|
updated = [...wishlist, currentProduct.id];
|
||||||
|
}
|
||||||
|
setWishlist(updated);
|
||||||
|
localStorage.setItem('wishlist', JSON.stringify(updated));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddToCart = () => {
|
||||||
|
const newCount = cartCount + quantity;
|
||||||
|
setCartCount(newCount);
|
||||||
|
localStorage.setItem('cart', JSON.stringify(newCount));
|
||||||
|
alert(`Added ${quantity} x "${currentProduct.name}" to cart!`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Image Magnifier Mouse Handler
|
||||||
|
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (!imageRef.current) return;
|
||||||
|
const { left, top, width, height } = imageRef.current.getBoundingClientRect();
|
||||||
|
const x = ((e.clientX - left) / width) * 100;
|
||||||
|
const y = ((e.clientY - top) / height) * 100;
|
||||||
|
setZoomPos({ x, y });
|
||||||
|
};
|
||||||
|
|
||||||
|
const isRange = currentProduct.name.includes('Ceptics') || currentProduct.name.includes('Lenovo');
|
||||||
|
const priceDisplay = isRange ? `$300 – $1,500` : `$${currentProduct.price.toLocaleString()}`;
|
||||||
|
|
||||||
|
// Comparison products
|
||||||
|
const comparisonProducts = products.slice(1, 6);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-white font-sans text-gray-800 antialiased pb-20">
|
||||||
|
{/* Announcement & Main Header */}
|
||||||
|
<AnnouncementBar />
|
||||||
|
<Header
|
||||||
|
onMenuToggle={() => setMobileDrawerOpen(true)}
|
||||||
|
cartCount={cartCount}
|
||||||
|
wishlistCount={wishlist.length}
|
||||||
|
/>
|
||||||
|
<Navbar />
|
||||||
|
|
||||||
|
<MobileDrawer
|
||||||
|
isOpen={mobileDrawerOpen}
|
||||||
|
onClose={() => setMobileDrawerOpen(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Breadcrumb Navigation */}
|
||||||
|
<div className="bg-[#F8F9FA] border-b border-gray-150 py-3 text-xs text-gray-500">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 flex items-center gap-2">
|
||||||
|
<Link href="/" className="hover:text-primary transition-colors">Home</Link>
|
||||||
|
<ChevronRight size={12} />
|
||||||
|
<Link href="/shop" className="hover:text-primary transition-colors">Shop</Link>
|
||||||
|
<ChevronRight size={12} />
|
||||||
|
<span className="text-gray-400 capitalize">{currentProduct.category}</span>
|
||||||
|
<ChevronRight size={12} />
|
||||||
|
<span className="text-gray-900 font-semibold truncate max-w-[250px]">{currentProduct.name}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Product Showcase Section */}
|
||||||
|
<main className="container mx-auto px-4 sm:px-6 lg:px-8 py-8 md:py-12">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 lg:gap-12 items-start">
|
||||||
|
|
||||||
|
{/* Left Column: Image Gallery & Zoom Lens */}
|
||||||
|
<div className="lg:col-span-6 flex flex-col md:flex-row gap-4">
|
||||||
|
|
||||||
|
{/* Vertical Thumbnail List */}
|
||||||
|
<div className="flex md:flex-col items-center gap-2 order-2 md:order-1 flex-shrink-0 justify-center">
|
||||||
|
<button className="text-gray-400 hover:text-primary hidden md:block">
|
||||||
|
<ChevronUp size={16} />
|
||||||
|
</button>
|
||||||
|
{colorGallery.map((img, idx) => (
|
||||||
|
<button
|
||||||
|
key={idx}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedImage(img);
|
||||||
|
setSelectedColorIndex(idx);
|
||||||
|
}}
|
||||||
|
className={`w-16 h-16 rounded-xl border p-1 transition-all overflow-hidden flex items-center justify-center bg-[#F8F9FB] ${
|
||||||
|
selectedImage === img ? 'border-[#0073EC] ring-2 ring-[#0073EC]/20' : 'border-gray-200 hover:border-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<img src={img} alt="" className="max-h-full max-w-full object-contain" />
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
<button className="text-gray-400 hover:text-primary hidden md:block">
|
||||||
|
<ChevronDown size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Stage Image with Magnifier Lens */}
|
||||||
|
<div className="flex-1 order-1 md:order-2">
|
||||||
|
<div
|
||||||
|
ref={imageRef}
|
||||||
|
onMouseEnter={() => setIsZooming(true)}
|
||||||
|
onMouseLeave={() => setIsZooming(false)}
|
||||||
|
onMouseMove={handleMouseMove}
|
||||||
|
className="relative bg-white border border-gray-150 rounded-2xl p-6 h-[420px] md:h-[480px] flex items-center justify-center overflow-hidden cursor-crosshair group select-none shadow-xs"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={selectedImage}
|
||||||
|
alt={currentProduct.name}
|
||||||
|
className="max-h-full max-w-full object-contain transition-transform duration-200"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Magnifier Lens Window overlay */}
|
||||||
|
{isZooming && (
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 pointer-events-none z-30 bg-white"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url(${selectedImage})`,
|
||||||
|
backgroundPosition: `${zoomPos.x}% ${zoomPos.y}%`,
|
||||||
|
backgroundSize: '250%',
|
||||||
|
backgroundRepeat: 'no-repeat'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Trust Badges under Main Image */}
|
||||||
|
<div className="grid grid-cols-3 gap-2 mt-4">
|
||||||
|
<div className="bg-[#F6F8FE] border border-gray-150 rounded-xl p-3 flex items-center justify-center gap-2 text-xs font-semibold text-gray-700">
|
||||||
|
<ShieldCheck size={16} className="text-[#0073EC]" />
|
||||||
|
<span>101% Original</span>
|
||||||
|
</div>
|
||||||
|
<div className="bg-[#F6F8FE] border border-gray-150 rounded-xl p-3 flex items-center justify-center gap-2 text-xs font-semibold text-gray-700">
|
||||||
|
<Coins size={16} className="text-[#0073EC]" />
|
||||||
|
<span>Lowest Price</span>
|
||||||
|
</div>
|
||||||
|
<div className="bg-[#F6F8FE] border border-gray-150 rounded-xl p-3 flex items-center justify-center gap-2 text-xs font-semibold text-gray-700">
|
||||||
|
<Truck size={16} className="text-[#0073EC]" />
|
||||||
|
<span>Free Shipping</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Column: Product Info & Purchase Form */}
|
||||||
|
<div className="lg:col-span-6 text-left">
|
||||||
|
{/* Brand Tag */}
|
||||||
|
<div className="text-xs font-bold text-[#0073EC] uppercase tracking-wider mb-1">
|
||||||
|
Brand: {currentProduct.brand || 'Gold'}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Product Title */}
|
||||||
|
<h1 className="text-2xl md:text-3xl font-extrabold text-gray-900 tracking-tight leading-tight mb-3">
|
||||||
|
{currentProduct.name}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{/* Price & Rating */}
|
||||||
|
<div className="flex items-center gap-4 mb-4">
|
||||||
|
<span className="text-2xl font-black text-gray-900">{priceDisplay}</span>
|
||||||
|
<div className="flex items-center gap-1.5 text-xs text-gray-500 border-l border-gray-200 pl-4">
|
||||||
|
<RatingStars rating={currentProduct.rating} size={14} />
|
||||||
|
<span className="font-semibold">(1 review)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Urgency Selling Fast Banner */}
|
||||||
|
<div className="inline-flex items-center gap-2 bg-red-50 text-red-500 border border-red-100 rounded-lg px-3 py-1.5 text-xs font-bold mb-5">
|
||||||
|
<Flame size={14} className="fill-red-500 animate-pulse" />
|
||||||
|
<span>Selling fast! Over 18 people have in their cart</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bullet Point Specifications */}
|
||||||
|
<ul className="text-xs text-gray-600 space-y-1.5 mb-6 list-disc list-inside font-medium">
|
||||||
|
<li>Gps+cellular</li>
|
||||||
|
<li>Noise Cancellation</li>
|
||||||
|
<li>Google Fast Pair</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{/* Color Swatch Options */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="text-xs font-bold text-gray-700">Color</span>
|
||||||
|
{selectedColorIndex !== null && (
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedColorIndex(null)}
|
||||||
|
className="text-xs text-gray-400 hover:text-red-500 transition-colors"
|
||||||
|
>
|
||||||
|
✕ Clear
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{colorGallery.slice(0, 5).map((img, idx) => (
|
||||||
|
<button
|
||||||
|
key={idx}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedImage(img);
|
||||||
|
setSelectedColorIndex(idx);
|
||||||
|
}}
|
||||||
|
className={`w-10 h-10 rounded-lg border p-1 transition-all overflow-hidden flex items-center justify-center bg-[#F8F9FB] ${
|
||||||
|
selectedColorIndex === idx ? 'border-[#0073EC] ring-2 ring-[#0073EC]/20' : 'border-gray-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<img src={img} alt="" className="max-h-full max-w-full object-contain" />
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quantity Selector & Action Buttons */}
|
||||||
|
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 mb-4">
|
||||||
|
{/* Quantity Counter */}
|
||||||
|
<div className="flex items-center border border-gray-200 rounded-xl bg-[#F8F9FB] px-3 py-2.5 justify-between sm:w-32">
|
||||||
|
<button
|
||||||
|
onClick={() => setQuantity(Math.max(1, quantity - 1))}
|
||||||
|
className="text-gray-500 hover:text-black font-extrabold text-sm px-2 select-none"
|
||||||
|
>
|
||||||
|
–
|
||||||
|
</button>
|
||||||
|
<span className="font-bold text-sm text-gray-900">{quantity}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setQuantity(quantity + 1)}
|
||||||
|
className="text-gray-500 hover:text-black font-extrabold text-sm px-2 select-none"
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Add to Cart Button */}
|
||||||
|
<button
|
||||||
|
onClick={handleAddToCart}
|
||||||
|
className="flex-1 bg-[#0073EC] hover:bg-[#005FC8] text-white font-extrabold text-xs uppercase tracking-wider py-3.5 px-6 rounded-xl transition-all shadow-md active:scale-[0.99] flex items-center justify-center gap-2 cursor-pointer"
|
||||||
|
>
|
||||||
|
<ShoppingCart size={15} className="stroke-[2.5]" />
|
||||||
|
Add To Cart
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Buy Now Button */}
|
||||||
|
<button
|
||||||
|
onClick={() => alert(`Proceeding to checkout for ${currentProduct.name}`)}
|
||||||
|
className="w-full bg-[#0073EC] hover:bg-[#005FC8] text-white font-extrabold text-xs uppercase tracking-wider py-3.5 px-6 rounded-xl transition-all shadow-md active:scale-[0.99] mb-6 cursor-pointer"
|
||||||
|
>
|
||||||
|
Buy Now
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Action Links Row */}
|
||||||
|
<div className="flex items-center gap-6 border-b border-gray-150 pb-6 mb-6 text-xs font-semibold text-gray-600">
|
||||||
|
<button className="flex items-center gap-1.5 hover:text-[#0073EC] transition-colors">
|
||||||
|
<GitCompare size={14} />
|
||||||
|
<span>Compare</span>
|
||||||
|
</button>
|
||||||
|
<button onClick={toggleWishlist} className={`flex items-center gap-1.5 transition-colors ${isWishlisted ? 'text-[#0073EC]' : 'hover:text-[#0073EC]'}`}>
|
||||||
|
<Heart size={14} className={isWishlisted ? 'fill-[#0073EC]' : ''} />
|
||||||
|
<span>Wishlist</span>
|
||||||
|
</button>
|
||||||
|
<button className="flex items-center gap-1.5 hover:text-[#0073EC] transition-colors">
|
||||||
|
<HelpCircle size={14} />
|
||||||
|
<span>Ask Us</span>
|
||||||
|
</button>
|
||||||
|
<button className="flex items-center gap-1.5 hover:text-[#0073EC] transition-colors">
|
||||||
|
<Share2 size={14} />
|
||||||
|
<span>Share</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Live Viewers Indicator */}
|
||||||
|
<div className="flex items-center gap-2 text-xs font-bold text-gray-800 mb-4">
|
||||||
|
<Eye size={15} className="text-gray-500" />
|
||||||
|
<span>26 people are viewing this right now</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Shipping & Returns Details */}
|
||||||
|
<div className="space-y-2 text-xs text-gray-600 font-medium mb-6">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CheckCircle2 size={14} className="text-emerald-500" />
|
||||||
|
<span><strong>Estimated Delivery :</strong> Up to 4 business days</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CheckCircle2 size={14} className="text-emerald-500" />
|
||||||
|
<span><strong>Free Shipping & Returns :</strong> On all orders over $200</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Safe Checkout Badges */}
|
||||||
|
<div className="bg-[#F8F9FB] border border-gray-150 rounded-xl p-4 text-center">
|
||||||
|
<p className="text-[11px] font-bold text-gray-500 uppercase tracking-wider mb-2">
|
||||||
|
Guaranteed Safe And Secure Checkout
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center justify-center gap-3 text-xs font-extrabold text-gray-700">
|
||||||
|
<span className="px-2 py-1 bg-white rounded border border-gray-200">VISA</span>
|
||||||
|
<span className="px-2 py-1 bg-white rounded border border-gray-200">MC</span>
|
||||||
|
<span className="px-2 py-1 bg-white rounded border border-gray-200">AMEX</span>
|
||||||
|
<span className="px-2 py-1 bg-white rounded border border-gray-200">DISCOVER</span>
|
||||||
|
<span className="px-2 py-1 bg-white rounded border border-gray-200">PayPal</span>
|
||||||
|
<span className="px-2 py-1 bg-white rounded border border-gray-200">Pay</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* Tabs & Detailed Specifications Section */}
|
||||||
|
<section className="container mx-auto px-4 sm:px-6 lg:px-8 my-12">
|
||||||
|
<div className="border border-gray-150 rounded-2xl overflow-hidden bg-white shadow-xs">
|
||||||
|
|
||||||
|
{/* Tab Navigation Header */}
|
||||||
|
<div className="flex items-center gap-2 border-b border-gray-150 px-6 pt-4 bg-[#F8F9FB] overflow-x-auto">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('desc')}
|
||||||
|
className={`pb-4 text-xs font-extrabold px-3 transition-colors border-b-2 whitespace-nowrap cursor-pointer ${
|
||||||
|
activeTab === 'desc' ? 'border-[#0073EC] text-[#0073EC]' : 'border-transparent text-gray-500 hover:text-gray-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Description
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('addInfo')}
|
||||||
|
className={`pb-4 text-xs font-extrabold px-3 transition-colors border-b-2 whitespace-nowrap cursor-pointer ${
|
||||||
|
activeTab === 'addInfo' ? 'border-[#0073EC] text-[#0073EC]' : 'border-transparent text-gray-500 hover:text-gray-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Additional information
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('reviews')}
|
||||||
|
className={`pb-4 text-xs font-extrabold px-3 transition-colors border-b-2 whitespace-nowrap cursor-pointer ${
|
||||||
|
activeTab === 'reviews' ? 'border-[#0073EC] text-[#0073EC]' : 'border-transparent text-gray-500 hover:text-gray-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Reviews(1)
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('sizeChart')}
|
||||||
|
className={`pb-4 text-xs font-extrabold px-3 transition-colors border-b-2 whitespace-nowrap cursor-pointer ${
|
||||||
|
activeTab === 'sizeChart' ? 'border-[#0073EC] text-[#0073EC]' : 'border-transparent text-gray-500 hover:text-gray-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Size Chart
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('imageTab')}
|
||||||
|
className={`pb-4 text-xs font-extrabold px-3 transition-colors border-b-2 whitespace-nowrap cursor-pointer ${
|
||||||
|
activeTab === 'imageTab' ? 'border-[#0073EC] text-[#0073EC]' : 'border-transparent text-gray-500 hover:text-gray-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Image Tab
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('shipping')}
|
||||||
|
className={`pb-4 text-xs font-extrabold px-3 transition-colors border-b-2 whitespace-nowrap cursor-pointer ${
|
||||||
|
activeTab === 'shipping' ? 'border-[#0073EC] text-[#0073EC]' : 'border-transparent text-gray-500 hover:text-gray-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Shipping & Return
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab Content Box */}
|
||||||
|
<div className="p-6 md:p-8 text-left">
|
||||||
|
{(activeTab === 'desc' || activeTab === 'addInfo') && (
|
||||||
|
<div className="space-y-6 text-xs text-gray-700">
|
||||||
|
|
||||||
|
{/* GENERAL INFORMATION */}
|
||||||
|
<div>
|
||||||
|
<h4 className="text-xs font-extrabold text-[#0073EC] uppercase tracking-wider mb-3 bg-gray-50 p-2 border-l-4 border-[#0073EC]">
|
||||||
|
GENERAL INFORMATION
|
||||||
|
</h4>
|
||||||
|
<table className="w-full border border-gray-150 divide-y divide-gray-150 text-left">
|
||||||
|
<tbody>
|
||||||
|
<tr className="bg-white">
|
||||||
|
<td className="p-3 font-semibold text-gray-900 w-1/3 border-r border-gray-150">Model</td>
|
||||||
|
<td className="p-3 text-gray-600">HomePod 2nd Generation</td>
|
||||||
|
</tr>
|
||||||
|
<tr className="bg-gray-50/50">
|
||||||
|
<td className="p-3 font-semibold text-gray-900 w-1/3 border-r border-gray-150">Wireless Connectivity</td>
|
||||||
|
<td className="p-3 text-gray-600">Bluetooth, Wi-Fi 802.11n</td>
|
||||||
|
</tr>
|
||||||
|
<tr className="bg-white">
|
||||||
|
<td className="p-3 font-semibold text-gray-900 w-1/3 border-r border-gray-150">Brand</td>
|
||||||
|
<td className="p-3 text-gray-600">{currentProduct.brand || 'Apple'}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* PHYSICAL DIMENSIONS */}
|
||||||
|
<div>
|
||||||
|
<h4 className="text-xs font-extrabold text-[#0073EC] uppercase tracking-wider mb-3 bg-gray-50 p-2 border-l-4 border-[#0073EC]">
|
||||||
|
PHYSICAL DIMENSIONS
|
||||||
|
</h4>
|
||||||
|
<table className="w-full border border-gray-150 divide-y divide-gray-150 text-left">
|
||||||
|
<tbody>
|
||||||
|
<tr className="bg-white">
|
||||||
|
<td className="p-3 font-semibold text-gray-900 w-1/3 border-r border-gray-150">Colour</td>
|
||||||
|
<td className="p-3 text-gray-600">White / Midnight</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ADDITIONAL FEATURES */}
|
||||||
|
<div>
|
||||||
|
<h4 className="text-xs font-extrabold text-[#0073EC] uppercase tracking-wider mb-3 bg-gray-50 p-2 border-l-4 border-[#0073EC]">
|
||||||
|
ADDITIONAL FEATURES
|
||||||
|
</h4>
|
||||||
|
<ul className="list-disc list-inside space-y-1 text-gray-600 pl-2">
|
||||||
|
<li>Touch Accommodations VoiceOver Home app and HomeKit Intercom Transcriptions</li>
|
||||||
|
<li>Compatibility: iPhone 14 Pro Max, iPhone 14 Pro, iPhone 14 Plus, iPhone 14</li>
|
||||||
|
<li>iPhone 13 Pro Max, iPhone 13 Pro, iPhone 13, iPhone 13 mini</li>
|
||||||
|
<li>iPhone 12 Pro Max, iPhone 12 Pro</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'reviews' && (
|
||||||
|
<div className="text-xs text-gray-600 space-y-4">
|
||||||
|
<div className="flex items-center gap-3 p-4 bg-gray-50 rounded-xl border border-gray-150">
|
||||||
|
<RatingStars rating={5} size={14} />
|
||||||
|
<div>
|
||||||
|
<p className="font-bold text-gray-900">John Doe — July 20, 2026</p>
|
||||||
|
<p className="text-gray-600 mt-1">Excellent sound quality and fast delivery!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'shipping' && (
|
||||||
|
<div className="text-xs text-gray-600 space-y-2">
|
||||||
|
<p><strong>Standard Shipping:</strong> 2-4 business days.</p>
|
||||||
|
<p><strong>Return Policy:</strong> 30-day money back guarantee for any defective products.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Quick Comparison Section */}
|
||||||
|
<section className="container mx-auto px-4 sm:px-6 lg:px-8 my-12 text-left">
|
||||||
|
<h3 className="text-xl font-extrabold text-gray-900 tracking-tight mb-6">
|
||||||
|
Quick Comparison
|
||||||
|
</h3>
|
||||||
|
<div className="border border-gray-150 rounded-2xl overflow-x-auto bg-white shadow-xs">
|
||||||
|
<table className="w-full text-xs text-left border-collapse min-w-[700px]">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-[#F8F9FB] border-b border-gray-150">
|
||||||
|
<th className="p-4 font-bold text-gray-500 w-32 border-r border-gray-150">Products</th>
|
||||||
|
{comparisonProducts.map((prod: Product) => (
|
||||||
|
<th key={prod.id} className="p-4 font-bold text-gray-900 border-r border-gray-150 last:border-r-0 min-w-[160px]">
|
||||||
|
<p className="line-clamp-2">{prod.name}</p>
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-150">
|
||||||
|
<tr>
|
||||||
|
<td className="p-4 font-bold text-gray-500 bg-[#F8F9FB] border-r border-gray-150">Image</td>
|
||||||
|
{comparisonProducts.map((prod: Product) => (
|
||||||
|
<td key={prod.id} className="p-4 border-r border-gray-150 last:border-r-0 text-center">
|
||||||
|
<div className="w-24 h-24 mx-auto p-2 bg-[#F8F9FB] rounded-lg flex items-center justify-center">
|
||||||
|
<img src={prod.image} alt={prod.name} className="max-h-full max-w-full object-contain" />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="p-4 font-bold text-gray-500 bg-[#F8F9FB] border-r border-gray-150">Rating</td>
|
||||||
|
{comparisonProducts.map((prod: Product) => (
|
||||||
|
<td key={prod.id} className="p-4 border-r border-gray-150 last:border-r-0">
|
||||||
|
<RatingStars rating={prod.rating} size={12} />
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="p-4 font-bold text-gray-500 bg-[#F8F9FB] border-r border-gray-150">Price</td>
|
||||||
|
{comparisonProducts.map((prod: Product) => (
|
||||||
|
<td key={prod.id} className="p-4 font-extrabold text-gray-900 border-r border-gray-150 last:border-r-0">
|
||||||
|
${prod.price}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="p-4 font-bold text-gray-500 bg-[#F8F9FB] border-r border-gray-150">Add to cart</td>
|
||||||
|
{comparisonProducts.map((prod: Product) => (
|
||||||
|
<td key={prod.id} className="p-4 border-r border-gray-150 last:border-r-0">
|
||||||
|
<button
|
||||||
|
onClick={() => alert(`Added ${prod.name} to cart!`)}
|
||||||
|
className="w-full bg-[#0073EC] hover:bg-[#005FC8] text-white text-[11px] font-bold py-2.5 px-3 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Select Options
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Related Products Grid Section */}
|
||||||
|
<section className="container mx-auto px-4 sm:px-6 lg:px-8 my-12 text-left">
|
||||||
|
<h3 className="text-xl font-extrabold text-gray-900 tracking-tight mb-6">
|
||||||
|
Related products
|
||||||
|
</h3>
|
||||||
|
<div className="bg-white border-t border-l border-gray-200 rounded-none shadow-sm overflow-hidden grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 lg:grid-cols-5">
|
||||||
|
{products.slice(2, 7).map((prod: Product) => (
|
||||||
|
<ProductCard key={prod.id} product={prod} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Sticky Bottom Purchase Bar */}
|
||||||
|
{showStickyBar && (
|
||||||
|
<div className="fixed bottom-0 left-0 right-0 z-[100] bg-white border-t border-gray-200 py-3 px-4 sm:px-8 shadow-2xl transition-all duration-300 flex items-center justify-between gap-4">
|
||||||
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
|
<img
|
||||||
|
src={selectedImage}
|
||||||
|
alt=""
|
||||||
|
className="w-10 h-10 object-contain p-1 bg-gray-50 border border-gray-200 rounded-lg flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<p className="text-xs font-bold text-gray-900 truncate max-w-[280px] sm:max-w-[400px]">
|
||||||
|
{currentProduct.name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<span className="text-sm font-extrabold text-gray-900 hidden sm:inline-block">
|
||||||
|
{priceDisplay}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={handleAddToCart}
|
||||||
|
className="bg-[#0073EC] hover:bg-[#005FC8] text-white font-extrabold text-xs uppercase tracking-wider py-2.5 px-5 rounded-xl transition-all shadow-sm cursor-pointer whitespace-nowrap"
|
||||||
|
>
|
||||||
|
Select Options
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Main Footer */}
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
900
NewFile/src/src/app/shop/page.tsx
Normal file
900
NewFile/src/src/app/shop/page.tsx
Normal file
|
|
@ -0,0 +1,900 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import {
|
||||||
|
Heart,
|
||||||
|
Eye,
|
||||||
|
ShoppingCart,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronUp,
|
||||||
|
Grid,
|
||||||
|
List,
|
||||||
|
ChevronRight,
|
||||||
|
Search,
|
||||||
|
Star,
|
||||||
|
X
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
import { AnnouncementBar } from '../../layout/AnnouncementBar';
|
||||||
|
import { Header } from '../../layout/Header';
|
||||||
|
import { Navbar } from '../../layout/Navbar';
|
||||||
|
import { Footer } from '../../layout/Footer';
|
||||||
|
import { MobileDrawer } from '../../components/MobileDrawer';
|
||||||
|
import { FloatingButtons } from '../../components/FloatingButtons';
|
||||||
|
import { LiveToast } from '../../components/LiveToast';
|
||||||
|
import { QuickViewModal } from '../../components/QuickViewModal';
|
||||||
|
import { WishlistModal } from '../../components/WishlistModal';
|
||||||
|
|
||||||
|
import { products as initialProducts } from '../../utils/mockData';
|
||||||
|
import { Product } from '../../types';
|
||||||
|
|
||||||
|
export default function ShopPage() {
|
||||||
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
|
const [layout, setLayout] = useState<'grid' | 'list'>('grid');
|
||||||
|
const [sortBy, setSortBy] = useState('default');
|
||||||
|
const [wishlist, setWishlist] = useState<string[]>([]);
|
||||||
|
const [quickViewProduct, setQuickViewProduct] = useState<Product | null>(null);
|
||||||
|
const [wishlistModalOpen, setWishlistModalOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const saved = localStorage.getItem('wishlist');
|
||||||
|
if (saved) {
|
||||||
|
setWishlist(JSON.parse(saved));
|
||||||
|
} else {
|
||||||
|
const initial: string[] = [];
|
||||||
|
setWishlist(initial);
|
||||||
|
localStorage.setItem('wishlist', JSON.stringify(initial));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleWishlist = (productId: string) => {
|
||||||
|
setWishlist(prev => {
|
||||||
|
const exists = prev.includes(productId);
|
||||||
|
let updated;
|
||||||
|
if (exists) {
|
||||||
|
updated = prev.filter(id => id !== productId);
|
||||||
|
} else {
|
||||||
|
setWishlistModalOpen(true);
|
||||||
|
updated = [...prev, productId];
|
||||||
|
}
|
||||||
|
localStorage.setItem('wishlist', JSON.stringify(updated));
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filter States
|
||||||
|
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
|
||||||
|
const [selectedBrands, setSelectedBrands] = useState<string[]>([]);
|
||||||
|
const [selectedHighlight, setSelectedHighlight] = useState<string>('All Products');
|
||||||
|
const [selectedColor, setSelectedColor] = useState<string | null>(null);
|
||||||
|
const [selectedPriceRange, setSelectedPriceRange] = useState<string>('All');
|
||||||
|
const [selectedRating, setSelectedRating] = useState<number | null>(null);
|
||||||
|
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||||
|
|
||||||
|
// Collapsible Sidebar States
|
||||||
|
const [collapsedFilters, setCollapsedFilters] = useState({
|
||||||
|
categories: false,
|
||||||
|
brands: false,
|
||||||
|
highlight: false,
|
||||||
|
color: false,
|
||||||
|
price: false,
|
||||||
|
rating: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleCollapse = (filterKey: keyof typeof collapsedFilters) => {
|
||||||
|
setCollapsedFilters(prev => ({
|
||||||
|
...prev,
|
||||||
|
[filterKey]: !prev[filterKey]
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sync category & search filters from URL if present
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const catParam = params.get('category');
|
||||||
|
const layoutParam = params.get('layout');
|
||||||
|
const searchParam = params.get('search');
|
||||||
|
if (catParam) {
|
||||||
|
setSelectedCategories([catParam]);
|
||||||
|
}
|
||||||
|
if (layoutParam === 'list' || layoutParam === 'grid') {
|
||||||
|
setLayout(layoutParam);
|
||||||
|
}
|
||||||
|
if (searchParam) {
|
||||||
|
setSearchQuery(searchParam);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Filter & Sort Logic
|
||||||
|
const filteredProducts = initialProducts.filter(product => {
|
||||||
|
// Search Filter
|
||||||
|
if (searchQuery && !product.name.toLowerCase().includes(searchQuery.toLowerCase()) && !product.category.toLowerCase().includes(searchQuery.toLowerCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Category Filter
|
||||||
|
if (selectedCategories.length > 0 && !selectedCategories.includes(product.category)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Brand Filter
|
||||||
|
if (selectedBrands.length > 0 && product.brand && !selectedBrands.includes(product.brand)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highlight Filter
|
||||||
|
if (selectedHighlight !== 'All Products') {
|
||||||
|
if (selectedHighlight === 'Best Seller' && !product.isFeatured) return false;
|
||||||
|
if (selectedHighlight === 'New Arrivals' && !product.isNewArrival) return false;
|
||||||
|
if (selectedHighlight === 'Sale' && !product.discount) return false;
|
||||||
|
if (selectedHighlight === 'Hot Items' && !product.isHot) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Color Filter
|
||||||
|
if (selectedColor && product.color !== selectedColor) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Price Filter
|
||||||
|
if (selectedPriceRange !== 'All') {
|
||||||
|
const price = product.price;
|
||||||
|
if (selectedPriceRange === '$0-$400' && (price < 0 || price > 400)) return false;
|
||||||
|
if (selectedPriceRange === '$400-$800' && (price < 400 || price > 800)) return false;
|
||||||
|
if (selectedPriceRange === '$800-$1,200' && (price < 800 || price > 1200)) return false;
|
||||||
|
if (selectedPriceRange === '$1,200-$1,650' && (price < 1200 || price > 1650)) return false; // Adjusted upper range to capture laptops
|
||||||
|
if (selectedPriceRange === '$1,600+' && price < 1600) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rating Filter
|
||||||
|
if (selectedRating && Math.floor(product.rating) !== selectedRating) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}).sort((a, b) => {
|
||||||
|
if (sortBy === 'price-low') return a.price - b.price;
|
||||||
|
if (sortBy === 'price-high') return b.price - a.price;
|
||||||
|
if (sortBy === 'rating') return b.rating - a.rating;
|
||||||
|
return 0; // default (no sorting)
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleCategoryChange = (category: string) => {
|
||||||
|
setSelectedCategories(prev =>
|
||||||
|
prev.includes(category)
|
||||||
|
? prev.filter(c => c !== category)
|
||||||
|
: [...prev, category]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBrandChange = (brand: string) => {
|
||||||
|
setSelectedBrands(prev =>
|
||||||
|
prev.includes(brand)
|
||||||
|
? prev.filter(b => b !== brand)
|
||||||
|
: [...prev, brand]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetAllFilters = () => {
|
||||||
|
setSelectedCategories([]);
|
||||||
|
setSelectedBrands([]);
|
||||||
|
setSelectedHighlight('All Products');
|
||||||
|
setSelectedColor(null);
|
||||||
|
setSelectedPriceRange('All');
|
||||||
|
setSelectedRating(null);
|
||||||
|
setSearchQuery('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRatingCount = (stars: number) => {
|
||||||
|
return initialProducts.filter(p => Math.floor(p.rating) === stars).length;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Price label resolver (to match mockup price displays)
|
||||||
|
const getProductPriceDisplay = (product: Product) => {
|
||||||
|
if (product.name.includes('Airpods Pro')) return '$299 – $1,900';
|
||||||
|
if (product.name.includes('Apple Watch')) return '$669 – $700';
|
||||||
|
if (product.name.includes('Ceptics')) return '$300 – $1,500';
|
||||||
|
if (product.name.includes('Bose Portable')) return '$139 – $190';
|
||||||
|
if (product.name.includes('Lenovo IdeaPad')) return '$400 – $469';
|
||||||
|
if (product.name.includes('Lenovo Tab')) return '$144 – $150';
|
||||||
|
if (product.name.includes('LG 674 litres')) return '$85 – $299';
|
||||||
|
if (product.name.includes('J.P.Gold')) return '$45 – $49';
|
||||||
|
if (product.name.includes('Laview Home')) return '$79 – $99';
|
||||||
|
if (product.name.includes('Jajot J2')) return `$${product.price.toLocaleString()}`;
|
||||||
|
if (product.name.includes('HP LaserJet')) return `$${product.price.toLocaleString()}`;
|
||||||
|
|
||||||
|
return product.oldPrice
|
||||||
|
? `$${product.price.toLocaleString()}`
|
||||||
|
: `$${product.price.toLocaleString()}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[#F8F9FB] text-[#1E293B] flex flex-col font-sans">
|
||||||
|
<AnnouncementBar />
|
||||||
|
<Header
|
||||||
|
onMenuToggle={() => setDrawerOpen(true)}
|
||||||
|
cartCount={0}
|
||||||
|
wishlistCount={wishlist.length}
|
||||||
|
onWishlistClick={() => setWishlistModalOpen(true)}
|
||||||
|
/>
|
||||||
|
<Navbar />
|
||||||
|
<MobileDrawer isOpen={drawerOpen} onClose={() => setDrawerOpen(false)} />
|
||||||
|
|
||||||
|
{/* Main Container */}
|
||||||
|
<main className="flex-grow pt-4 pb-12">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1400px]">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{/* Core Sidebar + Grid Layout Grid */}
|
||||||
|
<div className="flex flex-col lg:flex-row gap-8 items-start">
|
||||||
|
|
||||||
|
{/* Sidebar Filters */}
|
||||||
|
<aside className="w-full lg:w-[280px] flex-shrink-0 space-y-6">
|
||||||
|
|
||||||
|
{/* Category Filter */}
|
||||||
|
<div className="bg-white rounded-2xl border border-gray-150 p-5 shadow-xs">
|
||||||
|
<div
|
||||||
|
onClick={() => toggleCollapse('categories')}
|
||||||
|
className="flex items-center justify-between font-bold text-[14px] text-gray-800 cursor-pointer select-none pb-3 border-b border-gray-100"
|
||||||
|
>
|
||||||
|
<span>Shop By Categories</span>
|
||||||
|
{collapsedFilters.categories ? <ChevronDown size={16} /> : <ChevronUp size={16} />}
|
||||||
|
</div>
|
||||||
|
{!collapsedFilters.categories && (
|
||||||
|
<div className="pt-4 space-y-3">
|
||||||
|
{[
|
||||||
|
{ name: 'Our Store', count: 24 },
|
||||||
|
{ name: 'Cameras', count: 5 },
|
||||||
|
{ name: 'Chargers & Cables', count: 8 },
|
||||||
|
{ name: 'Laptop & Computers', count: 6 },
|
||||||
|
{ name: 'Phones', count: 6 },
|
||||||
|
{ name: 'Smart Devices', count: 20 },
|
||||||
|
{ name: 'TV & Speaker', count: 7 }
|
||||||
|
].map(cat => (
|
||||||
|
<label key={cat.name} className="flex items-center justify-between text-[13px] text-gray-600 cursor-pointer hover:text-primary transition-colors font-medium">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedCategories.includes(cat.name)}
|
||||||
|
onChange={() => handleCategoryChange(cat.name)}
|
||||||
|
className="w-4 h-4 rounded-md border-gray-305 text-primary focus:ring-primary cursor-pointer accent-primary"
|
||||||
|
/>
|
||||||
|
<span>{cat.name}</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-gray-400">({cat.count})</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Brands Filter */}
|
||||||
|
<div className="bg-white rounded-2xl border border-gray-150 p-5 shadow-xs">
|
||||||
|
<div
|
||||||
|
onClick={() => toggleCollapse('brands')}
|
||||||
|
className="flex items-center justify-between font-bold text-[14px] text-gray-800 cursor-pointer select-none pb-3 border-b border-gray-100"
|
||||||
|
>
|
||||||
|
<span>Brands</span>
|
||||||
|
{collapsedFilters.brands ? <ChevronDown size={16} /> : <ChevronUp size={16} />}
|
||||||
|
</div>
|
||||||
|
{!collapsedFilters.brands && (
|
||||||
|
<div className="pt-4 space-y-3">
|
||||||
|
{[
|
||||||
|
'Creative',
|
||||||
|
'Design',
|
||||||
|
'Gallery',
|
||||||
|
'Gold',
|
||||||
|
'Highlight',
|
||||||
|
'Modern',
|
||||||
|
'Nature',
|
||||||
|
'Sparker'
|
||||||
|
].map(brand => (
|
||||||
|
<label key={brand} className="flex items-center gap-2.5 text-[13px] text-gray-600 cursor-pointer hover:text-primary transition-colors font-medium font-outfit">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedBrands.includes(brand)}
|
||||||
|
onChange={() => handleBrandChange(brand)}
|
||||||
|
className="w-4 h-4 rounded-md border-gray-305 text-primary focus:ring-primary cursor-pointer accent-primary"
|
||||||
|
/>
|
||||||
|
<span>{brand}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Highlight Filter */}
|
||||||
|
<div className="bg-white rounded-2xl border border-gray-150 p-5 shadow-xs">
|
||||||
|
<div
|
||||||
|
onClick={() => toggleCollapse('highlight')}
|
||||||
|
className="flex items-center justify-between font-bold text-[14px] text-gray-800 cursor-pointer select-none pb-3 border-b border-gray-100"
|
||||||
|
>
|
||||||
|
<span>Highlight</span>
|
||||||
|
{collapsedFilters.highlight ? <ChevronDown size={16} /> : <ChevronUp size={16} />}
|
||||||
|
</div>
|
||||||
|
{!collapsedFilters.highlight && (
|
||||||
|
<div className="pt-4 space-y-3">
|
||||||
|
{[
|
||||||
|
'All Products',
|
||||||
|
'Best Seller',
|
||||||
|
'New Arrivals',
|
||||||
|
'Sale',
|
||||||
|
'Hot Items'
|
||||||
|
].map(hl => (
|
||||||
|
<button
|
||||||
|
key={hl}
|
||||||
|
onClick={() => setSelectedHighlight(hl)}
|
||||||
|
className={`block text-left w-full text-[13px] py-1 transition-colors font-bold ${selectedHighlight === hl ? 'text-primary font-bold' : 'text-gray-600 hover:text-primary'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{hl}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filter By Color */}
|
||||||
|
<div className="bg-white rounded-2xl border border-gray-150 p-5 shadow-xs">
|
||||||
|
<div
|
||||||
|
onClick={() => toggleCollapse('color')}
|
||||||
|
className="flex items-center justify-between font-bold text-[14px] text-gray-800 cursor-pointer select-none pb-3 border-b border-gray-100"
|
||||||
|
>
|
||||||
|
<span>Filter by Color</span>
|
||||||
|
{collapsedFilters.color ? <ChevronDown size={16} /> : <ChevronUp size={16} />}
|
||||||
|
</div>
|
||||||
|
{!collapsedFilters.color && (
|
||||||
|
<div className="pt-4 flex flex-wrap gap-2.5">
|
||||||
|
{[
|
||||||
|
{ name: 'black', class: 'bg-black border border-black' },
|
||||||
|
{ name: 'blue', class: 'bg-blue-600 border border-blue-600' },
|
||||||
|
{ name: 'brown', class: 'bg-amber-800 border border-amber-800' },
|
||||||
|
{ name: 'grey', class: 'bg-gray-500 border border-gray-500' },
|
||||||
|
{ name: 'green', class: 'bg-emerald-600 border border-emerald-600' },
|
||||||
|
{ name: 'orange', class: 'bg-orange-500 border border-orange-500' },
|
||||||
|
{ name: 'red', class: 'bg-red-650 border border-red-650' },
|
||||||
|
{ name: 'pink', class: 'bg-pink-400 border border-pink-400' },
|
||||||
|
{ name: 'purple', class: 'bg-purple-600 border border-purple-600' },
|
||||||
|
{ name: 'white', class: 'bg-white border border-gray-300' }
|
||||||
|
].map(col => (
|
||||||
|
<button
|
||||||
|
key={col.name}
|
||||||
|
onClick={() => setSelectedColor(selectedColor === col.name ? null : col.name)}
|
||||||
|
className={`w-6 h-6 rounded-full cursor-pointer transition-all ${col.class} ${selectedColor === col.name ? 'ring-2 ring-primary ring-offset-2 scale-110' : 'hover:scale-105'
|
||||||
|
}`}
|
||||||
|
title={col.name}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Price Filter */}
|
||||||
|
<div className="bg-white rounded-2xl border border-gray-150 p-5 shadow-xs">
|
||||||
|
<div
|
||||||
|
onClick={() => toggleCollapse('price')}
|
||||||
|
className="flex items-center justify-between font-bold text-[14px] text-gray-800 cursor-pointer select-none pb-3 border-b border-gray-100"
|
||||||
|
>
|
||||||
|
<span>Price Filter</span>
|
||||||
|
{collapsedFilters.price ? <ChevronDown size={16} /> : <ChevronUp size={16} />}
|
||||||
|
</div>
|
||||||
|
{!collapsedFilters.price && (
|
||||||
|
<div className="pt-4 space-y-3">
|
||||||
|
{[
|
||||||
|
{ name: 'All', label: 'All' },
|
||||||
|
{ name: '$0-$400', label: '$0 - $400' },
|
||||||
|
{ name: '$400-$800', label: '$400 - $800' },
|
||||||
|
{ name: '$800-$1,200', label: '$800 - $1,200' },
|
||||||
|
{ name: '$1,200-$1,650', label: '$1,200 - $1,600' },
|
||||||
|
{ name: '$1,600+', label: '$1,600+' }
|
||||||
|
].map(pr => (
|
||||||
|
<button
|
||||||
|
key={pr.name}
|
||||||
|
onClick={() => setSelectedPriceRange(pr.name)}
|
||||||
|
className={`block text-left w-full text-[13px] py-1 transition-colors font-bold ${selectedPriceRange === pr.name ? 'text-primary font-bold' : 'text-gray-600 hover:text-primary'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{pr.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Average Rating Filter */}
|
||||||
|
<div className="bg-white rounded-2xl border border-gray-150 p-5 shadow-xs">
|
||||||
|
<div
|
||||||
|
onClick={() => toggleCollapse('rating')}
|
||||||
|
className="flex items-center justify-between font-bold text-[14px] text-gray-800 cursor-pointer select-none pb-3 border-b border-gray-100"
|
||||||
|
>
|
||||||
|
<span>Average rating</span>
|
||||||
|
{collapsedFilters.rating ? <ChevronDown size={16} /> : <ChevronUp size={16} />}
|
||||||
|
</div>
|
||||||
|
{!collapsedFilters.rating && (
|
||||||
|
<div className="pt-4 space-y-3">
|
||||||
|
{[5, 4, 3, 2, 1].map(stars => (
|
||||||
|
<button
|
||||||
|
key={stars}
|
||||||
|
onClick={() => setSelectedRating(selectedRating === stars ? null : stars)}
|
||||||
|
className="flex items-center justify-between w-full text-[13px] text-gray-600 hover:text-primary transition-colors font-medium font-outfit"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{[...Array(5)].map((_, i) => (
|
||||||
|
<Star
|
||||||
|
key={i}
|
||||||
|
size={12}
|
||||||
|
className={i < stars ? "text-amber-400 fill-amber-400" : "text-gray-200"}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<span className="text-gray-400">({getRatingCount(stars)})</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* Products Main Grid */}
|
||||||
|
<div className="flex-grow min-w-0 w-full">
|
||||||
|
|
||||||
|
{/* Toolbar */}
|
||||||
|
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 mb-6 font-sans">
|
||||||
|
|
||||||
|
{/* Result count */}
|
||||||
|
<div className="text-[14px] font-semibold text-[#1E293B]">
|
||||||
|
Showing all {filteredProducts.length} results
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right controls */}
|
||||||
|
<div className="flex items-center gap-5 w-full sm:w-auto justify-end">
|
||||||
|
{/* Sorting */}
|
||||||
|
<div className="relative">
|
||||||
|
<select
|
||||||
|
value={sortBy}
|
||||||
|
onChange={(e) => setSortBy(e.target.value)}
|
||||||
|
className="appearance-none bg-transparent border-b border-gray-250 pr-6 pb-1 text-[13px] font-bold text-gray-700 focus:outline-none cursor-pointer font-outfit"
|
||||||
|
>
|
||||||
|
<option value="default">Default sorting</option>
|
||||||
|
<option value="price-low">Sort by price: low to high</option>
|
||||||
|
<option value="price-high">Sort by price: high to low</option>
|
||||||
|
<option value="rating">Sort by average rating</option>
|
||||||
|
</select>
|
||||||
|
<ChevronDown size={12} className="text-gray-400 absolute right-0.5 top-1/2 transform -translate-y-1/2 pointer-events-none" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid/List toggles */}
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
onClick={() => setLayout('grid')}
|
||||||
|
className={`p-2.5 cursor-pointer rounded-sm transition-colors text-[13px] font-bold ${layout === 'grid' ? 'bg-[#0073EC] text-white' : 'bg-gray-100 text-gray-500 hover:bg-gray-200'}`}
|
||||||
|
title="Grid Layout"
|
||||||
|
>
|
||||||
|
<Grid size={15} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setLayout('list')}
|
||||||
|
className={`p-2.5 cursor-pointer rounded-sm transition-colors text-[13px] font-bold ${layout === 'list' ? 'bg-[#0073EC] text-white' : 'bg-gray-100 text-gray-500 hover:bg-gray-200'}`}
|
||||||
|
title="List Layout"
|
||||||
|
>
|
||||||
|
<List size={15} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Active Filter Chips */}
|
||||||
|
{(selectedCategories.length > 0 || selectedBrands.length > 0 || selectedHighlight !== 'All Products' || selectedColor || selectedPriceRange !== 'All' || selectedRating || searchQuery) && (
|
||||||
|
<div className="flex flex-wrap items-center gap-2.5 mb-6 font-sans">
|
||||||
|
|
||||||
|
{searchQuery && (
|
||||||
|
<button
|
||||||
|
onClick={() => setSearchQuery('')}
|
||||||
|
className="bg-[#EFEFEF] hover:bg-black text-[#1E1E1E] hover:text-white transition-all cursor-pointer rounded-[4px] px-3.5 py-1.5 text-[12px] font-extrabold flex items-center gap-1.5 border-0 focus:outline-none"
|
||||||
|
title="Remove Search Term"
|
||||||
|
>
|
||||||
|
<span className="text-[14px] leading-none">×</span>
|
||||||
|
<span>Search: “{searchQuery}”</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedCategories.map(cat => (
|
||||||
|
<button
|
||||||
|
key={cat}
|
||||||
|
onClick={() => handleCategoryChange(cat)}
|
||||||
|
className="bg-[#EFEFEF] hover:bg-black text-[#1E1E1E] hover:text-white transition-all cursor-pointer rounded-[4px] px-3.5 py-1.5 text-[12px] font-extrabold flex items-center gap-1.5 border-0 focus:outline-none"
|
||||||
|
title="Remove This Category"
|
||||||
|
>
|
||||||
|
<span className="text-[14px] leading-none">×</span>
|
||||||
|
<span>{cat}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{selectedBrands.map(br => (
|
||||||
|
<button
|
||||||
|
key={br}
|
||||||
|
onClick={() => handleBrandChange(br)}
|
||||||
|
className="bg-[#EFEFEF] hover:bg-black text-[#1E1E1E] hover:text-white transition-all cursor-pointer rounded-[4px] px-3.5 py-1.5 text-[12px] font-extrabold flex items-center gap-1.5 border-0 focus:outline-none"
|
||||||
|
title="Remove This Product Brand"
|
||||||
|
>
|
||||||
|
<span className="text-[14px] leading-none">×</span>
|
||||||
|
<span>{br}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{selectedHighlight !== 'All Products' && (
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedHighlight('All Products')}
|
||||||
|
className="bg-[#EFEFEF] hover:bg-black text-[#1E1E1E] hover:text-white transition-all cursor-pointer rounded-[4px] px-3.5 py-1.5 text-[12px] font-extrabold flex items-center gap-1.5 border-0 focus:outline-none"
|
||||||
|
title="Remove This Highlight"
|
||||||
|
>
|
||||||
|
<span className="text-[14px] leading-none">×</span>
|
||||||
|
<span>{selectedHighlight}</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedColor && (
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedColor(null)}
|
||||||
|
className="bg-[#EFEFEF] hover:bg-black text-[#1E1E1E] hover:text-white transition-all cursor-pointer rounded-[4px] px-3.5 py-1.5 text-[12px] font-extrabold flex items-center gap-1.5 border-0 focus:outline-none"
|
||||||
|
title="Remove This Color"
|
||||||
|
>
|
||||||
|
<span className="text-[14px] leading-none">×</span>
|
||||||
|
<span>Color: {selectedColor}</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedPriceRange !== 'All' && (
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedPriceRange('All')}
|
||||||
|
className="bg-[#EFEFEF] hover:bg-black text-[#1E1E1E] hover:text-white transition-all cursor-pointer rounded-[4px] px-3.5 py-1.5 text-[12px] font-extrabold flex items-center gap-1.5 border-0 focus:outline-none"
|
||||||
|
title="Remove This Price Filter"
|
||||||
|
>
|
||||||
|
<span className="text-[14px] leading-none">×</span>
|
||||||
|
<span>Price: {selectedPriceRange}</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedRating && (
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedRating(null)}
|
||||||
|
className="bg-[#EFEFEF] hover:bg-black text-[#1E1E1E] hover:text-white transition-all cursor-pointer rounded-[4px] px-3.5 py-1.5 text-[12px] font-extrabold flex items-center gap-1.5 border-0 focus:outline-none"
|
||||||
|
title="Remove This Rating Filter"
|
||||||
|
>
|
||||||
|
<span className="text-[14px] leading-none">×</span>
|
||||||
|
<span>{selectedRating} Stars</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={resetAllFilters}
|
||||||
|
className="text-[13px] font-bold text-[#0073EC] hover:text-black transition-colors underline decoration-solid ml-2 cursor-pointer focus:outline-none"
|
||||||
|
>
|
||||||
|
Clear All
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* No Products Found */}
|
||||||
|
{filteredProducts.length === 0 && (
|
||||||
|
<div className="bg-white rounded-2xl border border-gray-150 p-12 text-center shadow-xs">
|
||||||
|
<div className="max-w-md mx-auto">
|
||||||
|
<Search className="mx-auto text-gray-300 mb-4 stroke-[1.5]" size={48} />
|
||||||
|
<h3 className="text-lg font-bold text-gray-800 mb-1">No products found</h3>
|
||||||
|
<p className="text-gray-500 text-sm mb-6">We couldn't find any items matching your current filters. Try removing some filters to see more products.</p>
|
||||||
|
<button
|
||||||
|
onClick={resetAllFilters}
|
||||||
|
className="bg-primary hover:bg-primary-hover text-white text-xs font-semibold px-6 py-3 rounded-lg shadow-sm transition-all"
|
||||||
|
>
|
||||||
|
Clear All Filters
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Products List/Grid Rendering */}
|
||||||
|
{layout === 'grid' ? (
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 bg-white border-t border-l border-gray-200 shadow-xs">
|
||||||
|
{filteredProducts.map((product) => (
|
||||||
|
<GridProductCard
|
||||||
|
key={product.id}
|
||||||
|
product={product}
|
||||||
|
priceDisplay={getProductPriceDisplay(product)}
|
||||||
|
isWishlisted={wishlist.includes(product.id)}
|
||||||
|
onToggleWishlist={toggleWishlist}
|
||||||
|
onQuickView={setQuickViewProduct}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="bg-white border border-gray-200 divide-y divide-gray-200 shadow-xs">
|
||||||
|
{filteredProducts.map((product) => (
|
||||||
|
<ListProductCard
|
||||||
|
key={product.id}
|
||||||
|
product={product}
|
||||||
|
priceDisplay={getProductPriceDisplay(product)}
|
||||||
|
isWishlisted={wishlist.includes(product.id)}
|
||||||
|
onToggleWishlist={toggleWishlist}
|
||||||
|
onQuickView={setQuickViewProduct}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
<FloatingButtons />
|
||||||
|
<LiveToast />
|
||||||
|
|
||||||
|
{/* Quick View Modal Overlay */}
|
||||||
|
{quickViewProduct && (
|
||||||
|
<QuickViewModal
|
||||||
|
product={quickViewProduct}
|
||||||
|
onClose={() => setQuickViewProduct(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Wishlist Modal Overlay */}
|
||||||
|
{wishlistModalOpen && (
|
||||||
|
<WishlistModal onClose={() => setWishlistModalOpen(false)} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Grid Product Card to match the exact mockup designs */
|
||||||
|
function GridProductCard({
|
||||||
|
product,
|
||||||
|
priceDisplay,
|
||||||
|
isWishlisted,
|
||||||
|
onToggleWishlist,
|
||||||
|
onQuickView
|
||||||
|
}: {
|
||||||
|
product: Product;
|
||||||
|
priceDisplay: string;
|
||||||
|
isWishlisted: boolean;
|
||||||
|
onToggleWishlist: (id: string) => void;
|
||||||
|
onQuickView: (product: Product) => void;
|
||||||
|
}) {
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
|
className="bg-white p-5 flex flex-col justify-between relative overflow-visible transition-all duration-300 min-h-[430px] group/card border-r border-b border-gray-200 rounded-none font-sans"
|
||||||
|
>
|
||||||
|
{/* Top badges & actions */}
|
||||||
|
<div className="flex justify-between items-start z-10 w-full mb-1">
|
||||||
|
{/* Discount Badge */}
|
||||||
|
{product.discount ? (
|
||||||
|
<span className="bg-primary text-white text-[10px] font-extrabold px-2 py-0.5 rounded-sm">
|
||||||
|
-{product.discount}%
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Hover action flyouts on the side */}
|
||||||
|
<div className={`flex flex-col gap-1.5 transition-all duration-300 absolute right-3 top-3 z-20 ${isHovered ? 'opacity-100 translate-x-0' : 'opacity-0 translate-x-2 pointer-events-none'}`}>
|
||||||
|
<button
|
||||||
|
onClick={(e) => { e.preventDefault(); e.stopPropagation(); onToggleWishlist(product.id); }}
|
||||||
|
className={`w-8 h-8 rounded-full border flex items-center justify-center shadow-sm transition-all cursor-pointer ${isWishlisted ? 'bg-primary text-white border-primary' : 'bg-white hover:bg-primary hover:text-white text-gray-500 border-gray-150'
|
||||||
|
}`}
|
||||||
|
title="Add to Wishlist"
|
||||||
|
>
|
||||||
|
<Heart size={13} className={isWishlisted ? 'fill-white' : ''} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => { e.preventDefault(); e.stopPropagation(); onQuickView(product); }}
|
||||||
|
className="w-8 h-8 rounded-full bg-white hover:bg-primary hover:text-white text-gray-500 border border-gray-150 flex items-center justify-center shadow-sm transition-all cursor-pointer"
|
||||||
|
title="Quick View"
|
||||||
|
>
|
||||||
|
<Eye size={13} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Product Image & Timer Container */}
|
||||||
|
<Link href={`/product/${product.id}`} className="w-full flex-1 flex flex-col items-center justify-center relative min-h-[170px] mb-3 select-none block">
|
||||||
|
<img
|
||||||
|
src={product.image}
|
||||||
|
alt={product.name}
|
||||||
|
className={`max-h-[150px] max-w-full object-contain drop-shadow-md rounded-lg transition-all duration-300 ${product.imageHover && isHovered ? 'opacity-0 scale-95' : 'opacity-100 scale-100'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{product.imageHover && (
|
||||||
|
<img
|
||||||
|
src={product.imageHover}
|
||||||
|
alt={product.name}
|
||||||
|
className={`max-h-[150px] max-w-full object-contain drop-shadow-md rounded-lg transition-all duration-300 absolute ${isHovered ? 'opacity-100 scale-100' : 'opacity-0 scale-95 pointer-events-none'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Countdown Timer Block (styled to match mockup) */}
|
||||||
|
{product.hasTimer && product.timeRemaining && (
|
||||||
|
<div className="absolute -bottom-2 left-1/2 transform -translate-x-1/2 bg-red-50 text-red-650 border border-red-100 px-3 py-1.5 rounded-lg shadow-sm z-10 w-[95%] text-center">
|
||||||
|
<div className="flex items-center justify-center gap-1.5 text-[9px] font-extrabold uppercase tracking-wide">
|
||||||
|
<div><span className="text-[12px] block leading-none font-black text-red-650">{product.timeRemaining.days}</span><span>DAYS</span></div>
|
||||||
|
<div className="text-gray-400 font-normal">:</div>
|
||||||
|
<div><span className="text-[12px] block leading-none font-black text-red-650">04</span><span>HRS</span></div>
|
||||||
|
<div className="text-gray-400 font-normal">:</div>
|
||||||
|
<div><span className="text-[12px] block leading-none font-black text-red-650">50</span><span>MIN</span></div>
|
||||||
|
<div className="text-gray-400 font-normal">:</div>
|
||||||
|
<div><span className="text-[12px] block leading-none font-black text-red-650">18</span><span>SEC</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Info Block */}
|
||||||
|
<div className="text-left w-full mt-3">
|
||||||
|
{/* Title */}
|
||||||
|
<Link href={`/product/${product.id}`}>
|
||||||
|
<h3 className="text-[12.5px] font-bold text-gray-800 line-clamp-2 leading-snug hover:text-primary transition-colors cursor-pointer min-h-[38px] mb-2 font-outfit" title={product.name}>
|
||||||
|
{product.name}
|
||||||
|
</h3>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Rating Stars */}
|
||||||
|
<div className="flex items-center gap-0.5 mb-1.5">
|
||||||
|
{[...Array(5)].map((_, i) => (
|
||||||
|
<Star
|
||||||
|
key={i}
|
||||||
|
size={12}
|
||||||
|
className={i < Math.floor(product.rating) ? "text-amber-400 fill-amber-400" : "text-gray-200"}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Price Display */}
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<span className="text-[14px] font-extrabold text-gray-900">
|
||||||
|
{priceDisplay}
|
||||||
|
</span>
|
||||||
|
{product.oldPrice && !priceDisplay.includes('–') && (
|
||||||
|
<span className="text-xs text-gray-400 line-through font-medium">
|
||||||
|
${product.oldPrice.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Select Options Button */}
|
||||||
|
<div
|
||||||
|
className={`w-full relative overflow-hidden transition-all duration-300 ${
|
||||||
|
isHovered ? 'h-[44px] mt-2' : 'h-0'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href={`/product/${product.id}`}
|
||||||
|
className="w-full bg-primary hover:bg-black text-white text-[11px] font-bold uppercase py-3 px-4 rounded-lg flex items-center justify-center gap-2 shadow-sm transition-all duration-300 cursor-pointer"
|
||||||
|
>
|
||||||
|
<ShoppingCart size={13} className="stroke-[2.5]" />
|
||||||
|
Select Options
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom List Product Card to support List layout view */
|
||||||
|
function ListProductCard({
|
||||||
|
product,
|
||||||
|
priceDisplay,
|
||||||
|
isWishlisted,
|
||||||
|
onToggleWishlist,
|
||||||
|
onQuickView
|
||||||
|
}: {
|
||||||
|
product: Product;
|
||||||
|
priceDisplay: string;
|
||||||
|
isWishlisted: boolean;
|
||||||
|
onToggleWishlist: (id: string) => void;
|
||||||
|
onQuickView: (product: Product) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white p-6 flex flex-col md:flex-row items-center gap-6 group font-sans rounded-none">
|
||||||
|
|
||||||
|
{/* Product Image */}
|
||||||
|
<Link href={`/product/${product.id}`} className="w-full md:w-48 h-48 flex-shrink-0 flex items-center justify-center relative select-none block">
|
||||||
|
<img
|
||||||
|
src={product.image}
|
||||||
|
alt={product.name}
|
||||||
|
className="max-h-[160px] max-w-full object-contain drop-shadow-md rounded-lg group-hover:scale-105 transition-transform duration-300"
|
||||||
|
/>
|
||||||
|
{product.discount && (
|
||||||
|
<span className="absolute top-2 left-2 bg-primary text-white text-[10px] font-extrabold px-2 py-0.5 rounded-md">
|
||||||
|
-{product.discount}%
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Product Details */}
|
||||||
|
<div className="flex-grow text-left">
|
||||||
|
{/* Title */}
|
||||||
|
<Link href={`/product/${product.id}`}>
|
||||||
|
<h3 className="text-base font-bold text-gray-800 hover:text-primary transition-colors cursor-pointer mb-2 font-outfit" title={product.name}>
|
||||||
|
{product.name}
|
||||||
|
</h3>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Rating Stars */}
|
||||||
|
<div className="flex items-center gap-0.5 mb-2">
|
||||||
|
{[...Array(5)].map((_, i) => (
|
||||||
|
<Star
|
||||||
|
key={i}
|
||||||
|
size={12}
|
||||||
|
className={i < Math.floor(product.rating) ? "text-amber-400 fill-amber-400" : "text-gray-200"}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Price Display */}
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<span className="text-[17px] font-extrabold text-gray-900">
|
||||||
|
{priceDisplay}
|
||||||
|
</span>
|
||||||
|
{product.oldPrice && !priceDisplay.includes('–') && (
|
||||||
|
<span className="text-xs text-gray-400 line-through font-medium">
|
||||||
|
${product.oldPrice.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Specs List (styled to match Image 2) */}
|
||||||
|
<ul className="text-gray-500 text-[12.5px] font-semibold space-y-1 mb-4 list-disc list-inside">
|
||||||
|
<li>Functional Crown</li>
|
||||||
|
<li>Fhd Footage</li>
|
||||||
|
<li>1.78" Amoled</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{/* Action Row */}
|
||||||
|
<div className="flex items-center gap-3 pt-3.5 border-t border-gray-100">
|
||||||
|
<Link
|
||||||
|
href={`/product/${product.id}`}
|
||||||
|
className="bg-[#0073EC] hover:bg-black text-white text-xs font-semibold px-6 py-2.5 rounded-lg flex items-center gap-2 shadow-sm transition-all cursor-pointer"
|
||||||
|
>
|
||||||
|
<ShoppingCart size={13} className="stroke-[2.5]" />
|
||||||
|
View Products
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
onClick={(e) => { e.preventDefault(); e.stopPropagation(); onToggleWishlist(product.id); }}
|
||||||
|
className={`p-2.5 rounded-lg border transition-colors cursor-pointer ${isWishlisted ? 'bg-primary text-white border-primary' : 'text-gray-400 hover:text-primary hover:bg-blue-50 border-gray-150'
|
||||||
|
}`}
|
||||||
|
title="Add to Wishlist"
|
||||||
|
>
|
||||||
|
<Heart size={14} className={isWishlisted ? 'fill-white' : ''} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => { e.preventDefault(); e.stopPropagation(); onQuickView(product); }}
|
||||||
|
className="p-2.5 text-gray-400 hover:text-primary hover:bg-blue-50 rounded-lg border border-gray-150 transition-colors cursor-pointer"
|
||||||
|
title="Quick View"
|
||||||
|
>
|
||||||
|
<Eye size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
83
NewFile/src/src/components/CategorySidebar.tsx
Normal file
83
NewFile/src/src/components/CategorySidebar.tsx
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { ChevronDown } from 'lucide-react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
interface CategoryItem {
|
||||||
|
name: string;
|
||||||
|
hasSubmenu?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CategorySidebarProps {
|
||||||
|
isDropdown?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CategorySidebar: React.FC<CategorySidebarProps> = ({ isDropdown = false }) => {
|
||||||
|
const categories: CategoryItem[] = [
|
||||||
|
{ name: 'All Products' },
|
||||||
|
{ name: 'Our Store', hasSubmenu: true },
|
||||||
|
{ name: 'TV & Speaker' },
|
||||||
|
{ name: 'Smart Devices', hasSubmenu: true },
|
||||||
|
{ name: 'Chargers & Cables' },
|
||||||
|
{ name: 'Laptop & Computers' },
|
||||||
|
{ name: 'Phones', hasSubmenu: true },
|
||||||
|
{ name: 'Cameras' },
|
||||||
|
{ name: 'Smart Watches' },
|
||||||
|
{ name: 'Home Theatre Systems' },
|
||||||
|
{ name: 'Televisions' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`w-[280px] bg-white rounded-b-theme border-l border-r border-b border-primary shadow-[0_0_12px_rgba(25,118,243,0.2)] flex-shrink-0 hidden xl:block relative z-45 mt-0 ${
|
||||||
|
isDropdown ? 'border-t' : ''
|
||||||
|
}`}>
|
||||||
|
<ul className="flex flex-col">
|
||||||
|
{categories.map((cat, idx) => (
|
||||||
|
<li key={cat.name} className="relative group/item">
|
||||||
|
<Link
|
||||||
|
href={cat.name === 'All Products' ? '/shop' : `/shop?category=${encodeURIComponent(cat.name)}`}
|
||||||
|
className={`flex items-center justify-between px-5 py-[12.5px] text-[13px] font-semibold text-gray-700 hover:text-primary hover:bg-blue-50/20 transition-all ${
|
||||||
|
idx < categories.length - 1 ? 'border-b border-gray-100' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>{cat.name}</span>
|
||||||
|
{cat.hasSubmenu && (
|
||||||
|
<ChevronDown size={13} className="text-gray-400 group-hover/item:text-primary transition-all" />
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Premium Flyout Mega Menu */}
|
||||||
|
{cat.hasSubmenu && (
|
||||||
|
<div className="absolute left-[279px] top-0 w-[500px] bg-white rounded-r-theme border border-gray-200 shadow-soft p-6 opacity-0 invisible translate-x-2 group-hover/item:opacity-100 group-hover/item:visible group-hover/item:translate-x-0 transition-all duration-200 z-50 grid grid-cols-2 gap-6 min-h-[380px]">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-[11px] font-bold text-gray-800 uppercase tracking-wider mb-3">Popular Brands</h4>
|
||||||
|
<ul className="space-y-2 text-[13px] text-gray-500 font-medium">
|
||||||
|
<li><a href="#" className="hover:text-primary transition-colors">Apple Store</a></li>
|
||||||
|
<li><a href="#" className="hover:text-primary transition-colors">Samsung Galaxy</a></li>
|
||||||
|
<li><a href="#" className="hover:text-primary transition-colors">Google Pixel</a></li>
|
||||||
|
<li><a href="#" className="hover:text-primary transition-colors">Sony Electronics</a></li>
|
||||||
|
<li><a href="#" className="hover:text-primary transition-colors">OnePlus Official</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-[11px] font-bold text-gray-800 uppercase tracking-wider mb-3">Trending Products</h4>
|
||||||
|
<ul className="space-y-2 text-[13px] text-gray-500 font-medium text-left">
|
||||||
|
<li><a href="#" className="hover:text-primary transition-colors">Top Sellers</a></li>
|
||||||
|
<li><a href="#" className="hover:text-primary transition-colors">Super Discounts</a></li>
|
||||||
|
<li><a href="#" className="hover:text-primary transition-colors">Vouchers & Coupons</a></li>
|
||||||
|
<li><a href="#" className="hover:text-primary transition-colors">New Releases</a></li>
|
||||||
|
<li><a href="#" className="hover:text-primary transition-colors">Exclusive Packs</a></li>
|
||||||
|
</ul>
|
||||||
|
<div className="mt-6 p-4 rounded-xl bg-gradient-to-r from-blue-500 to-indigo-600 text-white text-left">
|
||||||
|
<p className="text-[10px] font-bold uppercase tracking-widest text-blue-100">Hot Offer</p>
|
||||||
|
<h5 className="font-bold text-[13px] leading-tight mt-1">Get 20% OFF on Audio</h5>
|
||||||
|
<a href="#" className="inline-block mt-3 text-[11px] font-extrabold uppercase border-b-2 border-white leading-none pb-0.5">Shop Now</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
94
NewFile/src/src/components/CountdownTimer.tsx
Normal file
94
NewFile/src/src/components/CountdownTimer.tsx
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
interface CountdownTimerProps {
|
||||||
|
initialDays?: number;
|
||||||
|
initialHours?: number;
|
||||||
|
initialMinutes?: number;
|
||||||
|
initialSeconds?: number;
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CountdownTimer: React.FC<CountdownTimerProps> = ({
|
||||||
|
initialDays = 0,
|
||||||
|
initialHours = 0,
|
||||||
|
initialMinutes = 0,
|
||||||
|
initialSeconds = 0,
|
||||||
|
size = 'md',
|
||||||
|
className = '',
|
||||||
|
}) => {
|
||||||
|
const [totalSeconds, setTotalSeconds] = useState(
|
||||||
|
initialDays * 86400 + initialHours * 3600 + initialMinutes * 60 + initialSeconds
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (totalSeconds <= 0) return;
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setTotalSeconds((prev) => prev - 1);
|
||||||
|
}, 1000);
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, [totalSeconds]);
|
||||||
|
|
||||||
|
const days = Math.floor(totalSeconds / 86400);
|
||||||
|
const hours = Math.floor((totalSeconds % 86400) / 3600);
|
||||||
|
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||||
|
const seconds = totalSeconds % 60;
|
||||||
|
|
||||||
|
const formatNumber = (num: number) => String(num).padStart(2, '0');
|
||||||
|
|
||||||
|
const containerSizes = {
|
||||||
|
sm: 'text-[9px] gap-1',
|
||||||
|
md: 'text-xs gap-1.5',
|
||||||
|
lg: 'text-sm gap-2',
|
||||||
|
};
|
||||||
|
|
||||||
|
const blockSizes = {
|
||||||
|
sm: 'w-7 h-7 text-[11px] rounded-md',
|
||||||
|
md: 'w-9 h-9 text-[13px] rounded-lg',
|
||||||
|
lg: 'w-12 h-12 text-[16px] rounded-xl',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex items-center ${containerSizes[size]} ${className}`}>
|
||||||
|
{/* Days */}
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div className={`flex items-center justify-center font-bold text-red-500 bg-red-50 border border-red-100 shadow-sm ${blockSizes[size]}`}>
|
||||||
|
{formatNumber(days)}
|
||||||
|
</div>
|
||||||
|
<span className="text-[9px] uppercase tracking-wider text-gray-400 mt-1 font-semibold">Days</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className="font-bold text-red-500 relative -top-2.5">:</span>
|
||||||
|
|
||||||
|
{/* Hours */}
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div className={`flex items-center justify-center font-bold text-red-500 bg-red-50 border border-red-100 shadow-sm ${blockSizes[size]}`}>
|
||||||
|
{formatNumber(hours)}
|
||||||
|
</div>
|
||||||
|
<span className="text-[9px] uppercase tracking-wider text-gray-400 mt-1 font-semibold">Hrs</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className="font-bold text-red-500 relative -top-2.5">:</span>
|
||||||
|
|
||||||
|
{/* Minutes */}
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div className={`flex items-center justify-center font-bold text-red-500 bg-red-50 border border-red-100 shadow-sm ${blockSizes[size]}`}>
|
||||||
|
{formatNumber(minutes)}
|
||||||
|
</div>
|
||||||
|
<span className="text-[9px] uppercase tracking-wider text-gray-400 mt-1 font-semibold">Min</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className="font-bold text-red-500 relative -top-2.5">:</span>
|
||||||
|
|
||||||
|
{/* Seconds */}
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div className={`flex items-center justify-center font-bold text-red-500 bg-red-50 border border-red-100 shadow-sm ${blockSizes[size]}`}>
|
||||||
|
{formatNumber(seconds)}
|
||||||
|
</div>
|
||||||
|
<span className="text-[9px] uppercase tracking-wider text-gray-400 mt-1 font-semibold">Sec</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
62
NewFile/src/src/components/FloatingButtons.tsx
Normal file
62
NewFile/src/src/components/FloatingButtons.tsx
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { ChevronUp, MessageSquare } from 'lucide-react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
|
||||||
|
export const FloatingButtons: React.FC = () => {
|
||||||
|
const [showScrollTop, setShowScrollTop] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
setShowScrollTop(window.scrollY > 400);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('scroll', handleScroll);
|
||||||
|
return () => window.removeEventListener('scroll', handleScroll);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const scrollToTop = () => {
|
||||||
|
window.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed bottom-6 right-6 z-50 flex flex-col gap-3">
|
||||||
|
{/* Scroll to Top */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{showScrollTop && (
|
||||||
|
<motion.button
|
||||||
|
initial={{ opacity: 0, scale: 0.8, y: 20 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.8, y: 20 }}
|
||||||
|
whileHover={{ y: -3, scale: 1.05 }}
|
||||||
|
onClick={scrollToTop}
|
||||||
|
className="w-12 h-12 bg-white hover:bg-gray-50 text-gray-700 rounded-full shadow-soft hover:shadow-soft-hover border border-gray-100 flex items-center justify-center transition-colors"
|
||||||
|
title="Scroll to top"
|
||||||
|
>
|
||||||
|
<ChevronUp size={22} className="stroke-[2.5]" />
|
||||||
|
</motion.button>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* Live Chat Bubble */}
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ y: -3, scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
className="w-12 h-12 bg-primary hover:bg-primary-hover text-white rounded-full shadow-soft hover:shadow-soft-hover flex items-center justify-center relative group"
|
||||||
|
title="Live Chat Support"
|
||||||
|
onClick={() => alert('Support Chat simulation started! Hello, how can we help you today?')}
|
||||||
|
>
|
||||||
|
<MessageSquare size={20} className="text-white" />
|
||||||
|
<span className="absolute right-14 bg-gray-900 text-white text-[10px] font-bold px-2.5 py-1.5 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity duration-200 shadow-md whitespace-nowrap">
|
||||||
|
Chat with us!
|
||||||
|
</span>
|
||||||
|
{/* Pulse effect */}
|
||||||
|
<span className="absolute inset-0 rounded-full border-2 border-primary animate-ping opacity-25" />
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
116
NewFile/src/src/components/LiveToast.tsx
Normal file
116
NewFile/src/src/components/LiveToast.tsx
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
|
import { CheckCircle2, X } from 'lucide-react';
|
||||||
|
|
||||||
|
interface Purchase {
|
||||||
|
name: string;
|
||||||
|
location: string;
|
||||||
|
product: string;
|
||||||
|
time: string;
|
||||||
|
image: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockPurchases: Purchase[] = [
|
||||||
|
{
|
||||||
|
name: 'Ben',
|
||||||
|
location: 'New York',
|
||||||
|
product: 'HP (23.8 Inch) with IPS Panel Technology',
|
||||||
|
time: '3 hours ago',
|
||||||
|
image: 'https://images.unsplash.com/photo-1527443224154-c4a3942d3acf?w=100&auto=format&fit=crop&q=60'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Swaraa',
|
||||||
|
location: 'California',
|
||||||
|
product: 'HP LaserJet P1108 Mono Single Function Laser Printer',
|
||||||
|
time: '46 minutes ago',
|
||||||
|
image: 'https://images.unsplash.com/photo-1612815154858-60aa4c59eaa6?w=100&auto=format&fit=crop&q=60'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Alex',
|
||||||
|
location: 'London',
|
||||||
|
product: 'SwapME Braided Nylon Woven Smart Watch',
|
||||||
|
time: '12 minutes ago',
|
||||||
|
image: 'https://images.unsplash.com/photo-1579586337278-3befd40fd17a?w=100&auto=format&fit=crop&q=60'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Hiroshi',
|
||||||
|
location: 'Tokyo',
|
||||||
|
product: 'Ceptics India to Europe Travel Adapter Plug',
|
||||||
|
time: '5 minutes ago',
|
||||||
|
image: 'https://images.unsplash.com/photo-1628157582853-a796fa650a6a?w=100&auto=format&fit=crop&q=60'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const LiveToast: React.FC = () => {
|
||||||
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const initialTimeout = setTimeout(() => {
|
||||||
|
setIsVisible(true);
|
||||||
|
}, 4000);
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setIsVisible(false);
|
||||||
|
setTimeout(() => {
|
||||||
|
setCurrentIndex((prev) => (prev + 1) % mockPurchases.length);
|
||||||
|
setIsVisible(true);
|
||||||
|
}, 1000);
|
||||||
|
}, 15000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(initialTimeout);
|
||||||
|
clearInterval(interval);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!mockPurchases.length) return null;
|
||||||
|
const currentPurchase = mockPurchases[currentIndex];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{isVisible && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 50, scale: 0.9 }}
|
||||||
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, y: 20, scale: 0.9 }}
|
||||||
|
transition={{ type: 'spring', stiffness: 300, damping: 25 }}
|
||||||
|
className="fixed bottom-6 left-6 z-50 max-w-[340px] bg-white rounded-theme p-4 shadow-soft hover:shadow-soft-hover border border-gray-100 flex items-start gap-3"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={currentPurchase.image}
|
||||||
|
alt={currentPurchase.product}
|
||||||
|
className="w-12 h-12 object-cover rounded-lg border border-gray-100 flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<div className="flex-1 min-w-0 pr-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-[11px] text-gray-500 font-medium">
|
||||||
|
<span className="font-bold text-gray-800">{currentPurchase.name}</span> from{' '}
|
||||||
|
<span className="font-semibold text-primary">{currentPurchase.location}</span>
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsVisible(false)}
|
||||||
|
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
<X size={12} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<h4 className="text-[12px] font-bold text-gray-800 mt-0.5 leading-snug truncate">
|
||||||
|
{currentPurchase.product}
|
||||||
|
</h4>
|
||||||
|
<div className="flex items-center gap-1.5 mt-1.5">
|
||||||
|
<span className="text-[10px] text-gray-400 font-semibold">{currentPurchase.time}</span>
|
||||||
|
<span className="w-1 h-1 bg-gray-300 rounded-full" />
|
||||||
|
<div className="flex items-center gap-0.5 text-green-600">
|
||||||
|
<CheckCircle2 size={10} fill="currentColor" className="text-white" />
|
||||||
|
<span className="text-[9px] font-bold tracking-wider uppercase">Verified</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
};
|
||||||
112
NewFile/src/src/components/MobileDrawer.tsx
Normal file
112
NewFile/src/src/components/MobileDrawer.tsx
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { X, Tablet, Tv, Speaker, Cable, Laptop, Smartphone, Camera, Watch, LayoutGrid } from 'lucide-react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
|
||||||
|
interface MobileDrawerProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MobileDrawer: React.FC<MobileDrawerProps> = ({ isOpen, onClose }) => {
|
||||||
|
const categories = [
|
||||||
|
{ name: 'Our Store', icon: <LayoutGrid size={15} /> },
|
||||||
|
{ name: 'TV & Speaker', icon: <Speaker size={15} /> },
|
||||||
|
{ name: 'Smart Devices', icon: <Tablet size={15} /> },
|
||||||
|
{ name: 'Chargers & Cables', icon: <Cable size={15} /> },
|
||||||
|
{ name: 'Laptop & Computers', icon: <Laptop size={15} /> },
|
||||||
|
{ name: 'Phones', icon: <Smartphone size={15} /> },
|
||||||
|
{ name: 'Cameras', icon: <Camera size={15} /> },
|
||||||
|
{ name: 'Smart Watches', icon: <Watch size={15} /> },
|
||||||
|
];
|
||||||
|
|
||||||
|
const links = [
|
||||||
|
{ name: 'Home', isNew: false, badge: '', badgeBg: '' },
|
||||||
|
{ name: 'Shop', isNew: false, badge: '', badgeBg: '' },
|
||||||
|
{ name: 'Categories', isNew: true, badge: 'Sale', badgeBg: 'bg-[#E2F9F3] text-[#00B894]' },
|
||||||
|
{ name: 'Products', isNew: true, badge: 'Hot', badgeBg: 'bg-red-50 text-red-500' },
|
||||||
|
{ name: 'Top Deals', isNew: false, badge: '', badgeBg: '' },
|
||||||
|
{ name: 'Elements', isNew: false, badge: '', badgeBg: '' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{isOpen && (
|
||||||
|
<>
|
||||||
|
{/* Backdrop */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 0.5 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
onClick={onClose}
|
||||||
|
className="fixed inset-0 bg-black z-50 xl:hidden"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Drawer Panel */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ x: '-100%' }}
|
||||||
|
animate={{ x: 0 }}
|
||||||
|
exit={{ x: '-100%' }}
|
||||||
|
transition={{ type: 'tween', duration: 0.3 }}
|
||||||
|
className="fixed top-0 left-0 bottom-0 w-[280px] bg-white z-50 xl:hidden flex flex-col justify-between shadow-xl overflow-y-auto"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between p-4 border-b border-gray-100">
|
||||||
|
<span className="text-xl font-extrabold tracking-tight text-gray-900">
|
||||||
|
Tech<span className="text-primary">Shop</span>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="p-1.5 text-gray-400 hover:text-gray-650 hover:bg-gray-50 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<X size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation Links */}
|
||||||
|
<div className="p-4 border-b border-gray-100 text-left">
|
||||||
|
<h4 className="text-[11px] font-bold text-gray-400 uppercase tracking-widest mb-3">Navigation</h4>
|
||||||
|
<ul className="space-y-3.5">
|
||||||
|
{links.map((link) => (
|
||||||
|
<li key={link.name}>
|
||||||
|
<a href="#" className="flex items-center justify-between text-sm font-bold text-gray-700 hover:text-primary transition-colors">
|
||||||
|
<span>{link.name}</span>
|
||||||
|
{link.isNew && (
|
||||||
|
<span className={`text-[8px] font-extrabold px-1.5 py-0.5 rounded tracking-wide uppercase ${link.badgeBg}`}>
|
||||||
|
{link.badge}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Categories */}
|
||||||
|
<div className="p-4 text-left">
|
||||||
|
<h4 className="text-[11px] font-bold text-gray-400 uppercase tracking-widest mb-3">Categories</h4>
|
||||||
|
<ul className="space-y-3">
|
||||||
|
{categories.map((cat) => (
|
||||||
|
<li key={cat.name}>
|
||||||
|
<a href="#" className="flex items-center gap-3 text-sm font-semibold text-gray-650 hover:text-primary transition-colors">
|
||||||
|
<span className="text-gray-400">{cat.icon}</span>
|
||||||
|
<span>{cat.name}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="p-4 border-t border-gray-100 text-xs text-gray-400 text-center font-medium bg-gray-50">
|
||||||
|
© {new Date().getFullYear()} TechShop. All Rights Reserved.
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
};
|
||||||
209
NewFile/src/src/components/ProductCard.tsx
Normal file
209
NewFile/src/src/components/ProductCard.tsx
Normal file
|
|
@ -0,0 +1,209 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { Heart, Eye, ShoppingCart } from 'lucide-react';
|
||||||
|
import { Product } from '../types';
|
||||||
|
import { RatingStars } from './RatingStars';
|
||||||
|
import { CountdownTimer } from './CountdownTimer';
|
||||||
|
|
||||||
|
interface ProductCardProps {
|
||||||
|
product: Product;
|
||||||
|
onAddToCart?: (product: Product) => void;
|
||||||
|
onAddToWishlist?: (product: Product) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProductCard: React.FC<ProductCardProps> = ({
|
||||||
|
product,
|
||||||
|
onAddToCart,
|
||||||
|
onAddToWishlist,
|
||||||
|
}) => {
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
const [isWishlisted, setIsWishlisted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkWishlist = () => {
|
||||||
|
const saved = localStorage.getItem('wishlist');
|
||||||
|
if (saved) {
|
||||||
|
try {
|
||||||
|
const list: string[] = JSON.parse(saved);
|
||||||
|
const active = list.includes(product.id) || list.includes(product.id.replace('prod-', ''));
|
||||||
|
setIsWishlisted(active);
|
||||||
|
} catch (e) {
|
||||||
|
setIsWishlisted(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setIsWishlisted(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
checkWishlist();
|
||||||
|
window.addEventListener('storage', checkWishlist);
|
||||||
|
window.addEventListener('wishlistUpdated', checkWishlist);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('storage', checkWishlist);
|
||||||
|
window.removeEventListener('wishlistUpdated', checkWishlist);
|
||||||
|
};
|
||||||
|
}, [product.id]);
|
||||||
|
|
||||||
|
const handleCartClick = (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (onAddToCart) {
|
||||||
|
onAddToCart(product);
|
||||||
|
} else {
|
||||||
|
alert(`Added ${product.name} to cart!`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWishlistClick = (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
let saved = localStorage.getItem('wishlist');
|
||||||
|
let list: string[] = [];
|
||||||
|
if (saved) {
|
||||||
|
try {
|
||||||
|
list = JSON.parse(saved);
|
||||||
|
} catch (err) {
|
||||||
|
list = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const cleanNumId = product.id.replace('prod-', '');
|
||||||
|
const isPresent = list.includes(product.id) || list.includes(cleanNumId);
|
||||||
|
if (isPresent) {
|
||||||
|
list = list.filter((id) => id !== product.id && id !== cleanNumId);
|
||||||
|
} else {
|
||||||
|
list.push(product.id);
|
||||||
|
}
|
||||||
|
localStorage.setItem('wishlist', JSON.stringify(list));
|
||||||
|
setIsWishlisted(!isPresent);
|
||||||
|
window.dispatchEvent(new Event('wishlistUpdated'));
|
||||||
|
window.dispatchEvent(new Event('openWishlistModal'));
|
||||||
|
if (onAddToWishlist) onAddToWishlist(product);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Determine price text representation
|
||||||
|
const isRange = product.name.includes('Ceptics') || product.name.includes('Lenovo');
|
||||||
|
const priceDisplay = isRange
|
||||||
|
? `$300 – $1,500`
|
||||||
|
: `$${product.price.toLocaleString()}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
|
className="bg-white p-5 flex flex-col justify-between relative overflow-visible transition-all duration-300 min-h-[390px] font-sans hover:shadow-soft-hover group/card border-r border-b border-gray-200 rounded-none"
|
||||||
|
>
|
||||||
|
{/* Top badges & actions */}
|
||||||
|
<div className="flex justify-between items-start z-10 w-full mb-1">
|
||||||
|
{/* Discount Tag */}
|
||||||
|
{product.discount ? (
|
||||||
|
<span className="bg-primary text-white text-[10px] font-extrabold px-2 py-0.5 rounded-md">
|
||||||
|
-{product.discount}%
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Hover action flyouts */}
|
||||||
|
<div className={`flex flex-col gap-1.5 transition-all duration-300 absolute right-3 top-3 z-20 ${isHovered ? 'opacity-100 translate-x-0' : 'opacity-0 translate-x-2 pointer-events-none'}`}>
|
||||||
|
<button
|
||||||
|
onClick={handleWishlistClick}
|
||||||
|
className={`w-8 h-8 rounded-full border flex items-center justify-center shadow-sm transition-all cursor-pointer ${
|
||||||
|
isWishlisted ? 'bg-primary text-white border-primary' : 'bg-white hover:bg-primary hover:text-white text-gray-500 border-gray-150'
|
||||||
|
}`}
|
||||||
|
title={isWishlisted ? "Remove from Wishlist" : "Add to Wishlist"}
|
||||||
|
>
|
||||||
|
<Heart size={13} className={isWishlisted ? 'fill-white' : ''} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
window.dispatchEvent(new CustomEvent('openQuickView', { detail: product }));
|
||||||
|
}}
|
||||||
|
className="w-8 h-8 rounded-full bg-white hover:bg-primary hover:text-white text-gray-500 border border-gray-150 flex items-center justify-center shadow-sm transition-all cursor-pointer"
|
||||||
|
title="Quick View"
|
||||||
|
>
|
||||||
|
<Eye size={13} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Product Image & Timer Container */}
|
||||||
|
<Link href={`/product/${product.id}`} className="w-full flex-1 flex flex-col items-center justify-center relative min-h-[160px] mb-3 select-none block">
|
||||||
|
{/* Default Image */}
|
||||||
|
<img
|
||||||
|
src={product.image}
|
||||||
|
alt={product.name}
|
||||||
|
className={`max-h-[140px] max-w-full object-contain drop-shadow-md rounded-lg transition-opacity duration-300 absolute ${
|
||||||
|
product.imageHover && isHovered ? 'opacity-0' : 'opacity-100'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Hover Swap Image */}
|
||||||
|
{product.imageHover && (
|
||||||
|
<img
|
||||||
|
src={product.imageHover}
|
||||||
|
alt={product.name}
|
||||||
|
className={`max-h-[140px] max-w-full object-contain drop-shadow-md rounded-lg transition-opacity duration-300 ${
|
||||||
|
isHovered ? 'opacity-100' : 'opacity-0 pointer-events-none'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Countdown Timer (Bottom of Image) */}
|
||||||
|
{product.hasTimer && product.timeRemaining && (
|
||||||
|
<div className="absolute bottom-0 left-1/2 transform -translate-x-1/2 bg-white/95 backdrop-blur-xs px-2.5 py-1 rounded-xl border border-red-100 shadow-md z-10">
|
||||||
|
<CountdownTimer
|
||||||
|
initialDays={product.timeRemaining.days}
|
||||||
|
initialHours={product.timeRemaining.hours}
|
||||||
|
initialMinutes={product.timeRemaining.minutes}
|
||||||
|
initialSeconds={product.timeRemaining.seconds}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Name, Rating & Price */}
|
||||||
|
<div className="text-left w-full mt-2">
|
||||||
|
{/* Title */}
|
||||||
|
<Link href={`/product/${product.id}`}>
|
||||||
|
<h3 className="text-[12.5px] font-bold text-gray-800 line-clamp-2 leading-snug hover:text-primary transition-colors cursor-pointer min-h-[34px] mb-1.5" title={product.name}>
|
||||||
|
{product.name}
|
||||||
|
</h3>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Rating */}
|
||||||
|
<RatingStars rating={product.rating} size={12} className="mb-1" />
|
||||||
|
|
||||||
|
{/* Price & Old Price */}
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<span className="text-[13.5px] font-extrabold text-gray-900">
|
||||||
|
{priceDisplay}
|
||||||
|
</span>
|
||||||
|
{product.oldPrice && !isRange && (
|
||||||
|
<span className="text-xs text-gray-400 line-through font-medium">
|
||||||
|
${product.oldPrice.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Add To Cart slide-up action */}
|
||||||
|
<div
|
||||||
|
className={`w-full relative overflow-hidden transition-all duration-300 ${
|
||||||
|
isHovered ? 'h-[38px] mt-2' : 'h-0'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={handleCartClick}
|
||||||
|
className="w-full bg-primary hover:bg-black text-white text-[11px] font-bold py-2.5 px-4 rounded-lg flex items-center justify-center gap-2 shadow-sm transition-all duration-300"
|
||||||
|
>
|
||||||
|
<ShoppingCart size={13} className="stroke-[2.5]" />
|
||||||
|
Add To Cart
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
189
NewFile/src/src/components/QuickViewModal.tsx
Normal file
189
NewFile/src/src/components/QuickViewModal.tsx
Normal file
|
|
@ -0,0 +1,189 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Star, X } from 'lucide-react';
|
||||||
|
import { Product } from '../types';
|
||||||
|
|
||||||
|
interface QuickViewModalProps {
|
||||||
|
product: Product;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QuickViewModal: React.FC<QuickViewModalProps> = ({ product, onClose }) => {
|
||||||
|
const [activeImgIndex, setActiveImgIndex] = useState(0);
|
||||||
|
const images = product.imageHover ? [product.image, product.imageHover] : [product.image];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handlePrev = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setActiveImgIndex((prev) => (prev === 0 ? images.length - 1 : prev - 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNext = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setActiveImgIndex((prev) => (prev === images.length - 1 ? 0 : prev + 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
const ratingsCount = 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={onClose}
|
||||||
|
className="fixed inset-0 bg-black/75 backdrop-blur-xs flex items-center justify-center z-[200] p-4 font-sans"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
className="bg-white w-full max-w-4xl max-h-[90vh] overflow-y-auto rounded-lg shadow-2xl relative flex flex-col md:flex-row text-left animate-in fade-in zoom-in duration-200"
|
||||||
|
>
|
||||||
|
{/* Close Button */}
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="absolute top-4 right-4 text-gray-400 hover:text-black transition-colors cursor-pointer z-30"
|
||||||
|
title="Close Modal"
|
||||||
|
>
|
||||||
|
<X size={20} className="stroke-[2.5]" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Left Side: Product Image Slider */}
|
||||||
|
<div className="w-full md:w-1/2 p-6 flex flex-col items-center justify-center border-r border-gray-100 min-h-[350px] relative select-none bg-white">
|
||||||
|
<div className="w-full h-72 flex items-center justify-center relative">
|
||||||
|
<img
|
||||||
|
src={images[activeImgIndex]}
|
||||||
|
alt={product.name}
|
||||||
|
className="max-h-full max-w-full object-contain transition-all duration-300"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{images.length > 1 && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={handlePrev}
|
||||||
|
className="absolute left-2 top-1/2 transform -translate-y-1/2 bg-[#0073EC] hover:bg-[#005bb5] text-white w-9 h-9 rounded-full flex items-center justify-center cursor-pointer transition-all shadow-md font-bold text-lg select-none z-10"
|
||||||
|
>
|
||||||
|
‹
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleNext}
|
||||||
|
className="absolute right-2 top-1/2 transform -translate-y-1/2 bg-[#0073EC] hover:bg-[#005bb5] text-white w-9 h-9 rounded-full flex items-center justify-center cursor-pointer transition-all shadow-md font-bold text-lg select-none z-10"
|
||||||
|
>
|
||||||
|
›
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Dots Indicator */}
|
||||||
|
{images.length > 1 && (
|
||||||
|
<div className="flex gap-1.5 mt-4">
|
||||||
|
{images.map((_, idx) => (
|
||||||
|
<button
|
||||||
|
key={idx}
|
||||||
|
onClick={(e) => { e.stopPropagation(); setActiveImgIndex(idx); }}
|
||||||
|
className={`w-2 h-2 rounded-full cursor-pointer transition-all ${
|
||||||
|
activeImgIndex === idx ? 'bg-black w-4' : 'bg-gray-300'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Side: Details & Options */}
|
||||||
|
<div className="w-full md:w-1/2 p-8 flex flex-col justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold text-gray-900 pr-6 mb-2 font-outfit">
|
||||||
|
{product.name}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{/* Ratings */}
|
||||||
|
<div className="flex items-center gap-1.5 mb-4">
|
||||||
|
<div className="flex items-center gap-0.5">
|
||||||
|
{[...Array(5)].map((_, i) => (
|
||||||
|
<Star
|
||||||
|
key={i}
|
||||||
|
size={13}
|
||||||
|
className={i < Math.floor(product.rating) ? "text-amber-400 fill-amber-400" : "text-gray-200"}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-[#0073EC] font-semibold cursor-pointer hover:underline">
|
||||||
|
({ratingsCount} review)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Price */}
|
||||||
|
<p className="text-lg font-black text-gray-950 mb-5">
|
||||||
|
{product.name.includes('Airpods Pro')
|
||||||
|
? '$299 – $1,900'
|
||||||
|
: product.name.includes('Apple Watch')
|
||||||
|
? '$669 – $700'
|
||||||
|
: `$${product.price.toLocaleString()}`}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Specs Bullet Points */}
|
||||||
|
<ul className="text-gray-500 text-[13px] font-semibold space-y-2 mb-6 list-disc list-inside">
|
||||||
|
<li>Functional Crown</li>
|
||||||
|
<li>Fhd Footage</li>
|
||||||
|
<li>1.78" Amoled</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Cross-Sell Bundles list */}
|
||||||
|
<div className="mt-4 pt-4 border-t border-gray-100">
|
||||||
|
<div className="border border-gray-150 rounded-lg p-3 bg-white space-y-3">
|
||||||
|
{/* Bundle item 1 */}
|
||||||
|
<div className="flex items-center justify-between gap-3 text-left">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<img
|
||||||
|
src="https://images.unsplash.com/photo-1579586337278-3befd40fd17a?w=100&auto=format&fit=crop&q=60"
|
||||||
|
alt="Raptag Screen Guard"
|
||||||
|
className="w-10 h-10 object-contain rounded-md border border-gray-100"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-xs font-bold text-gray-800 line-clamp-1">Raptag Screen Guard</h4>
|
||||||
|
<p className="text-[11px] text-gray-500 font-bold">
|
||||||
|
<span className="text-gray-400 line-through mr-1">$2,000</span>
|
||||||
|
<span className="text-gray-800">$1,900</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
defaultValue={0}
|
||||||
|
min={0}
|
||||||
|
className="w-11 text-center border border-gray-250 py-1 rounded text-xs font-bold focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bundle item 2 */}
|
||||||
|
<div className="flex items-center justify-between gap-3 text-left pt-3 border-t border-gray-100">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<img
|
||||||
|
src="https://images.unsplash.com/photo-1543512214-318c7553f230?w=100&auto=format&fit=crop&q=60"
|
||||||
|
alt="Google Home Smart Speaker"
|
||||||
|
className="w-10 h-10 object-contain rounded-md border border-gray-100"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-xs font-bold text-gray-800 line-clamp-1">Google Home Smart...</h4>
|
||||||
|
<p className="text-[11px] text-gray-800 font-bold">$300</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
defaultValue={0}
|
||||||
|
min={0}
|
||||||
|
className="w-11 text-center border border-gray-250 py-1 rounded text-xs font-bold focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
43
NewFile/src/src/components/RatingStars.tsx
Normal file
43
NewFile/src/src/components/RatingStars.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Star } from 'lucide-react';
|
||||||
|
|
||||||
|
interface RatingStarsProps {
|
||||||
|
rating: number;
|
||||||
|
max?: number;
|
||||||
|
size?: number;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RatingStars: React.FC<RatingStarsProps> = ({
|
||||||
|
rating,
|
||||||
|
max = 5,
|
||||||
|
size = 16,
|
||||||
|
className = '',
|
||||||
|
}) => {
|
||||||
|
const stars = [];
|
||||||
|
const roundedRating = Math.round(rating * 2) / 2;
|
||||||
|
|
||||||
|
for (let i = 1; i <= max; i++) {
|
||||||
|
const isFull = i <= roundedRating;
|
||||||
|
const isHalf = !isFull && i - 0.5 <= roundedRating;
|
||||||
|
|
||||||
|
stars.push(
|
||||||
|
<span key={i} className="text-[#FFB21E] inline-flex">
|
||||||
|
{isFull ? (
|
||||||
|
<Star size={size} fill="currentColor" stroke="currentColor" />
|
||||||
|
) : isHalf ? (
|
||||||
|
<span className="relative inline-flex" style={{ width: size, height: size }}>
|
||||||
|
<Star size={size} className="absolute top-0 left-0 text-gray-300" />
|
||||||
|
<span className="absolute top-0 left-0 overflow-hidden" style={{ width: '50%' }}>
|
||||||
|
<Star size={size} fill="currentColor" stroke="currentColor" />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<Star size={size} className="text-gray-300" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className={`flex items-center gap-0.5 ${className}`}>{stars}</div>;
|
||||||
|
};
|
||||||
70
NewFile/src/src/components/SearchBar.tsx
Normal file
70
NewFile/src/src/components/SearchBar.tsx
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { Search, ChevronDown } from 'lucide-react';
|
||||||
|
|
||||||
|
export const SearchBar: React.FC = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const [category, setCategory] = useState('All Categories');
|
||||||
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
|
const categories = [
|
||||||
|
'All Categories',
|
||||||
|
'TV & Speaker',
|
||||||
|
'Smart Devices',
|
||||||
|
'Chargers & Cables',
|
||||||
|
'Laptop & Computers',
|
||||||
|
'Phones',
|
||||||
|
'Cameras',
|
||||||
|
'Smart Watches',
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleSearchSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const catQuery = category === 'All Categories' ? '' : `category=${encodeURIComponent(category)}`;
|
||||||
|
const searchQueryStr = searchQuery.trim() ? `search=${encodeURIComponent(searchQuery.trim())}` : '';
|
||||||
|
const queryParts = [catQuery, searchQueryStr].filter(Boolean).join('&');
|
||||||
|
const path = queryParts ? `/shop?${queryParts}` : '/shop';
|
||||||
|
router.push(path);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSearchSubmit} className="flex flex-1 max-w-[720px] items-center bg-white rounded-lg border border-gray-200 overflow-visible shadow-sm h-[46px] relative">
|
||||||
|
{/* Category Dropdown */}
|
||||||
|
<div className="relative h-full hidden lg:block">
|
||||||
|
<select
|
||||||
|
value={category}
|
||||||
|
onChange={(e) => setCategory(e.target.value)}
|
||||||
|
className="appearance-none bg-gray-50 border-r border-gray-150 h-full px-5 pr-8 text-[13px] font-semibold text-gray-700 hover:bg-gray-100 transition-colors focus:outline-none cursor-pointer rounded-l-lg font-sans"
|
||||||
|
>
|
||||||
|
{categories.map((cat) => (
|
||||||
|
<option key={cat} value={cat}>
|
||||||
|
{cat}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<ChevronDown size={14} className="text-gray-400 absolute right-3 top-1/2 transform -translate-y-1/2 pointer-events-none" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Input Field */}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search products..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="flex-1 px-4 text-[13px] text-gray-800 placeholder-gray-400 focus:outline-none h-full font-medium"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Search Button */}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="h-full bg-primary hover:bg-primary-hover text-white px-7 flex items-center justify-center font-bold text-sm transition-colors rounded-r-lg"
|
||||||
|
title="Search"
|
||||||
|
>
|
||||||
|
Search
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
171
NewFile/src/src/components/WishlistModal.tsx
Normal file
171
NewFile/src/src/components/WishlistModal.tsx
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Heart, X } from 'lucide-react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { products as initialProducts } from '../utils/mockData';
|
||||||
|
|
||||||
|
interface WishlistModalProps {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WishlistModal: React.FC<WishlistModalProps> = ({ onClose }) => {
|
||||||
|
const [wishlistIds, setWishlistIds] = useState<string[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
const loadWishlist = () => {
|
||||||
|
const saved = localStorage.getItem('wishlist');
|
||||||
|
if (saved) {
|
||||||
|
try {
|
||||||
|
const list = JSON.parse(saved);
|
||||||
|
setWishlistIds(Array.isArray(list) ? list : []);
|
||||||
|
} catch (e) {
|
||||||
|
setWishlistIds([]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setWishlistIds([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadWishlist();
|
||||||
|
window.addEventListener('storage', loadWishlist);
|
||||||
|
window.addEventListener('wishlistUpdated', loadWishlist);
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
window.removeEventListener('storage', loadWishlist);
|
||||||
|
window.removeEventListener('wishlistUpdated', loadWishlist);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleRemove = (id: string) => {
|
||||||
|
const updated = wishlistIds.filter(
|
||||||
|
(item) => item !== id && item !== id.replace('prod-', '') && `prod-${item}` !== id
|
||||||
|
);
|
||||||
|
setWishlistIds(updated);
|
||||||
|
localStorage.setItem('wishlist', JSON.stringify(updated));
|
||||||
|
window.dispatchEvent(new Event('wishlistUpdated'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const wishlistProducts = initialProducts.filter(
|
||||||
|
(p) => wishlistIds.includes(p.id) || wishlistIds.includes(p.id.replace('prod-', ''))
|
||||||
|
);
|
||||||
|
|
||||||
|
const getAddedDate = (id: string) => {
|
||||||
|
const dates: Record<string, string> = {
|
||||||
|
'prod-1': 'July 25, 2026',
|
||||||
|
'prod-2': 'July 25, 2026',
|
||||||
|
'prod-3': 'June 8, 2026',
|
||||||
|
'prod-4': 'March 22, 2026',
|
||||||
|
'prod-5': 'October 20, 2025',
|
||||||
|
'prod-6': 'October 8, 2025',
|
||||||
|
'prod-7': 'April 28, 2025',
|
||||||
|
};
|
||||||
|
return dates[id] || 'July 25, 2026';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={onClose}
|
||||||
|
className="fixed inset-0 bg-black/75 backdrop-blur-xs flex items-center justify-center z-[200] p-4 font-sans animate-in fade-in duration-200"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
className="bg-white w-full max-w-[740px] rounded-2xl shadow-2xl overflow-hidden relative flex flex-col max-h-[85vh] text-left"
|
||||||
|
>
|
||||||
|
{/* Header (Dark style matching mockup) */}
|
||||||
|
<div className="bg-[#1E1E1E] text-white px-7 py-4 flex items-center justify-between">
|
||||||
|
<h2 className="text-[16px] font-bold tracking-wide">
|
||||||
|
Wishlist ({wishlistProducts.length})
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="w-9 h-9 rounded-full flex items-center justify-center text-gray-400 hover:text-white transition-colors cursor-pointer -mr-2"
|
||||||
|
title="Close"
|
||||||
|
>
|
||||||
|
<X size={19} className="stroke-[2.5]" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Wishlist Items List */}
|
||||||
|
{wishlistProducts.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-20 text-gray-500 flex-grow bg-[#F8F9FB]">
|
||||||
|
<Heart size={40} className="text-gray-300 mb-3 stroke-[1.5]" />
|
||||||
|
<p className="text-[13px] font-semibold">Your wishlist is empty!</p>
|
||||||
|
<p className="text-[11px] text-gray-400 mt-1">Add items from the store to see them here.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="overflow-y-auto divide-y divide-gray-150 max-h-[495px] bg-white px-7 border-b border-gray-100"
|
||||||
|
>
|
||||||
|
{wishlistProducts.map((product) => {
|
||||||
|
const hasOldPrice =
|
||||||
|
product.oldPrice &&
|
||||||
|
!product.name.includes('Airpods') &&
|
||||||
|
!product.name.includes('Apple Watch');
|
||||||
|
return (
|
||||||
|
<div key={product.id} className="py-5.5 flex items-center gap-5 justify-between">
|
||||||
|
<div className="flex items-center gap-4 flex-grow min-w-0">
|
||||||
|
<img
|
||||||
|
src={product.image}
|
||||||
|
alt={product.name}
|
||||||
|
className="w-[72px] h-[72px] object-contain rounded-xl border border-gray-150 bg-white p-2 flex-shrink-0 shadow-xs"
|
||||||
|
/>
|
||||||
|
<div className="min-w-0 pr-2">
|
||||||
|
{/* Title Link (Blue) */}
|
||||||
|
<Link
|
||||||
|
href={`/product/${product.id}`}
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-[15px] font-bold text-[#0073EC] hover:text-black line-clamp-1 block transition-colors leading-snug mb-1"
|
||||||
|
>
|
||||||
|
{product.name}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Price display */}
|
||||||
|
<p className="text-[14.5px] font-extrabold text-gray-950 mb-1">
|
||||||
|
{hasOldPrice && (
|
||||||
|
<span className="text-gray-400 line-through mr-2 font-medium text-xs">
|
||||||
|
${product.oldPrice?.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span>
|
||||||
|
{product.name.includes('Airpods')
|
||||||
|
? '$299 – $1,900'
|
||||||
|
: product.name.includes('Apple Watch')
|
||||||
|
? '$669 – $700'
|
||||||
|
: `$${product.price.toLocaleString()}`}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Date */}
|
||||||
|
<p className="text-xs text-gray-400 font-medium">
|
||||||
|
{getAddedDate(product.id)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Right Buttons */}
|
||||||
|
<div className="flex items-center gap-3.5 flex-shrink-0">
|
||||||
|
<button
|
||||||
|
onClick={() => alert(`Added ${product.name} to cart!`)}
|
||||||
|
className="bg-[#0073EC] hover:bg-black text-white text-[12.5px] font-bold px-5 py-3 rounded-xl transition-colors cursor-pointer select-none shadow-xs whitespace-nowrap"
|
||||||
|
>
|
||||||
|
Move to Cart
|
||||||
|
</button>
|
||||||
|
{/* Inline trash close icon aligned with header X */}
|
||||||
|
<button
|
||||||
|
onClick={() => handleRemove(product.id)}
|
||||||
|
className="w-9 h-9 rounded-full flex items-center justify-center text-gray-400 hover:text-red-500 transition-colors cursor-pointer -mr-2"
|
||||||
|
title="Remove Item"
|
||||||
|
>
|
||||||
|
<X size={18} className="stroke-[2.5]" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
36
NewFile/src/src/layout/AnnouncementBar.tsx
Normal file
36
NewFile/src/src/layout/AnnouncementBar.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Truck, Package } from 'lucide-react';
|
||||||
|
|
||||||
|
export const AnnouncementBar: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="bg-white border-b border-gray-200 py-2.5 text-[12px] text-gray-500 font-medium z-50">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 flex flex-col md:flex-row justify-between items-center gap-2">
|
||||||
|
{/* Left Side Links */}
|
||||||
|
<div className="flex flex-wrap items-center gap-x-5 gap-y-1 justify-center md:justify-start">
|
||||||
|
<a href="#" className="hover:text-primary transition-colors flex items-center gap-1.5">
|
||||||
|
<Package size={13} className="text-gray-400" />
|
||||||
|
Track Order
|
||||||
|
</a>
|
||||||
|
<a href="#" className="hover:text-primary transition-colors">
|
||||||
|
About Us
|
||||||
|
</a>
|
||||||
|
<a href="#" className="hover:text-primary transition-colors">
|
||||||
|
Blog
|
||||||
|
</a>
|
||||||
|
<a href="#" className="hover:text-primary transition-colors">
|
||||||
|
Contact Us
|
||||||
|
</a>
|
||||||
|
<a href="#" className="hover:text-primary transition-colors">
|
||||||
|
FAQs
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Side Info */}
|
||||||
|
<div className="flex items-center gap-1.5 text-gray-600 font-semibold text-center text-[12px]">
|
||||||
|
<Truck size={14} className="text-primary" />
|
||||||
|
<span>Free Shipping on orders over <span className="text-primary font-bold">$100!</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
207
NewFile/src/src/layout/Footer.tsx
Normal file
207
NewFile/src/src/layout/Footer.tsx
Normal file
|
|
@ -0,0 +1,207 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Mail } from 'lucide-react';
|
||||||
|
|
||||||
|
export const Footer: React.FC = () => {
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
|
||||||
|
const handleSubscribe = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (email.trim()) {
|
||||||
|
alert(`Thank you for subscribing with: ${email}`);
|
||||||
|
setEmail('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer className="bg-white border-t border-gray-200 font-sans">
|
||||||
|
|
||||||
|
{/* 1. Bright Blue Newsletter Banner */}
|
||||||
|
<div className="bg-[#0073EC] py-10 text-white">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1400px] flex flex-col lg:flex-row items-center justify-between gap-6">
|
||||||
|
{/* Left Text Block with Mail Envelope Icon */}
|
||||||
|
<div className="flex items-center gap-4 text-left">
|
||||||
|
<div className="w-12 h-12 flex-shrink-0 flex items-center justify-center border-2 border-white/80 rounded-lg">
|
||||||
|
<Mail className="w-6 h-6 stroke-[1.5]" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-[17px] font-extrabold tracking-tight">
|
||||||
|
Join our newsletter and get $20 discount!
|
||||||
|
</h3>
|
||||||
|
<p className="text-[12px] text-blue-100 font-medium">
|
||||||
|
Get E-mail updates about our latest shop and special offers.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Input and Subscribe Form */}
|
||||||
|
<form onSubmit={handleSubscribe} className="w-full lg:w-[550px] max-w-[600px] flex items-center bg-white rounded-lg p-1 shadow-sm">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
placeholder="Your email address"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
className="flex-grow px-4 py-1.5 bg-transparent text-gray-800 placeholder-gray-400 focus:outline-none text-[13px] font-medium"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="bg-[#1E1E1E] hover:bg-primary text-white font-extrabold text-[11px] uppercase tracking-wider px-6 py-2.5 rounded-md transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
Subscribe
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 2. Main Footer Links & Contact Info */}
|
||||||
|
<div className="py-16 bg-[#F8F9FB] border-b border-gray-150">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1400px]">
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-8">
|
||||||
|
|
||||||
|
{/* Column 1: Contact Us */}
|
||||||
|
<div className="flex flex-col gap-4 text-left">
|
||||||
|
<h4 className="text-[12.5px] font-bold text-[#1E293B] tracking-[0.05em] uppercase font-sans">Contact Us</h4>
|
||||||
|
|
||||||
|
<div className="text-[17px] font-bold text-gray-900 font-sans tracking-tight leading-tight">
|
||||||
|
(+00) 123-456-789
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-[13px] text-gray-500 font-semibold font-sans leading-relaxed max-w-[220px]">
|
||||||
|
99 New Theme St. XY, USA 12345, Beside the Sun point land.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="mailto:demo@example.com"
|
||||||
|
className="text-[13px] text-gray-500 font-semibold underline hover:text-[#0073EC] transition-colors font-sans"
|
||||||
|
>
|
||||||
|
demo@example.com
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{/* Social Media Links */}
|
||||||
|
<div className="flex items-center gap-4 mt-4">
|
||||||
|
<a href="#" className="text-gray-600 hover:text-[#0073EC] transition-colors" title="Facebook">
|
||||||
|
<svg className="w-5 h-5 fill-current" viewBox="0 0 24 24">
|
||||||
|
<path d="M9 8h-3v4h3v12h5v-12h3.642l.358-4h-4v-1.667c0-.955.192-1.333 1.115-1.333h2.885v-5h-3.808c-3.596 0-5.192 1.583-5.192 4.615v3.385z"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a href="#" className="text-gray-600 hover:text-[#0073EC] transition-colors" title="Twitter/X">
|
||||||
|
<svg className="w-4.5 h-4.5 fill-current" viewBox="0 0 24 24">
|
||||||
|
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a href="#" className="text-gray-600 hover:text-[#0073EC] transition-colors" title="Instagram">
|
||||||
|
<svg className="w-5 h-5 fill-current" viewBox="0 0 24 24">
|
||||||
|
<path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a href="#" className="text-gray-600 hover:text-[#0073EC] transition-colors" title="TikTok">
|
||||||
|
<svg className="w-4.5 h-4.5 fill-current" viewBox="0 0 24 24">
|
||||||
|
<path d="M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.17-2.86-.74-3.99-1.72-.08-.07-.17-.17-.24-.25v5.13c-.02 3.42-1.92 6.78-5.26 7.82-3.34 1.04-7.25-.33-8.81-3.41-1.57-3.07-.48-7.3 2.5-8.88 1.4-.74 3.01-.93 4.55-.58v4.01c-.88-.23-1.84-.13-2.63.34-.8.47-1.34 1.34-1.37 2.27-.02 1.32.96 2.55 2.27 2.76 1.3.2 2.6-.62 2.97-1.87.12-.41.15-.85.15-1.28V.02z"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a href="#" className="text-gray-600 hover:text-[#0073EC] transition-colors" title="Pinterest">
|
||||||
|
<svg className="w-4.5 h-4.5 fill-current" viewBox="0 0 24 24">
|
||||||
|
<path d="M12 0c-6.627 0-12 5.372-12 12 0 5.084 3.163 9.426 7.627 11.174-.105-.949-.2-2.405.042-3.441.218-.937 1.407-5.965 1.407-5.965s-.359-.719-.359-1.782c0-1.668.967-2.914 2.171-2.914 1.023 0 1.518.769 1.518 1.69 0 1.029-.655 2.568-.993 3.994-.283 1.194.599 2.169 1.777 2.169 2.133 0 3.772-2.249 3.772-5.495 0-2.873-2.064-4.882-5.012-4.882-3.414 0-5.418 2.561-5.418 5.207 0 1.031.397 2.138.893 2.738.098.119.112.224.083.345-.091.377-.294 1.197-.333 1.36-.053.218-.173.264-.399.158-1.492-.695-2.424-2.876-2.424-4.629 0-3.769 2.739-7.229 7.892-7.229 4.144 0 7.365 2.953 7.365 6.899 0 4.117-2.595 7.431-6.199 7.431-1.21 0-2.348-.628-2.738-1.373 0 0-.599 2.282-.744 2.84-.282 1.084-1.047 2.443-1.558 3.272 1.124.348 2.316.537 3.551.537 6.628 0 12-5.373 12-12 0-6.628-5.372-12-12-12z"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Column 2: Get to know Us */}
|
||||||
|
<div className="text-left">
|
||||||
|
<h4 className="text-[12.5px] font-bold text-[#1E293B] tracking-[0.05em] uppercase mb-5 font-sans">Get to know Us</h4>
|
||||||
|
<ul className="space-y-3.5 text-[13px] font-semibold text-gray-500 font-sans">
|
||||||
|
<li><a href="#" className="hover:text-[#0073EC] transition-colors">About Us</a></li>
|
||||||
|
<li><a href="#" className="hover:text-[#0073EC] transition-colors">Term & Policy</a></li>
|
||||||
|
<li><a href="#" className="hover:text-[#0073EC] transition-colors">Careers</a></li>
|
||||||
|
<li><a href="#" className="hover:text-[#0073EC] transition-colors">News & Blog</a></li>
|
||||||
|
<li><a href="#" className="hover:text-[#0073EC] transition-colors">Contact Us</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Column 3: Information */}
|
||||||
|
<div className="text-left">
|
||||||
|
<h4 className="text-[12.5px] font-bold text-[#1E293B] tracking-[0.05em] uppercase mb-5 font-sans">Information</h4>
|
||||||
|
<ul className="space-y-3.5 text-[13px] font-semibold text-gray-500 font-sans">
|
||||||
|
<li><a href="#" className="hover:text-[#0073EC] transition-colors">Help Center</a></li>
|
||||||
|
<li><a href="#" className="hover:text-[#0073EC] transition-colors">Feedback</a></li>
|
||||||
|
<li><a href="#" className="hover:text-[#0073EC] transition-colors">FAQs</a></li>
|
||||||
|
<li><a href="#" className="hover:text-[#0073EC] transition-colors">Size Guide</a></li>
|
||||||
|
<li><a href="#" className="hover:text-[#0073EC] transition-colors">Payments</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Column 4: Orders & Returns */}
|
||||||
|
<div className="text-left">
|
||||||
|
<h4 className="text-[12.5px] font-bold text-[#1E293B] tracking-[0.05em] uppercase mb-5 font-sans">Orders & Returns</h4>
|
||||||
|
<ul className="space-y-3.5 text-[13px] font-semibold text-gray-500 font-sans">
|
||||||
|
<li><a href="#" className="hover:text-[#0073EC] transition-colors">Track Order</a></li>
|
||||||
|
<li><a href="#" className="hover:text-[#0073EC] transition-colors">Delivery</a></li>
|
||||||
|
<li><a href="#" className="hover:text-[#0073EC] transition-colors">Services</a></li>
|
||||||
|
<li><a href="#" className="hover:text-[#0073EC] transition-colors">Returns</a></li>
|
||||||
|
<li><a href="#" className="hover:text-[#0073EC] transition-colors">Exchange</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Column 5: Our Store */}
|
||||||
|
<div className="text-left">
|
||||||
|
<h4 className="text-[12.5px] font-bold text-[#1E293B] tracking-[0.05em] uppercase mb-5 font-sans">Our Store</h4>
|
||||||
|
<ul className="space-y-3.5 text-[13px] font-semibold text-gray-500 font-sans">
|
||||||
|
<li><a href="#" className="hover:text-[#0073EC] transition-colors">Affiliate</a></li>
|
||||||
|
<li><a href="#" className="hover:text-[#0073EC] transition-colors">Best Seller</a></li>
|
||||||
|
<li><a href="#" className="hover:text-[#0073EC] transition-colors">New Products</a></li>
|
||||||
|
<li><a href="#" className="hover:text-[#0073EC] transition-colors">On Sale</a></li>
|
||||||
|
<li><a href="#" className="hover:text-[#0073EC] transition-colors">Featured</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 3. Bottom Bar with copyright and payment methods */}
|
||||||
|
<div className="py-8 bg-[#F0F2F5]">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1400px] flex flex-col md:flex-row justify-between items-center gap-4">
|
||||||
|
<p className="text-[12.5px] text-gray-500 font-medium text-center md:text-left">
|
||||||
|
© 2026 ifixkart
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Payment Card Logos */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{/* Visa */}
|
||||||
|
<div className="bg-white border border-gray-200 rounded px-2.5 py-1 text-[#1A1F71] font-bold italic text-[11px] shadow-2xs select-none" title="Visa">
|
||||||
|
Visa
|
||||||
|
</div>
|
||||||
|
{/* CB */}
|
||||||
|
<div className="bg-white border border-gray-200 rounded px-2.5 py-1 text-emerald-600 font-black text-[11px] shadow-2xs select-none" title="CB">
|
||||||
|
CB
|
||||||
|
</div>
|
||||||
|
{/* Amex */}
|
||||||
|
<div className="bg-white border border-gray-200 rounded px-2.5 py-1 text-[#0070CD] font-bold text-[10px] shadow-2xs select-none" title="American Express">
|
||||||
|
Amex
|
||||||
|
</div>
|
||||||
|
{/* Maestro */}
|
||||||
|
<div className="bg-white border border-gray-200 rounded px-2 py-0.5 shadow-2xs select-none flex items-center gap-1" title="Maestro">
|
||||||
|
<span className="w-2.5 h-2.5 rounded-full bg-[#EB001B] opacity-80" />
|
||||||
|
<span className="w-2.5 h-2.5 rounded-full bg-[#00A2E8] -ml-2 opacity-80" />
|
||||||
|
<span className="text-[10px] font-bold text-gray-500">maestro</span>
|
||||||
|
</div>
|
||||||
|
{/* Mastercard */}
|
||||||
|
<div className="bg-white border border-gray-200 rounded px-2 py-0.5 shadow-2xs select-none flex items-center gap-1" title="Mastercard">
|
||||||
|
<span className="w-2.5 h-2.5 rounded-full bg-[#EB001B] opacity-90" />
|
||||||
|
<span className="w-2.5 h-2.5 rounded-full bg-[#F79E1B] -ml-2 opacity-90" />
|
||||||
|
<span className="text-[10px] font-bold text-gray-500">mastercard</span>
|
||||||
|
</div>
|
||||||
|
{/* PayPal */}
|
||||||
|
<div className="bg-white border border-gray-200 rounded px-2.5 py-1 text-[#003087] font-black italic text-[11px] shadow-2xs select-none" title="PayPal">
|
||||||
|
PayPal
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
};
|
||||||
124
NewFile/src/src/layout/Header.tsx
Normal file
124
NewFile/src/src/layout/Header.tsx
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { Heart, User, ShoppingCart, Menu, X, Search } from 'lucide-react';
|
||||||
|
import { SearchBar } from '../components/SearchBar';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
|
||||||
|
interface HeaderProps {
|
||||||
|
onMenuToggle: () => void;
|
||||||
|
cartCount: number;
|
||||||
|
wishlistCount: number;
|
||||||
|
onWishlistClick?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Header: React.FC<HeaderProps> = ({ onMenuToggle, cartCount = 0, wishlistCount = 0, onWishlistClick }) => {
|
||||||
|
const [mobileSearchOpen, setMobileSearchOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="bg-white pt-6 pb-4 relative z-[110]">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 flex items-center justify-between gap-5">
|
||||||
|
{/* Mobile Hamburger Menu */}
|
||||||
|
<button
|
||||||
|
onClick={onMenuToggle}
|
||||||
|
className="lg:hidden p-2 text-gray-600 hover:text-primary transition-colors hover:bg-gray-50 rounded-lg"
|
||||||
|
aria-label="Open Menu"
|
||||||
|
>
|
||||||
|
<Menu size={24} className="stroke-[2.5]" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Brand Logo */}
|
||||||
|
<Link href="/" className="flex items-center select-none tracking-tighter hover:opacity-95 transition-opacity">
|
||||||
|
<span className="text-[#0073EC] text-[38px] font-black leading-none">iFix</span>
|
||||||
|
<span className="text-gray-900 text-[38px] font-black leading-none">Kart</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Desktop Search Bar */}
|
||||||
|
<div className="hidden lg:block flex-grow max-w-[660px] mx-2 xl:mx-6">
|
||||||
|
<SearchBar />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions (Wishlist, Account, Cart) */}
|
||||||
|
<div className="flex items-center gap-2 md:gap-5 flex-shrink-0">
|
||||||
|
{/* Mobile Search Toggle */}
|
||||||
|
<button
|
||||||
|
onClick={() => setMobileSearchOpen(!mobileSearchOpen)}
|
||||||
|
className="lg:hidden p-2 text-gray-600 hover:text-primary transition-colors hover:bg-gray-50 rounded-lg"
|
||||||
|
>
|
||||||
|
<Search size={22} className="stroke-[2.5]" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Wishlist */}
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (onWishlistClick) {
|
||||||
|
onWishlistClick();
|
||||||
|
} else {
|
||||||
|
window.dispatchEvent(new Event('openWishlistModal'));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="relative p-2 text-gray-600 hover:text-primary transition-colors hover:bg-gray-50 rounded-lg group cursor-pointer focus:outline-none"
|
||||||
|
>
|
||||||
|
<Heart size={24} className="group-hover:scale-105 transition-transform" />
|
||||||
|
<span className="absolute top-0 right-0 w-5 h-5 bg-[#0073EC] text-white text-[10px] font-bold rounded-full flex items-center justify-center border-2 border-white shadow-xs">
|
||||||
|
{wishlistCount}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Account */}
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="flex items-center gap-2.5 p-2 text-gray-600 hover:text-primary transition-colors hover:bg-gray-50 rounded-lg group"
|
||||||
|
>
|
||||||
|
<User size={24} className="group-hover:scale-105 transition-transform text-gray-600 group-hover:text-primary transition-colors" />
|
||||||
|
<div className="text-left hidden md:block text-[11.5px] leading-tight font-medium">
|
||||||
|
<span className="text-gray-400 block font-normal">My Account</span>
|
||||||
|
<span className="text-gray-800 font-bold group-hover:text-primary transition-colors text-[13.5px]">Login</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{/* Cart */}
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="flex items-center gap-2.5 p-2 text-gray-600 hover:text-primary transition-colors hover:bg-gray-50 rounded-lg group relative"
|
||||||
|
>
|
||||||
|
<div className="relative">
|
||||||
|
<ShoppingCart size={24} className="group-hover:scale-105 transition-transform" />
|
||||||
|
<span className="absolute -top-1.5 -right-1.5 w-5 h-5 bg-[#0073EC] text-white text-[10px] font-bold rounded-full flex items-center justify-center border-2 border-white shadow-xs">
|
||||||
|
{cartCount}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-left hidden md:block text-[11.5px] leading-tight font-medium">
|
||||||
|
<span className="text-gray-400 block font-normal">My Cart</span>
|
||||||
|
<span className="text-gray-800 font-bold group-hover:text-primary transition-colors text-[13.5px]">$0.00</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Search Overlay */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{mobileSearchOpen && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, height: 0 }}
|
||||||
|
animate={{ opacity: 1, height: 'auto' }}
|
||||||
|
exit={{ opacity: 0, height: 0 }}
|
||||||
|
className="lg:hidden border-t border-gray-100 px-4 pt-3 pb-2 bg-white"
|
||||||
|
>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<SearchBar />
|
||||||
|
<button
|
||||||
|
onClick={() => setMobileSearchOpen(false)}
|
||||||
|
className="p-2 text-gray-400 hover:text-gray-650"
|
||||||
|
>
|
||||||
|
<X size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
101
NewFile/src/src/layout/Navbar.tsx
Normal file
101
NewFile/src/src/layout/Navbar.tsx
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { ChevronDown, Percent } from 'lucide-react';
|
||||||
|
import { CategorySidebar } from '../components/CategorySidebar';
|
||||||
|
|
||||||
|
export const Navbar: React.FC = () => {
|
||||||
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||||
|
const [isSticky, setIsSticky] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (window.scrollY > 120) {
|
||||||
|
setIsSticky(true);
|
||||||
|
} else {
|
||||||
|
setIsSticky(false);
|
||||||
|
}
|
||||||
|
// Always close dropdown on scroll
|
||||||
|
setDropdownOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('scroll', handleScroll);
|
||||||
|
return () => window.removeEventListener('scroll', handleScroll);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className={`bg-white hidden xl:block transition-all duration-200 z-[100] ${
|
||||||
|
isSticky
|
||||||
|
? 'fixed top-0 left-0 right-0 shadow-md py-0 border-b border-gray-150'
|
||||||
|
: 'relative pt-3.5'
|
||||||
|
}`}>
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 flex items-center justify-between">
|
||||||
|
<div className="flex items-center">
|
||||||
|
{/* All Categories Header Anchor Box with Dropdown */}
|
||||||
|
<div className="relative">
|
||||||
|
<div
|
||||||
|
onClick={() => setDropdownOpen(!dropdownOpen)}
|
||||||
|
className="w-[280px] bg-primary text-white py-4 px-6 font-bold text-[13px] flex items-center gap-3 rounded-t-lg select-none cursor-pointer"
|
||||||
|
>
|
||||||
|
<span className="flex flex-col gap-1 w-4">
|
||||||
|
<span className="h-[2px] bg-white w-full rounded" />
|
||||||
|
<span className="h-[2px] bg-white w-4/5 rounded" />
|
||||||
|
<span className="h-[2px] bg-white w-full rounded" />
|
||||||
|
</span>
|
||||||
|
All Categories
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isSticky && dropdownOpen && (
|
||||||
|
<div className="absolute top-full left-0 z-50 mt-0">
|
||||||
|
<CategorySidebar isDropdown={true} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Links */}
|
||||||
|
<div className="flex items-center gap-10 ml-10">
|
||||||
|
<Link href="/" className="text-primary font-bold text-[14px] hover:text-primary transition-colors py-[14.5px]">
|
||||||
|
Home
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Shop Dropdown */}
|
||||||
|
<div className="relative group py-[14.5px]">
|
||||||
|
<Link href="/shop" className="text-gray-700 font-bold text-[14px] hover:text-primary transition-colors flex items-center gap-1">
|
||||||
|
Shop
|
||||||
|
<ChevronDown size={14} className="text-gray-400 group-hover:text-primary transition-colors" />
|
||||||
|
</Link>
|
||||||
|
<div className="absolute top-[48px] left-0 bg-white border border-gray-100 shadow-lg py-2 rounded-lg w-48 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50">
|
||||||
|
<Link href="/shop?layout=grid" className="block px-4 py-2 text-xs font-semibold text-gray-600 hover:bg-gray-50 hover:text-primary transition-colors">Grid Layout</Link>
|
||||||
|
<Link href="/shop?layout=list" className="block px-4 py-2 text-xs font-semibold text-gray-600 hover:bg-gray-50 hover:text-primary transition-colors">List Layout</Link>
|
||||||
|
<Link href="#" className="block px-4 py-2 text-xs font-semibold text-gray-600 hover:bg-gray-50 hover:text-primary transition-colors">Product Detail Page</Link>
|
||||||
|
<Link href="#" className="block px-4 py-2 text-xs font-semibold text-gray-600 hover:bg-gray-50 hover:text-primary transition-colors">Cart Page</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Service */}
|
||||||
|
<a href="#" className="text-gray-700 font-bold text-[14px] hover:text-primary transition-colors py-[14.5px]">
|
||||||
|
Service
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{/* Top Deals */}
|
||||||
|
<a href="#" className="text-gray-700 font-bold text-[14px] hover:text-primary transition-colors py-[14.5px]">
|
||||||
|
Top Deals
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{/* Featured Products */}
|
||||||
|
<a href="#" className="text-gray-700 font-bold text-[14px] hover:text-primary transition-colors py-[14.5px]">
|
||||||
|
Featured Products
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Super Discount banner */}
|
||||||
|
<a href="#" className="flex items-center gap-2 text-red-500 hover:text-red-650 font-bold text-[13px] transition-colors py-[14.5px]">
|
||||||
|
<Percent size={14} className="stroke-[2.5] text-red-500 animate-pulse" />
|
||||||
|
Super Discount
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
10
NewFile/src/src/main.tsx
Normal file
10
NewFile/src/src/main.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
import './styles/index.css';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
79
NewFile/src/src/sections/ArticlesSection.tsx
Normal file
79
NewFile/src/src/sections/ArticlesSection.tsx
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { blogs } from '../utils/mockData';
|
||||||
|
import { ArrowRight } from 'lucide-react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
export const ArticlesSection: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<section className="py-8 bg-bgLight font-sans">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
|
||||||
|
{/* Section Header */}
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h2 className="text-xl md:text-2xl font-extrabold text-gray-900 tracking-tight font-outfit">
|
||||||
|
From Our Articles
|
||||||
|
</h2>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="text-xs font-bold text-gray-900 hover:text-primary transition-colors flex items-center gap-1 group"
|
||||||
|
>
|
||||||
|
View All
|
||||||
|
<ArrowRight size={14} className="group-hover:translate-x-1 transition-transform stroke-[2.5]" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Blog Cards Grid */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
{blogs.map((blog) => (
|
||||||
|
<motion.article
|
||||||
|
key={blog.id}
|
||||||
|
whileHover={{ y: -5 }}
|
||||||
|
transition={{ type: 'spring', stiffness: 300, damping: 25 }}
|
||||||
|
className="bg-white rounded-theme border border-gray-150 overflow-hidden shadow-sm hover:shadow-soft-hover transition-all duration-300 flex flex-col justify-between"
|
||||||
|
>
|
||||||
|
{/* Image Frame */}
|
||||||
|
<div className="w-full aspect-[16/10] overflow-hidden bg-gray-50 relative select-none">
|
||||||
|
<img
|
||||||
|
src={blog.image}
|
||||||
|
alt={blog.title}
|
||||||
|
className="w-full h-full object-cover transition-transform duration-500 hover:scale-105"
|
||||||
|
/>
|
||||||
|
<span className="absolute top-3 left-3 bg-white/90 backdrop-blur-xs text-[10px] font-extrabold px-2.5 py-1 rounded-md text-gray-700 shadow-sm">
|
||||||
|
{blog.readTime}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Text Body */}
|
||||||
|
<div className="p-6 text-left flex-1 flex flex-col justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2 text-[11px] text-gray-400 font-semibold mb-2">
|
||||||
|
<span>{blog.author}</span>
|
||||||
|
<span className="w-1 h-1 bg-gray-300 rounded-full" />
|
||||||
|
<span>{blog.date}</span>
|
||||||
|
</div>
|
||||||
|
<h4 className="text-[13.5px] font-bold text-gray-900 hover:text-primary transition-colors leading-snug mb-2 cursor-pointer">
|
||||||
|
{blog.title}
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs text-gray-500 font-medium leading-relaxed line-clamp-2 mb-4">
|
||||||
|
{blog.excerpt}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="inline-flex items-center gap-1 text-xs font-bold text-primary hover:text-primary-dark transition-colors group/link mt-auto pt-2"
|
||||||
|
>
|
||||||
|
Read More
|
||||||
|
<ArrowRight size={13} className="group-hover/link:translate-x-0.5 transition-transform" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</motion.article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
56
NewFile/src/src/sections/BrandsSection.tsx
Normal file
56
NewFile/src/src/sections/BrandsSection.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
interface Brand {
|
||||||
|
name: string;
|
||||||
|
hoverColor: string;
|
||||||
|
fontStyle: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BrandsSection: React.FC = () => {
|
||||||
|
const brandList: Brand[] = [
|
||||||
|
{ name: 'amazon', hoverColor: 'text-[#FF9900]', fontStyle: 'font-extrabold italic tracking-tighter' },
|
||||||
|
{ name: 'AMD', hoverColor: 'text-[#ED1C24]', fontStyle: 'font-sans font-black tracking-normal' },
|
||||||
|
{ name: 'facebook', hoverColor: 'text-[#1877F2]', fontStyle: 'font-serif font-bold lowercase tracking-tight' },
|
||||||
|
{ name: 'logitech', hoverColor: 'text-[#00B0FF]', fontStyle: 'font-sans font-bold lowercase' },
|
||||||
|
{ name: 'apper', hoverColor: 'text-[#8E44AD]', fontStyle: 'font-mono font-bold tracking-widest' },
|
||||||
|
{ name: 'hooli', hoverColor: 'text-[#2ECC71]', fontStyle: 'font-sans font-extrabold tracking-tight lowercase' },
|
||||||
|
{ name: 'FedEx', hoverColor: 'text-[#4F2D7F]', fontStyle: 'font-sans font-extrabold tracking-tighter' },
|
||||||
|
{ name: 'PayPal', hoverColor: 'text-[#003087]', fontStyle: 'font-serif font-black italic tracking-tight' },
|
||||||
|
{ name: 'NETFLIX', hoverColor: 'text-[#E50914]', fontStyle: 'font-sans font-bold tracking-widest uppercase' },
|
||||||
|
{ name: 'Spotify', hoverColor: 'text-[#1DB954]', fontStyle: 'font-sans font-extrabold tracking-tight' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-8 bg-bgLight font-sans">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
|
||||||
|
{/* Section Header */}
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h2 className="text-xl md:text-2xl font-extrabold text-gray-900 tracking-tight font-outfit">
|
||||||
|
Popular Brands
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Brand Grid */}
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
|
||||||
|
{brandList.map((brand) => (
|
||||||
|
<motion.div
|
||||||
|
key={brand.name}
|
||||||
|
whileHover={{ y: -4, scale: 1.02 }}
|
||||||
|
transition={{ type: 'spring', stiffness: 350, damping: 25 }}
|
||||||
|
className="bg-white border border-gray-150 rounded-theme h-16 flex items-center justify-center p-4 shadow-sm hover:shadow-soft-hover transition-all duration-300 group cursor-pointer"
|
||||||
|
>
|
||||||
|
<span className={`text-[17px] font-bold text-gray-300 group-hover:${brand.hoverColor} transition-colors select-none ${brand.fontStyle}`}>
|
||||||
|
{brand.name}
|
||||||
|
</span>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
59
NewFile/src/src/sections/CategoriesSection.tsx
Normal file
59
NewFile/src/src/sections/CategoriesSection.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { categories } from '../utils/mockData';
|
||||||
|
import { ArrowRight } from 'lucide-react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
export const CategoriesSection: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<section className="py-8 bg-white font-sans">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
{/* Section Header */}
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h2 className="text-xl md:text-2xl font-extrabold text-gray-900 tracking-tight font-outfit">
|
||||||
|
Shop By Categories
|
||||||
|
</h2>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="text-xs font-bold text-gray-900 hover:text-primary transition-colors flex items-center gap-1 group"
|
||||||
|
>
|
||||||
|
View All
|
||||||
|
<ArrowRight size={14} className="group-hover:translate-x-1 transition-transform stroke-[2.5]" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Categories List */}
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-6">
|
||||||
|
{categories.map((cat) => (
|
||||||
|
<motion.a
|
||||||
|
key={cat.id}
|
||||||
|
href="#"
|
||||||
|
whileHover={{ y: -6 }}
|
||||||
|
transition={{ type: 'spring', stiffness: 300, damping: 25 }}
|
||||||
|
className="flex flex-col items-center group text-center"
|
||||||
|
>
|
||||||
|
{/* Image Box */}
|
||||||
|
<div className="w-full aspect-square bg-[#F8F9FB] rounded-theme flex items-center justify-center p-6 border border-transparent group-hover:border-blue-100 group-hover:bg-blue-50/10 transition-all duration-300 relative overflow-hidden">
|
||||||
|
<motion.img
|
||||||
|
whileHover={{ scale: 1.06 }}
|
||||||
|
src={cat.image}
|
||||||
|
alt={cat.name}
|
||||||
|
className="max-h-full max-w-full object-contain rounded-lg drop-shadow-md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Title & Count */}
|
||||||
|
<h3 className="text-[13px] font-bold text-gray-800 mt-3.5 group-hover:text-primary transition-colors">
|
||||||
|
{cat.name}
|
||||||
|
</h3>
|
||||||
|
<span className="text-[10px] font-bold text-gray-400 mt-0.5 tracking-wide">
|
||||||
|
{cat.productCount} Products
|
||||||
|
</span>
|
||||||
|
</motion.a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
52
NewFile/src/src/sections/FeaturesSection.tsx
Normal file
52
NewFile/src/src/sections/FeaturesSection.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Truck, RotateCcw, Headphones, ShieldCheck } from 'lucide-react';
|
||||||
|
|
||||||
|
export const FeaturesSection: React.FC = () => {
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
icon: <Truck size={24} className="text-primary" />,
|
||||||
|
title: 'Free Shipping',
|
||||||
|
desc: 'Free shipping on all orders over $100',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <RotateCcw size={24} className="text-primary" />,
|
||||||
|
title: 'Easy Returns',
|
||||||
|
desc: '30 days money back guarantee policy',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Headphones size={24} className="text-primary" />,
|
||||||
|
title: '24/7 Support',
|
||||||
|
desc: 'Friendly 24/7 support line whenever needed',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <ShieldCheck size={24} className="text-primary" />,
|
||||||
|
title: 'Secure Payment',
|
||||||
|
desc: '100% secure payment gateways and protocols',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-12 bg-white font-sans border-t border-gray-100">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||||
|
{items.map((item, idx) => (
|
||||||
|
<div
|
||||||
|
key={item.title}
|
||||||
|
className={`flex items-center gap-4 text-left ${
|
||||||
|
idx < items.length - 1 ? 'lg:border-r lg:border-gray-100 pr-6' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="p-3 bg-blue-50/50 rounded-xl flex-shrink-0">
|
||||||
|
{item.icon}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-bold text-gray-800 mb-0.5">{item.title}</h4>
|
||||||
|
<p className="text-[11px] text-gray-400 font-semibold leading-relaxed">{item.desc}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
94
NewFile/src/src/sections/HeroSection.tsx
Normal file
94
NewFile/src/src/sections/HeroSection.tsx
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||||
|
import { Autoplay, Pagination } from 'swiper/modules';
|
||||||
|
import { CategorySidebar } from '../components/CategorySidebar';
|
||||||
|
import { ArrowRight } from 'lucide-react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
// Import Swiper styles
|
||||||
|
import 'swiper/css';
|
||||||
|
import 'swiper/css/pagination';
|
||||||
|
|
||||||
|
export const HeroSection: React.FC = () => {
|
||||||
|
const slides = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
badge: 'Limited Time Offer',
|
||||||
|
title: 'Ultimate Virtual Reality\nExperience Box',
|
||||||
|
subtitle: 'Experience the Future of Entertainment Today',
|
||||||
|
image: 'https://images.unsplash.com/photo-1593508512255-86ab42a8e620?w=600&auto=format&fit=crop&q=80',
|
||||||
|
gradient: 'from-blue-600 via-indigo-600 to-purple-650',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
badge: 'Exclusive Launch',
|
||||||
|
title: 'Next-Gen Performance\nPro Gaming Gear',
|
||||||
|
subtitle: 'Unleash Your Full Creative Power Everywhere',
|
||||||
|
image: 'https://images.unsplash.com/photo-1606144042614-b2417e99c4e3?w=600&auto=format&fit=crop&q=80',
|
||||||
|
gradient: 'from-slate-900 via-blue-900 to-indigo-950',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="pt-0 pb-6 bg-white font-sans">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="flex gap-6 items-start">
|
||||||
|
{/* Left Vertical Categories Sidebar */}
|
||||||
|
<CategorySidebar />
|
||||||
|
|
||||||
|
{/* Right Swiper Hero Slider */}
|
||||||
|
<div className="flex-1 min-w-0 rounded-theme overflow-hidden shadow-sm h-[455px]">
|
||||||
|
<Swiper
|
||||||
|
modules={[Autoplay, Pagination]}
|
||||||
|
pagination={{ clickable: true }}
|
||||||
|
autoplay={{ delay: 6000, disableOnInteraction: false }}
|
||||||
|
loop={true}
|
||||||
|
className="h-full"
|
||||||
|
>
|
||||||
|
{slides.map((slide) => (
|
||||||
|
<SwiperSlide key={slide.id}>
|
||||||
|
<div className={`h-full w-full bg-gradient-to-r ${slide.gradient} p-8 md:p-12 lg:p-16 flex flex-col md:flex-row items-center justify-between gap-8 text-white relative`}>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="text-left flex-1 z-10">
|
||||||
|
<span className="inline-block bg-red-650 text-[11px] font-extrabold uppercase tracking-wider px-3.5 py-1 rounded-full mb-4">
|
||||||
|
{slide.badge}
|
||||||
|
</span>
|
||||||
|
<h2 className="text-3xl md:text-4xl lg:text-5xl font-extrabold tracking-tight leading-[1.1] whitespace-pre-line mb-4 font-outfit">
|
||||||
|
{slide.title}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm md:text-base text-blue-100 font-medium mb-8">
|
||||||
|
{slide.subtitle}
|
||||||
|
</p>
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
className="bg-primary text-white hover:bg-primary-hover text-xs md:text-sm font-semibold px-8 py-3.5 rounded-lg flex items-center shadow-md transition-colors"
|
||||||
|
>
|
||||||
|
Shop Now
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Image */}
|
||||||
|
<div className="flex-1 w-full md:w-auto h-[220px] md:h-full relative flex items-center justify-center">
|
||||||
|
<motion.img
|
||||||
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
src={slide.image}
|
||||||
|
alt={slide.title}
|
||||||
|
className="max-h-full max-w-full object-contain rounded-xl drop-shadow-2xl mix-blend-screen"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SwiperSlide>
|
||||||
|
))}
|
||||||
|
</Swiper>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
221
NewFile/src/src/sections/HotDealsSection.tsx
Normal file
221
NewFile/src/src/sections/HotDealsSection.tsx
Normal file
|
|
@ -0,0 +1,221 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { products } from '../utils/mockData';
|
||||||
|
import { CountdownTimer } from '../components/CountdownTimer';
|
||||||
|
import { RatingStars } from '../components/RatingStars';
|
||||||
|
import { ChevronLeft, ChevronRight, ShoppingCart, Heart, Eye } from 'lucide-react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
export const HotDealsSection: React.FC = () => {
|
||||||
|
// Hot Deal product (Jajot J2)
|
||||||
|
const hotDealProduct = products.find((p) => p.isHot) || products[0];
|
||||||
|
|
||||||
|
// Monthly Featured items (6 items displayed in mini horizontal cards)
|
||||||
|
const featuredProducts = products.filter((p) => p.isFeatured).slice(0, 6);
|
||||||
|
|
||||||
|
const soldCount = hotDealProduct.soldCount || 85;
|
||||||
|
const totalStock = hotDealProduct.totalStock || 120;
|
||||||
|
const percentSold = Math.round((soldCount / totalStock) * 100);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-8 bg-bgLight font-sans">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||||
|
|
||||||
|
{/* Left Column: Hot Deals */}
|
||||||
|
<div className="lg:col-span-1">
|
||||||
|
<div className="bg-white rounded-none border-2 border-primary/20 overflow-hidden shadow-sm flex flex-col justify-between p-6 h-full relative">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex justify-between items-center border-b border-gray-100 pb-4 mb-6">
|
||||||
|
<h3 className="text-lg font-extrabold text-gray-900 tracking-tight font-outfit">
|
||||||
|
Hot Deals
|
||||||
|
</h3>
|
||||||
|
<div className="flex gap-1.5">
|
||||||
|
<button className="w-7 h-7 rounded-full border border-gray-200 text-gray-400 hover:text-primary hover:border-primary flex items-center justify-center transition-colors">
|
||||||
|
<ChevronLeft size={14} className="stroke-[2.5]" />
|
||||||
|
</button>
|
||||||
|
<button className="w-7 h-7 rounded-full border border-gray-200 text-gray-400 hover:text-primary hover:border-primary flex items-center justify-center transition-colors">
|
||||||
|
<ChevronRight size={14} className="stroke-[2.5]" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Badge */}
|
||||||
|
<div className="absolute top-20 right-6">
|
||||||
|
<span className="bg-primary text-white text-[10px] font-extrabold px-2.5 py-1 rounded-md">
|
||||||
|
-{hotDealProduct.discount}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Product Image */}
|
||||||
|
<div className="w-full flex-1 flex items-center justify-center min-h-[200px] mb-6">
|
||||||
|
<img
|
||||||
|
src={hotDealProduct.image}
|
||||||
|
alt={hotDealProduct.name}
|
||||||
|
className="max-h-[180px] max-w-full object-contain rounded-lg drop-shadow-md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Product Info */}
|
||||||
|
<div className="text-left mt-2">
|
||||||
|
{/* Rating */}
|
||||||
|
<RatingStars rating={hotDealProduct.rating} size={13} className="mb-2" />
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<h4 className="text-sm font-extrabold text-gray-800 leading-snug hover:text-primary cursor-pointer transition-colors mb-2">
|
||||||
|
{hotDealProduct.name}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
{/* Pricing */}
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<span className="text-lg font-extrabold text-gray-900">${hotDealProduct.price}</span>
|
||||||
|
{hotDealProduct.oldPrice && (
|
||||||
|
<span className="text-xs text-gray-400 line-through font-semibold">${hotDealProduct.oldPrice}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress bar */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="flex justify-between text-[11px] text-gray-500 font-bold mb-1.5">
|
||||||
|
<span>Sold: {soldCount} Items</span>
|
||||||
|
<span>Available: {totalStock - soldCount} Items</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full h-2 bg-gray-100 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full bg-primary rounded-full transition-all duration-500"
|
||||||
|
style={{ width: `${percentSold}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Countdown Timer */}
|
||||||
|
<div className="flex flex-col items-center p-4 bg-red-50/50 border border-red-100/55 rounded-2xl mb-2">
|
||||||
|
<span className="text-[10px] text-red-500 font-extrabold uppercase tracking-wider mb-2">
|
||||||
|
Offer Expires In:
|
||||||
|
</span>
|
||||||
|
{hotDealProduct.timeRemaining && (
|
||||||
|
<CountdownTimer
|
||||||
|
initialDays={hotDealProduct.timeRemaining.days}
|
||||||
|
initialHours={hotDealProduct.timeRemaining.hours}
|
||||||
|
initialMinutes={hotDealProduct.timeRemaining.minutes}
|
||||||
|
initialSeconds={hotDealProduct.timeRemaining.seconds}
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Column: Monthly Featured Items */}
|
||||||
|
<div className="lg:col-span-2">
|
||||||
|
<div className="bg-white rounded-none border border-gray-150 p-6 h-full flex flex-col justify-between">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex justify-between items-center border-b border-gray-100 pb-4 mb-6">
|
||||||
|
<h3 className="text-lg font-extrabold text-gray-900 tracking-tight font-outfit">
|
||||||
|
Monthly Featured Item
|
||||||
|
</h3>
|
||||||
|
<div className="flex gap-1.5">
|
||||||
|
<button className="w-7 h-7 rounded-full border border-gray-200 text-gray-400 hover:text-primary hover:border-primary flex items-center justify-center transition-colors">
|
||||||
|
<ChevronLeft size={14} className="stroke-[2.5]" />
|
||||||
|
</button>
|
||||||
|
<button className="w-7 h-7 rounded-full border border-gray-200 text-gray-400 hover:text-primary hover:border-primary flex items-center justify-center transition-colors">
|
||||||
|
<ChevronRight size={14} className="stroke-[2.5]" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid of Mini Horizontal Cards */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5 flex-1 mt-4">
|
||||||
|
{featuredProducts.map((product) => (
|
||||||
|
<div
|
||||||
|
key={product.id}
|
||||||
|
className="flex gap-3 p-4 bg-white border border-gray-150 rounded-xl hover:border-[#0073EC]/30 hover:shadow-xs transition-all duration-300 group text-left relative"
|
||||||
|
>
|
||||||
|
{/* Thumbnail */}
|
||||||
|
<div className="w-20 h-20 bg-[#F8F9FB] rounded-lg p-2 flex-shrink-0 flex items-center justify-center relative overflow-hidden">
|
||||||
|
<img
|
||||||
|
src={product.image}
|
||||||
|
alt={product.name}
|
||||||
|
className="max-h-full max-w-full object-contain rounded drop-shadow group-hover:scale-105 transition-transform duration-300"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Meta info */}
|
||||||
|
<div className="flex-1 min-w-0 flex flex-col justify-start">
|
||||||
|
<div>
|
||||||
|
<h5 className="text-[12px] font-bold text-gray-800 line-clamp-1 leading-tight mb-0.5" title={product.name}>
|
||||||
|
{product.name}
|
||||||
|
</h5>
|
||||||
|
<RatingStars rating={product.rating} size={11} className="mb-0.5" />
|
||||||
|
|
||||||
|
{/* Price */}
|
||||||
|
<div className="flex items-center gap-1.5 my-1">
|
||||||
|
<span className="text-[13px] font-extrabold text-red-500">
|
||||||
|
${product.price}
|
||||||
|
</span>
|
||||||
|
{product.oldPrice && (
|
||||||
|
<span className="text-[10px] text-gray-400 line-through font-medium">
|
||||||
|
${product.oldPrice}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Features List */}
|
||||||
|
<p className="text-[10px] text-gray-400 font-semibold line-clamp-1 mb-1.5">
|
||||||
|
{product.features ? product.features.slice(0, 2).join(' • ') : "Functional Crown • 1.78\" Amoled"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Triggers */}
|
||||||
|
<div className="flex items-center justify-end gap-2 mt-2 pt-2 border-t border-gray-50">
|
||||||
|
<button
|
||||||
|
onClick={() => alert(`Added ${product.name} to cart!`)}
|
||||||
|
className="text-gray-400 hover:text-[#0073EC] transition-colors p-1"
|
||||||
|
title="Add to Cart"
|
||||||
|
>
|
||||||
|
<ShoppingCart size={13} className="stroke-[2.5]" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
let saved = localStorage.getItem('wishlist');
|
||||||
|
let list: string[] = saved ? JSON.parse(saved) : [];
|
||||||
|
if (!list.includes(product.id)) {
|
||||||
|
list.push(product.id);
|
||||||
|
localStorage.setItem('wishlist', JSON.stringify(list));
|
||||||
|
}
|
||||||
|
window.dispatchEvent(new Event('wishlistUpdated'));
|
||||||
|
window.dispatchEvent(new Event('openWishlistModal'));
|
||||||
|
}}
|
||||||
|
className="text-gray-400 hover:text-[#0073EC] transition-colors p-1 cursor-pointer"
|
||||||
|
title="Add to Wishlist"
|
||||||
|
>
|
||||||
|
<Heart size={13} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
window.dispatchEvent(new CustomEvent('openQuickView', { detail: product }));
|
||||||
|
}}
|
||||||
|
className="text-gray-400 hover:text-[#0073EC] transition-colors p-1 cursor-pointer"
|
||||||
|
title="Quick View"
|
||||||
|
>
|
||||||
|
<Eye size={13} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
35
NewFile/src/src/sections/NewArrivalsSection.tsx
Normal file
35
NewFile/src/src/sections/NewArrivalsSection.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { products } from '../utils/mockData';
|
||||||
|
import { ProductCard } from '../components/ProductCard';
|
||||||
|
import { ArrowRight } from 'lucide-react';
|
||||||
|
|
||||||
|
export const NewArrivalsSection: React.FC = () => {
|
||||||
|
const newArrivals = products.filter((p) => p.isNewArrival);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-8 bg-bgLight font-sans">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
{/* Section Header */}
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h2 className="text-xl md:text-2xl font-extrabold text-gray-900 tracking-tight font-outfit">
|
||||||
|
New Arrivals Products
|
||||||
|
</h2>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="text-xs font-bold text-gray-900 hover:text-primary transition-colors flex items-center gap-1 group"
|
||||||
|
>
|
||||||
|
View All
|
||||||
|
<ArrowRight size={14} className="group-hover:translate-x-1 transition-transform stroke-[2.5]" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Product Grid */}
|
||||||
|
<div className="bg-white border-t border-l border-gray-200 rounded-none shadow-sm overflow-hidden grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
||||||
|
{newArrivals.map((product) => (
|
||||||
|
<ProductCard key={product.id} product={product} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
59
NewFile/src/src/sections/NewsletterSection.tsx
Normal file
59
NewFile/src/src/sections/NewsletterSection.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Send } from 'lucide-react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
export const NewsletterSection: React.FC = () => {
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
|
||||||
|
const handleSubscribe = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (email.trim()) {
|
||||||
|
alert(`Thank you for subscribing with: ${email}`);
|
||||||
|
setEmail('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-16 bg-primary font-sans text-white relative overflow-hidden">
|
||||||
|
{/* Decorative background shapes */}
|
||||||
|
<div className="absolute top-0 left-0 w-64 h-64 bg-blue-500 rounded-full mix-blend-multiply filter blur-3xl opacity-30 -translate-x-1/2 -translate-y-1/2 pointer-events-none" />
|
||||||
|
<div className="absolute bottom-0 right-0 w-80 h-80 bg-indigo-500 rounded-full mix-blend-multiply filter blur-3xl opacity-30 translate-x-1/3 translate-y-1/3 pointer-events-none" />
|
||||||
|
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
|
<div className="max-w-2xl mx-auto text-center flex flex-col items-center">
|
||||||
|
<span className="text-[10px] font-extrabold uppercase tracking-widest text-blue-100 bg-blue-800/40 px-3 py-1 rounded-full mb-4">
|
||||||
|
Newsletter Signup
|
||||||
|
</span>
|
||||||
|
<h2 className="text-2xl md:text-3xl font-extrabold tracking-tight mb-2 font-outfit">
|
||||||
|
Subscribe to our Newsletter
|
||||||
|
</h2>
|
||||||
|
<p className="text-xs md:text-sm text-blue-100 font-medium max-w-md mb-8 leading-relaxed">
|
||||||
|
Get the latest tech arrivals, exclusive brand discounts, and weekly hot deal alerts directly in your inbox.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubscribe} className="w-full max-w-md flex flex-col sm:flex-row gap-3">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
placeholder="Your email address..."
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
className="flex-grow px-4 py-3.5 bg-white/10 border border-white/20 hover:border-white/40 focus:border-white rounded-lg text-white placeholder-white/60 focus:outline-none text-xs font-semibold backdrop-blur-sm transition-all"
|
||||||
|
/>
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.03 }}
|
||||||
|
whileTap={{ scale: 0.97 }}
|
||||||
|
type="submit"
|
||||||
|
className="bg-white hover:bg-gray-50 text-primary hover:text-primary-dark font-extrabold text-xs px-8 py-3.5 rounded-lg flex items-center justify-center gap-2 shadow-lg transition-colors"
|
||||||
|
>
|
||||||
|
Subscribe
|
||||||
|
<Send size={13} className="stroke-[2.5]" />
|
||||||
|
</motion.button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
65
NewFile/src/src/sections/PromoBannersSection.tsx
Normal file
65
NewFile/src/src/sections/PromoBannersSection.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { ArrowRight } from 'lucide-react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
export const PromoBannersSection: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<section className="py-8 bg-bgLight font-sans">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
{/* Banner 1 */}
|
||||||
|
<motion.a
|
||||||
|
href="#"
|
||||||
|
whileHover={{ y: -4 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="rounded-theme bg-[#E5E9E6] p-8 md:p-10 flex items-center justify-between overflow-hidden shadow-sm relative min-h-[220px] cursor-pointer block"
|
||||||
|
>
|
||||||
|
<div className="text-left z-10 max-w-[55%]">
|
||||||
|
<h3 className="text-xl md:text-2xl font-extrabold text-gray-900 leading-tight mb-2 font-outfit">
|
||||||
|
Precision Sound with Amazing Quality
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-gray-500 font-semibold">
|
||||||
|
USB 3 Rechargeable
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/* Image */}
|
||||||
|
<div className="absolute right-4 bottom-0 w-[200px] h-[90%] flex items-end justify-center select-none">
|
||||||
|
<img
|
||||||
|
src="https://images.unsplash.com/photo-1598327105666-5b89351aff97?w=350&auto=format&fit=crop&q=60"
|
||||||
|
alt="Precision Sound"
|
||||||
|
className="max-h-full max-w-full object-contain rounded-t-lg drop-shadow-xl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</motion.a>
|
||||||
|
|
||||||
|
{/* Banner 2 */}
|
||||||
|
<motion.a
|
||||||
|
href="#"
|
||||||
|
whileHover={{ y: -4 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="rounded-theme bg-[#E5ECEF] p-8 md:p-10 flex items-center justify-between overflow-hidden shadow-sm relative min-h-[220px] cursor-pointer block"
|
||||||
|
>
|
||||||
|
<div className="text-left z-10 max-w-[55%]">
|
||||||
|
<h3 className="text-xl md:text-2xl font-extrabold text-gray-900 leading-tight mb-2 font-outfit">
|
||||||
|
Nonstop Sound with Amazing Quality
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-gray-500 font-semibold">
|
||||||
|
Portable Power in Style
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/* Image */}
|
||||||
|
<div className="absolute right-4 bottom-0 w-[200px] h-[95%] flex items-end justify-center select-none">
|
||||||
|
<img
|
||||||
|
src="https://images.unsplash.com/photo-1590658268037-6bf12165a8df?w=350&auto=format&fit=crop&q=60"
|
||||||
|
alt="Nonstop Sound"
|
||||||
|
className="max-h-full max-w-full object-contain rounded-t-lg drop-shadow-xl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</motion.a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
53
NewFile/src/src/sections/PromoCardsSection.tsx
Normal file
53
NewFile/src/src/sections/PromoCardsSection.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { promoCards } from '../utils/mockData';
|
||||||
|
import { ArrowRight } from 'lucide-react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
export const PromoCardsSection: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<section className="py-8 bg-bgLight font-sans">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
{promoCards.map((card) => (
|
||||||
|
<motion.a
|
||||||
|
key={card.id}
|
||||||
|
href="#"
|
||||||
|
whileHover={{ y: -6 }}
|
||||||
|
transition={{ type: 'spring', stiffness: 300, damping: 25 }}
|
||||||
|
className={`rounded-theme p-6 md:p-8 flex items-center justify-between ${card.bgColor} relative overflow-hidden shadow-sm hover:shadow-soft-hover transition-shadow duration-300 min-h-[190px] cursor-pointer block`}
|
||||||
|
>
|
||||||
|
{/* Text side */}
|
||||||
|
<div className="flex flex-col items-start text-left z-10 max-w-[60%]">
|
||||||
|
<span className={`text-[9px] font-extrabold uppercase px-2.5 py-0.5 rounded-full mb-3 tracking-wider ${card.badgeBg}`}>
|
||||||
|
{card.badge}
|
||||||
|
</span>
|
||||||
|
<h3 className="text-lg md:text-xl font-extrabold text-gray-900 leading-tight mb-1 font-outfit">
|
||||||
|
{card.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-[11px] text-gray-500 font-semibold mb-3">
|
||||||
|
{card.subtitle}
|
||||||
|
</p>
|
||||||
|
<div className="text-[13px] font-extrabold text-gray-800">
|
||||||
|
{card.priceText}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Image side */}
|
||||||
|
<div className="w-[110px] h-[110px] relative flex items-center justify-center flex-shrink-0">
|
||||||
|
<motion.img
|
||||||
|
whileHover={{ scale: 1.08 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
src={card.image}
|
||||||
|
alt={card.title}
|
||||||
|
className="max-h-full max-w-full object-contain drop-shadow-xl rounded-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</motion.a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
111
NewFile/src/src/sections/RecentlyLaunchedSection.tsx
Normal file
111
NewFile/src/src/sections/RecentlyLaunchedSection.tsx
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||||
|
import { Navigation } from 'swiper/modules';
|
||||||
|
import { products } from '../utils/mockData';
|
||||||
|
import { ProductCard } from '../components/ProductCard';
|
||||||
|
import { ArrowRight, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
// Import Swiper styles
|
||||||
|
import 'swiper/css';
|
||||||
|
import 'swiper/css/navigation';
|
||||||
|
|
||||||
|
export const RecentlyLaunchedSection: React.FC = () => {
|
||||||
|
const recentlyLaunched = products.filter((p) => p.isRecentlyLaunched);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-8 bg-white font-sans">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
|
||||||
|
{/* Section Header */}
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h2 className="text-xl md:text-2xl font-extrabold text-gray-900 tracking-tight font-outfit">
|
||||||
|
Recently Launched
|
||||||
|
</h2>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="text-xs font-bold text-gray-900 hover:text-primary transition-colors flex items-center gap-1 group"
|
||||||
|
>
|
||||||
|
View All
|
||||||
|
<ArrowRight size={14} className="group-hover:translate-x-1 transition-transform stroke-[2.5]" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Layout: Left Banner, Right Carousel */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-stretch">
|
||||||
|
|
||||||
|
{/* Left Large Feature Banner */}
|
||||||
|
<div className="lg:col-span-4 h-full">
|
||||||
|
<motion.a
|
||||||
|
href="#"
|
||||||
|
whileHover={{ y: -4 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="rounded-theme bg-[#EFEFEF] p-8 md:p-10 flex flex-col justify-between overflow-hidden shadow-sm relative h-full min-h-[380px] cursor-pointer block"
|
||||||
|
>
|
||||||
|
<div className="text-left z-10">
|
||||||
|
<h3 className="text-2xl font-extrabold text-gray-900 leading-tight mb-2 font-outfit">
|
||||||
|
iPhone Prodigy
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-gray-500 font-semibold">
|
||||||
|
Performance in Motion
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Image */}
|
||||||
|
<div className="absolute right-0 bottom-0 w-[80%] h-[60%] flex items-end justify-end select-none">
|
||||||
|
<img
|
||||||
|
src="https://images.unsplash.com/photo-1511707171634-5f897ff02aa9?w=450&auto=format&fit=crop&q=60"
|
||||||
|
alt="iPhone Prodigy"
|
||||||
|
className="max-h-full max-w-full object-contain rounded-tl-xl drop-shadow-2xl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</motion.a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Product Carousel */}
|
||||||
|
<div className="lg:col-span-8 relative">
|
||||||
|
{/* Custom Carousel Arrows */}
|
||||||
|
<div className="absolute -top-12 right-2 flex gap-1.5 z-20">
|
||||||
|
<button className="rl-prev w-7 h-7 rounded-full border border-gray-250 text-gray-450 hover:text-primary hover:border-primary flex items-center justify-center transition-colors bg-white">
|
||||||
|
<ChevronLeft size={14} className="stroke-[2.5]" />
|
||||||
|
</button>
|
||||||
|
<button className="rl-next w-7 h-7 rounded-full border border-gray-250 text-gray-450 hover:text-primary hover:border-primary flex items-center justify-center transition-colors bg-white">
|
||||||
|
<ChevronRight size={14} className="stroke-[2.5]" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white border border-gray-150 rounded-none shadow-sm overflow-hidden">
|
||||||
|
<Swiper
|
||||||
|
modules={[Navigation]}
|
||||||
|
navigation={{
|
||||||
|
prevEl: '.rl-prev',
|
||||||
|
nextEl: '.rl-next',
|
||||||
|
}}
|
||||||
|
spaceBetween={0}
|
||||||
|
slidesPerView={1}
|
||||||
|
breakpoints={{
|
||||||
|
480: { slidesPerView: 1 },
|
||||||
|
640: { slidesPerView: 2 },
|
||||||
|
1024: { slidesPerView: 3 },
|
||||||
|
}}
|
||||||
|
className="h-full"
|
||||||
|
>
|
||||||
|
{recentlyLaunched.map((product) => (
|
||||||
|
<SwiperSlide key={product.id} className="h-full border-r border-gray-100 last:border-r-0">
|
||||||
|
<ProductCard product={product} />
|
||||||
|
</SwiperSlide>
|
||||||
|
))}
|
||||||
|
</Swiper>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
76
NewFile/src/src/sections/UltimateTechSection.tsx
Normal file
76
NewFile/src/src/sections/UltimateTechSection.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { products } from '../utils/mockData';
|
||||||
|
import { ProductCard } from '../components/ProductCard';
|
||||||
|
import { ArrowRight } from 'lucide-react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
export const UltimateTechSection: React.FC = () => {
|
||||||
|
// Query 4 tech gadget products
|
||||||
|
const techGadgets = products.filter((p) => p.isTechGadget).slice(0, 4);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-8 bg-white font-sans">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
|
||||||
|
{/* Section Header */}
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h2 className="text-xl md:text-2xl font-extrabold text-gray-900 tracking-tight font-outfit">
|
||||||
|
Ultimate Tech Gadgets
|
||||||
|
</h2>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="text-xs font-bold text-gray-900 hover:text-primary transition-colors flex items-center gap-1 group"
|
||||||
|
>
|
||||||
|
View All
|
||||||
|
<ArrowRight size={14} className="group-hover:translate-x-1 transition-transform stroke-[2.5]" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Layout: Left Grid, Right Banner */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-stretch">
|
||||||
|
|
||||||
|
{/* Left Product Grid */}
|
||||||
|
<div className="lg:col-span-9">
|
||||||
|
<div className="bg-white border-t border-l border-gray-200 rounded-none shadow-sm overflow-hidden grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4">
|
||||||
|
{techGadgets.map((product) => (
|
||||||
|
<ProductCard key={product.id} product={product} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Large Feature Banner */}
|
||||||
|
<div className="lg:col-span-3 h-full">
|
||||||
|
<motion.a
|
||||||
|
href="#"
|
||||||
|
whileHover={{ y: -4 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="rounded-theme bg-[#FBF2EE] p-8 flex flex-col justify-between overflow-hidden shadow-sm relative h-full min-h-[380px] cursor-pointer block"
|
||||||
|
>
|
||||||
|
<div className="text-left z-10">
|
||||||
|
<h3 className="text-2xl font-extrabold text-gray-900 leading-tight mb-2 font-outfit">
|
||||||
|
Active Laptop
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-gray-500 font-semibold">
|
||||||
|
Smooth in Performance
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Image */}
|
||||||
|
<div className="absolute right-0 bottom-0 w-[95%] h-[60%] flex items-end justify-end select-none">
|
||||||
|
<img
|
||||||
|
src="https://images.unsplash.com/photo-1544244015-0df4b3ffc6b0?w=350&auto=format&fit=crop&q=60"
|
||||||
|
alt="Active Laptop"
|
||||||
|
className="max-h-full max-w-full object-contain rounded-tl-xl drop-shadow-2xl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</motion.a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
61
NewFile/src/src/styles/index.css
Normal file
61
NewFile/src/src/styles/index.css
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply font-sans bg-bgLight text-textMain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Swiper Styles */
|
||||||
|
.swiper-pagination-bullet-active {
|
||||||
|
background: #1976F3 !important;
|
||||||
|
width: 24px !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swiper-button-next,
|
||||||
|
.swiper-button-prev {
|
||||||
|
color: #1976F3 !important;
|
||||||
|
background: white;
|
||||||
|
width: 40px !important;
|
||||||
|
height: 40px !important;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.swiper-button-next::after,
|
||||||
|
.swiper-button-prev::after {
|
||||||
|
font-size: 16px !important;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar behaviors */
|
||||||
|
.no-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.no-scrollbar {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Direct font overrides */
|
||||||
|
body, p, span, a, input, select, button {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6, .font-outfit {
|
||||||
|
font-family: 'Outfit', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Force container max-width to 1400px on large displays */
|
||||||
|
@media (min-width: 1400px) {
|
||||||
|
.container {
|
||||||
|
max-w-screen-2xl: 1400px;
|
||||||
|
max-width: 1400px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
61
NewFile/src/src/types/index.ts
Normal file
61
NewFile/src/src/types/index.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
export interface Product {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
price: number;
|
||||||
|
oldPrice?: number;
|
||||||
|
rating: number;
|
||||||
|
reviewsCount?: number;
|
||||||
|
image: string;
|
||||||
|
imageHover?: string;
|
||||||
|
discount?: number;
|
||||||
|
category: string;
|
||||||
|
hasTimer?: boolean;
|
||||||
|
timeRemaining?: {
|
||||||
|
days: number;
|
||||||
|
hours: number;
|
||||||
|
minutes: number;
|
||||||
|
seconds: number;
|
||||||
|
};
|
||||||
|
features?: string[];
|
||||||
|
description?: string;
|
||||||
|
isHot?: boolean;
|
||||||
|
isFeatured?: boolean;
|
||||||
|
isNewArrival?: boolean;
|
||||||
|
isRecentlyLaunched?: boolean;
|
||||||
|
isTechGadget?: boolean;
|
||||||
|
soldCount?: number;
|
||||||
|
totalStock?: number;
|
||||||
|
color?: string;
|
||||||
|
brand?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Category {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
iconName: string;
|
||||||
|
productCount: number;
|
||||||
|
image: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlogArticle {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
excerpt: string;
|
||||||
|
date: string;
|
||||||
|
author: string;
|
||||||
|
image: string;
|
||||||
|
readTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PromoCardData {
|
||||||
|
id: string;
|
||||||
|
badge: string;
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
priceText: string;
|
||||||
|
image: string;
|
||||||
|
bgColor: string;
|
||||||
|
textColor: string;
|
||||||
|
badgeBg: string;
|
||||||
|
}
|
||||||
384
NewFile/src/src/utils/mockData.ts
Normal file
384
NewFile/src/src/utils/mockData.ts
Normal file
|
|
@ -0,0 +1,384 @@
|
||||||
|
import { Product, Category, BlogArticle, PromoCardData } from '../types';
|
||||||
|
|
||||||
|
export const categories: Category[] = [
|
||||||
|
{
|
||||||
|
id: 'cat-1',
|
||||||
|
name: 'TV & Speaker',
|
||||||
|
slug: 'tv-speaker',
|
||||||
|
iconName: 'Speaker',
|
||||||
|
productCount: 7,
|
||||||
|
image: 'https://images.unsplash.com/photo-1545454675-3531b543be5d?w=200&auto=format&fit=crop&q=60',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cat-2',
|
||||||
|
name: 'Party Speakers',
|
||||||
|
slug: 'party-speakers',
|
||||||
|
iconName: 'Volume2',
|
||||||
|
productCount: 3,
|
||||||
|
image: 'https://images.unsplash.com/photo-1508700115892-45ecd05ae2ad?w=200&auto=format&fit=crop&q=60',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cat-3',
|
||||||
|
name: 'Cameras',
|
||||||
|
slug: 'cameras',
|
||||||
|
iconName: 'Camera',
|
||||||
|
productCount: 5,
|
||||||
|
image: 'https://images.unsplash.com/photo-1516035069371-29a1b244cc32?w=200&auto=format&fit=crop&q=60',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cat-4',
|
||||||
|
name: 'Phones',
|
||||||
|
slug: 'phones',
|
||||||
|
iconName: 'Smartphone',
|
||||||
|
productCount: 6,
|
||||||
|
image: 'https://images.unsplash.com/photo-1511707171634-5f897ff02aa9?w=200&auto=format&fit=crop&q=60',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cat-5',
|
||||||
|
name: 'Chargers & Cables',
|
||||||
|
slug: 'chargers-cables',
|
||||||
|
iconName: 'Cable',
|
||||||
|
productCount: 6,
|
||||||
|
image: 'https://images.unsplash.com/photo-1628157582853-a796fa650a6a?w=200&auto=format&fit=crop&q=60',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cat-6',
|
||||||
|
name: 'Smart Watches',
|
||||||
|
slug: 'smart-watches',
|
||||||
|
iconName: 'Watch',
|
||||||
|
productCount: 4,
|
||||||
|
image: 'https://images.unsplash.com/photo-1508685096489-7aacd43bd3b1?w=200&auto=format&fit=crop&q=60',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const products: Product[] = [
|
||||||
|
{
|
||||||
|
id: 'prod-1',
|
||||||
|
name: 'Apple Airpods Pro (2nd Gen) with MagSafe Charging Case',
|
||||||
|
price: 299,
|
||||||
|
oldPrice: 1900,
|
||||||
|
rating: 5.0,
|
||||||
|
reviewsCount: 24,
|
||||||
|
image: 'https://images.unsplash.com/photo-1588449668365-d15e397f6787?w=300&auto=format&fit=crop&q=60',
|
||||||
|
imageHover: 'https://images.unsplash.com/photo-1600294037681-c80b4cb5b434?w=300&auto=format&fit=crop&q=60',
|
||||||
|
discount: 5,
|
||||||
|
category: 'Smart Devices',
|
||||||
|
brand: 'Creative',
|
||||||
|
color: 'white',
|
||||||
|
isNewArrival: true,
|
||||||
|
isFeatured: true,
|
||||||
|
isHot: true,
|
||||||
|
isTechGadget: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'prod-2',
|
||||||
|
name: 'Apple HomePod Assistant and Voice Recognition',
|
||||||
|
price: 397,
|
||||||
|
rating: 4.0,
|
||||||
|
reviewsCount: 15,
|
||||||
|
image: 'https://images.unsplash.com/photo-1543512214-318c7553f230?w=300&auto=format&fit=crop&q=60',
|
||||||
|
imageHover: 'https://images.unsplash.com/photo-1546435770-a3e426bf472b?w=300&auto=format&fit=crop&q=60',
|
||||||
|
category: 'TV & Speaker',
|
||||||
|
brand: 'Design',
|
||||||
|
color: 'grey',
|
||||||
|
isNewArrival: true,
|
||||||
|
isFeatured: true,
|
||||||
|
isTechGadget: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'prod-3',
|
||||||
|
name: 'Apple Watch Cellular 40 mm Starlight Aluminium Case',
|
||||||
|
price: 669,
|
||||||
|
oldPrice: 700,
|
||||||
|
rating: 5.0,
|
||||||
|
reviewsCount: 32,
|
||||||
|
image: 'https://images.unsplash.com/photo-1508685096489-7aacd43bd3b1?w=300&auto=format&fit=crop&q=60',
|
||||||
|
imageHover: 'https://images.unsplash.com/photo-1579586337278-3befd40fd17a?w=300&auto=format&fit=crop&q=60',
|
||||||
|
discount: 5,
|
||||||
|
category: 'Smart Watches',
|
||||||
|
brand: 'Gallery',
|
||||||
|
color: 'pink',
|
||||||
|
isNewArrival: true,
|
||||||
|
isFeatured: true,
|
||||||
|
isHot: true,
|
||||||
|
isRecentlyLaunched: true,
|
||||||
|
isTechGadget: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'prod-4',
|
||||||
|
name: 'Bose Portable Speaker, Upto 12 hrs of playtime',
|
||||||
|
price: 139,
|
||||||
|
oldPrice: 190,
|
||||||
|
rating: 4.8,
|
||||||
|
reviewsCount: 45,
|
||||||
|
image: 'https://images.unsplash.com/photo-1608043152269-423dbba4e7e1?w=300&auto=format&fit=crop&q=60',
|
||||||
|
imageHover: 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=300&auto=format&fit=crop&q=60',
|
||||||
|
discount: 5,
|
||||||
|
category: 'TV & Speaker',
|
||||||
|
brand: 'Gold',
|
||||||
|
color: 'grey',
|
||||||
|
isFeatured: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'prod-5',
|
||||||
|
name: 'Ceptics India to Europe & More Travel Adapter Plug',
|
||||||
|
price: 300,
|
||||||
|
oldPrice: 1500,
|
||||||
|
rating: 5.0,
|
||||||
|
reviewsCount: 12,
|
||||||
|
image: 'https://images.unsplash.com/photo-1628157582853-a796fa650a6a?w=300&auto=format&fit=crop&q=60',
|
||||||
|
imageHover: 'https://images.unsplash.com/photo-1583863788434-e58a36330cf0?w=300&auto=format&fit=crop&q=60',
|
||||||
|
discount: 25,
|
||||||
|
category: 'Chargers & Cables',
|
||||||
|
brand: 'Highlight',
|
||||||
|
color: 'white',
|
||||||
|
isNewArrival: true,
|
||||||
|
isRecentlyLaunched: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'prod-6',
|
||||||
|
name: "D'Wild USB Type C 100W Pd Power Delivery 20V Cable",
|
||||||
|
price: 9,
|
||||||
|
rating: 4.0,
|
||||||
|
reviewsCount: 19,
|
||||||
|
image: 'https://images.unsplash.com/photo-1589492477829-5e65395b66cc?w=300&auto=format&fit=crop&q=60',
|
||||||
|
imageHover: 'https://images.unsplash.com/photo-1611186871348-b1ce696e52c9?w=300&auto=format&fit=crop&q=60',
|
||||||
|
category: 'Chargers & Cables',
|
||||||
|
brand: 'Modern',
|
||||||
|
color: 'orange',
|
||||||
|
isNewArrival: true,
|
||||||
|
isRecentlyLaunched: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'prod-7',
|
||||||
|
name: 'Google Home Smart Voice Activated Speaker',
|
||||||
|
price: 300,
|
||||||
|
rating: 5.0,
|
||||||
|
reviewsCount: 41,
|
||||||
|
image: 'https://images.unsplash.com/photo-1543512214-318c7553f230?w=300&auto=format&fit=crop&q=60',
|
||||||
|
imageHover: 'https://images.unsplash.com/photo-1546435770-a3e426bf472b?w=300&auto=format&fit=crop&q=60',
|
||||||
|
category: 'TV & Speaker',
|
||||||
|
brand: 'Nature',
|
||||||
|
color: 'white',
|
||||||
|
isFeatured: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'prod-8',
|
||||||
|
name: 'HP (23.8 Inch) with IPS Panel Technology',
|
||||||
|
price: 1899,
|
||||||
|
oldPrice: 1989,
|
||||||
|
rating: 5.0,
|
||||||
|
reviewsCount: 72,
|
||||||
|
image: 'https://images.unsplash.com/photo-1527443224154-c4a3942d3acf?w=300&auto=format&fit=crop&q=60',
|
||||||
|
imageHover: 'https://images.unsplash.com/photo-1585776245991-cf89dd7fc73a?w=300&auto=format&fit=crop&q=60',
|
||||||
|
discount: 5,
|
||||||
|
category: 'Laptop & Computers',
|
||||||
|
brand: 'Sparker',
|
||||||
|
color: 'grey',
|
||||||
|
isFeatured: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'prod-9',
|
||||||
|
name: 'HP LaserJet P1108 Mono Single Function Laser Printer',
|
||||||
|
price: 85,
|
||||||
|
oldPrice: 89,
|
||||||
|
rating: 5.0,
|
||||||
|
reviewsCount: 8,
|
||||||
|
image: 'https://images.unsplash.com/photo-1612815154858-60aa4c59eaa6?w=300&auto=format&fit=crop&q=60',
|
||||||
|
imageHover: 'https://images.unsplash.com/photo-1563206767-5b18f218e8de?w=300&auto=format&fit=crop&q=60',
|
||||||
|
discount: 4,
|
||||||
|
category: 'Laptop & Computers',
|
||||||
|
brand: 'Creative',
|
||||||
|
color: 'white',
|
||||||
|
hasTimer: true,
|
||||||
|
timeRemaining: { days: 41, hours: 4, minutes: 50, seconds: 18 },
|
||||||
|
isFeatured: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'prod-10',
|
||||||
|
name: 'J.P.Gold Wireless Stereo Earphones Headphonea',
|
||||||
|
price: 45,
|
||||||
|
oldPrice: 49,
|
||||||
|
rating: 4.8,
|
||||||
|
reviewsCount: 16,
|
||||||
|
image: 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=300&auto=format&fit=crop&q=60',
|
||||||
|
imageHover: 'https://images.unsplash.com/photo-1546868871-7041f2a55e12?w=300&auto=format&fit=crop&q=60',
|
||||||
|
discount: 8,
|
||||||
|
category: 'Smart Devices',
|
||||||
|
brand: 'Design',
|
||||||
|
color: 'white',
|
||||||
|
isNewArrival: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'prod-11',
|
||||||
|
name: 'Jajot J2 - Feature Phone, Single Sim Keypad Phone',
|
||||||
|
price: 1500,
|
||||||
|
oldPrice: 2000,
|
||||||
|
rating: 4.0,
|
||||||
|
reviewsCount: 229,
|
||||||
|
image: 'https://images.unsplash.com/photo-1598327105666-5b89351aff97?w=300&auto=format&fit=crop&q=60',
|
||||||
|
imageHover: 'https://images.unsplash.com/photo-1580910051074-3eb694886505?w=300&auto=format&fit=crop&q=60',
|
||||||
|
discount: 25,
|
||||||
|
category: 'Phones',
|
||||||
|
brand: 'Gallery',
|
||||||
|
color: 'grey',
|
||||||
|
hasTimer: true,
|
||||||
|
timeRemaining: { days: 229, hours: 4, minutes: 50, seconds: 18 },
|
||||||
|
isNewArrival: true,
|
||||||
|
isHot: true,
|
||||||
|
isRecentlyLaunched: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'prod-12',
|
||||||
|
name: 'Laview Home Security Camera HD 1080p',
|
||||||
|
price: 79,
|
||||||
|
oldPrice: 99,
|
||||||
|
rating: 5.0,
|
||||||
|
reviewsCount: 15,
|
||||||
|
image: 'https://images.unsplash.com/photo-1558002038-1055907df827?w=300&auto=format&fit=crop&q=60',
|
||||||
|
imageHover: 'https://images.unsplash.com/photo-1525786210598-d527194d852a?w=300&auto=format&fit=crop&q=60',
|
||||||
|
discount: 11,
|
||||||
|
category: 'Smart Devices',
|
||||||
|
brand: 'Gold',
|
||||||
|
color: 'white',
|
||||||
|
isNewArrival: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'prod-13',
|
||||||
|
name: 'Lenovo IdeaPad Slim 3 Laptop (12th Gen Intel i3)',
|
||||||
|
price: 400,
|
||||||
|
oldPrice: 469,
|
||||||
|
rating: 4.0,
|
||||||
|
reviewsCount: 35,
|
||||||
|
image: 'https://images.unsplash.com/photo-1588872657578-7efd1f1555ed?w=300&auto=format&fit=crop&q=60',
|
||||||
|
imageHover: 'https://images.unsplash.com/photo-1603302576837-37561b2e2302?w=300&auto=format&fit=crop&q=60',
|
||||||
|
discount: 15,
|
||||||
|
category: 'Laptop & Computers',
|
||||||
|
brand: 'Highlight',
|
||||||
|
color: 'grey',
|
||||||
|
isNewArrival: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'prod-14',
|
||||||
|
name: 'Lenovo Tab M9 Tablet 4 GB RAM 64 GB ROM',
|
||||||
|
price: 144,
|
||||||
|
oldPrice: 150,
|
||||||
|
rating: 5.0,
|
||||||
|
reviewsCount: 22,
|
||||||
|
image: 'https://images.unsplash.com/photo-1544244015-0df4b3ffc6b0?w=300&auto=format&fit=crop&q=60',
|
||||||
|
imageHover: 'https://images.unsplash.com/photo-1585776245991-cf89dd7fc73a?w=300&auto=format&fit=crop&q=60',
|
||||||
|
discount: 4,
|
||||||
|
category: 'Smart Devices',
|
||||||
|
brand: 'Modern',
|
||||||
|
color: 'black',
|
||||||
|
isFeatured: true,
|
||||||
|
isTechGadget: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'prod-15',
|
||||||
|
name: 'LG 674 litres Side by Side Refrigerator, Noble Steel',
|
||||||
|
price: 85,
|
||||||
|
oldPrice: 299,
|
||||||
|
rating: 4.0,
|
||||||
|
reviewsCount: 18,
|
||||||
|
image: 'https://images.unsplash.com/photo-1571887463133-d900b77945d8?w=300&auto=format&fit=crop&q=60',
|
||||||
|
imageHover: 'https://images.unsplash.com/photo-1584622650111-993a426fbf0a?w=300&auto=format&fit=crop&q=60',
|
||||||
|
discount: 4,
|
||||||
|
category: 'Smart Devices',
|
||||||
|
brand: 'Nature',
|
||||||
|
color: 'grey',
|
||||||
|
isHot: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'prod-16',
|
||||||
|
name: 'Logitech M350 WHITE Optical Wireless Mouse',
|
||||||
|
price: 299,
|
||||||
|
rating: 5.0,
|
||||||
|
reviewsCount: 94,
|
||||||
|
image: 'https://images.unsplash.com/photo-1615663245857-ac93bb7c39e7?w=300&auto=format&fit=crop&q=60',
|
||||||
|
imageHover: 'https://images.unsplash.com/photo-1625842268584-8f3290447001?w=300&auto=format&fit=crop&q=60',
|
||||||
|
category: 'Smart Devices',
|
||||||
|
brand: 'Sparker',
|
||||||
|
color: 'white',
|
||||||
|
isFeatured: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const promoCards: PromoCardData[] = [
|
||||||
|
{
|
||||||
|
id: 'promo-1',
|
||||||
|
badge: 'Weekend Discount',
|
||||||
|
title: 'iPhone Vision',
|
||||||
|
subtitle: 'Only for this week...',
|
||||||
|
priceText: 'from $ 239.99',
|
||||||
|
image: 'https://images.unsplash.com/photo-1510557880182-3d4d3cba35a5?w=350&auto=format&fit=crop&q=60',
|
||||||
|
bgColor: 'bg-[#F2EDFD]', // soft light lavender
|
||||||
|
textColor: 'text-[#4A154B]',
|
||||||
|
badgeBg: 'bg-[#7B2CBF] text-white',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'promo-2',
|
||||||
|
badge: 'Amazing Deal',
|
||||||
|
title: 'Vibe Your Sound',
|
||||||
|
subtitle: 'This Week Exclusively...',
|
||||||
|
priceText: 'from $ 399.99',
|
||||||
|
image: 'https://images.unsplash.com/photo-1545454675-3531b543be5d?w=350&auto=format&fit=crop&q=60',
|
||||||
|
bgColor: 'bg-[#E3F2FD]', // soft light blue
|
||||||
|
textColor: 'text-primary-dark',
|
||||||
|
badgeBg: 'bg-primary text-white',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'promo-3',
|
||||||
|
badge: 'Flat 30% OFF',
|
||||||
|
title: 'True Harmony',
|
||||||
|
subtitle: 'Limited Time Only...',
|
||||||
|
priceText: 'from $ 189.99',
|
||||||
|
image: 'https://images.unsplash.com/photo-1590658268037-6bf12165a8df?w=350&auto=format&fit=crop&q=60',
|
||||||
|
bgColor: 'bg-[#FCE4EC]', // soft light pink
|
||||||
|
textColor: 'text-[#880E4F]',
|
||||||
|
badgeBg: 'bg-[#EC407A] text-white',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const brands = [
|
||||||
|
{ name: 'Amazon', logo: 'amazon' },
|
||||||
|
{ name: 'AMD', logo: 'amd' },
|
||||||
|
{ name: 'Facebook', logo: 'facebook' },
|
||||||
|
{ name: 'Logitech', logo: 'logitech' },
|
||||||
|
{ name: 'Apper', logo: 'apper' },
|
||||||
|
{ name: 'Hooli', logo: 'hooli' },
|
||||||
|
{ name: 'FedEx', logo: 'fedex' },
|
||||||
|
{ name: 'PayPal', logo: 'paypal' },
|
||||||
|
{ name: 'Netflix', logo: 'netflix' },
|
||||||
|
{ name: 'Spotify', logo: 'spotify' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const blogs: BlogArticle[] = [
|
||||||
|
{
|
||||||
|
id: 'blog-1',
|
||||||
|
title: 'The Future of Wireless Audio: True Stereo Earbuds',
|
||||||
|
excerpt: 'Explore how audio drivers and high bitrates are redefining our daily listening routines...',
|
||||||
|
date: 'July 10, 2026',
|
||||||
|
author: 'Sarah Connor',
|
||||||
|
image: 'https://images.unsplash.com/photo-1590658268037-6bf12165a8df?w=400&auto=format&fit=crop&q=60',
|
||||||
|
readTime: '5 min read',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'blog-2',
|
||||||
|
title: 'Top 5 Tech Gadgets Every Student Needs in 2026',
|
||||||
|
excerpt: 'From digital note-taking tablets to portable power delivery adapters, stay ahead of the class...',
|
||||||
|
date: 'July 05, 2026',
|
||||||
|
author: 'James Smith',
|
||||||
|
image: 'https://images.unsplash.com/photo-1527443224154-c4a3942d3acf?w=400&auto=format&fit=crop&q=60',
|
||||||
|
readTime: '8 min read',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'blog-3',
|
||||||
|
title: 'How Smartwatches Changed Health Monitoring Forever',
|
||||||
|
excerpt: 'Continuous heart-rate checks, oxygen metrics, and sleep charts fit cleanly on your wrist...',
|
||||||
|
date: 'June 28, 2026',
|
||||||
|
author: 'Dr. Evelyn L.',
|
||||||
|
image: 'https://images.unsplash.com/photo-1579586337278-3befd40fd17a?w=400&auto=format&fit=crop&q=60',
|
||||||
|
readTime: '6 min read',
|
||||||
|
},
|
||||||
|
];
|
||||||
6
NewFile/src/src/utils/utils.ts
Normal file
6
NewFile/src/src/utils/utils.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { type ClassValue, clsx } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
50
NewFile/src/tailwind.config.js
Normal file
50
NewFile/src/tailwind.config.js
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
container: {
|
||||||
|
center: true,
|
||||||
|
padding: {
|
||||||
|
DEFAULT: '1rem',
|
||||||
|
sm: '1.5rem',
|
||||||
|
lg: '2rem',
|
||||||
|
},
|
||||||
|
screens: {
|
||||||
|
sm: '640px',
|
||||||
|
md: '768px',
|
||||||
|
lg: '1024px',
|
||||||
|
xl: '1280px',
|
||||||
|
'2xl': '1400px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
DEFAULT: "#1976F3",
|
||||||
|
hover: "#1565C0",
|
||||||
|
light: "#E3F2FD",
|
||||||
|
dark: "#0D47A1",
|
||||||
|
},
|
||||||
|
bgLight: "#F8F9FB",
|
||||||
|
cardLight: "#FFFFFF",
|
||||||
|
textMain: "#1E293B",
|
||||||
|
textMuted: "#64748B",
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
'theme': '12px',
|
||||||
|
},
|
||||||
|
boxShadow: {
|
||||||
|
'soft': '0 4px 20px -2px rgba(0, 0, 0, 0.05), 0 2px 10px -1px rgba(0, 0, 0, 0.03)',
|
||||||
|
'soft-hover': '0 10px 30px -5px rgba(0, 0, 0, 0.08), 0 4px 15px -2px rgba(0, 0, 0, 0.04)',
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Inter', 'sans-serif'],
|
||||||
|
outfit: ['Outfit', 'sans-serif'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
55
NewFile/src/tailwind.config.ts
Normal file
55
NewFile/src/tailwind.config.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import type { Config } from "tailwindcss";
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
content: [
|
||||||
|
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./src/layout/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./src/sections/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
container: {
|
||||||
|
center: true,
|
||||||
|
padding: {
|
||||||
|
DEFAULT: '1rem',
|
||||||
|
sm: '1.5rem',
|
||||||
|
lg: '2rem',
|
||||||
|
},
|
||||||
|
screens: {
|
||||||
|
sm: '640px',
|
||||||
|
md: '768px',
|
||||||
|
lg: '1024px',
|
||||||
|
xl: '1280px',
|
||||||
|
'2xl': '1400px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
DEFAULT: "#1976F3",
|
||||||
|
hover: "#1565C0",
|
||||||
|
light: "#E3F2FD",
|
||||||
|
dark: "#0D47A1",
|
||||||
|
},
|
||||||
|
bgLight: "#F8F9FB",
|
||||||
|
cardLight: "#FFFFFF",
|
||||||
|
textMain: "#1E293B",
|
||||||
|
textMuted: "#64748B",
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
'theme': '12px', // changed from 16px to 12px per GEMINI.md rules
|
||||||
|
},
|
||||||
|
boxShadow: {
|
||||||
|
'soft': '0 4px 20px -2px rgba(0, 0, 0, 0.05), 0 2px 10px -1px rgba(0, 0, 0, 0.03)',
|
||||||
|
'soft-hover': '0 10px 30px -5px rgba(0, 0, 0, 0.08), 0 4px 15px -2px rgba(0, 0, 0, 0.04)',
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Inter', 'sans-serif'],
|
||||||
|
outfit: ['Outfit', 'sans-serif'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
|
export default config;
|
||||||
26
NewFile/src/tsconfig.json
Normal file
26
NewFile/src/tsconfig.json
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "vite.config.ts", "src/main.tsx", "src/App.tsx"]
|
||||||
|
}
|
||||||
1
NewFile/src/tsconfig.tsbuildinfo
Normal file
1
NewFile/src/tsconfig.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
17
NewFile/src/vite.config.ts
Normal file
17
NewFile/src/vite.config.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
12
app/about/layout.tsx
Normal file
12
app/about/layout.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'About Us | iFixKart',
|
||||||
|
description:
|
||||||
|
'iFixKart helps you buy certified refurbished gadgets, book device repairs, and keep electronics in use longer.',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AboutLayout({ children }: { children: ReactNode }) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
121
app/about/page.tsx
Normal file
121
app/about/page.tsx
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { Mail, MapPin, Phone, Smartphone, Wrench } from 'lucide-react';
|
||||||
|
import { AnnouncementBar } from '@/layout/AnnouncementBar';
|
||||||
|
import { Header } from '@/layout/Header';
|
||||||
|
import { Navbar } from '@/layout/Navbar';
|
||||||
|
import { Footer } from '@/layout/Footer';
|
||||||
|
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
|
||||||
|
import { FloatingButtons } from '@/components/FloatingButtons';
|
||||||
|
import {
|
||||||
|
storefrontService,
|
||||||
|
type FooterInfo,
|
||||||
|
type StorefrontSettings,
|
||||||
|
MOCK_FOOTER_INFO,
|
||||||
|
} from '@/services/api/storefrontService';
|
||||||
|
|
||||||
|
export default function AboutPage() {
|
||||||
|
const [footer, setFooter] = useState<FooterInfo>(MOCK_FOOTER_INFO);
|
||||||
|
const [settings, setSettings] = useState<StorefrontSettings>({ store_name: 'iFixKart' });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
Promise.all([
|
||||||
|
storefrontService.getFooterInfo(),
|
||||||
|
storefrontService.getSettings(),
|
||||||
|
]).then(([info, store]) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
if (info) setFooter(info);
|
||||||
|
if (store) setSettings(store);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const storeName = settings.store_name || 'iFixKart';
|
||||||
|
const phone = footer.phone || settings.support_phone || '';
|
||||||
|
const email = footer.email || '';
|
||||||
|
const address = footer.address || '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[#F8F9FB] text-[#1E293B] flex flex-col font-sans">
|
||||||
|
<AnnouncementBar />
|
||||||
|
<Header />
|
||||||
|
<StickyHeaderSpacer />
|
||||||
|
<Navbar />
|
||||||
|
|
||||||
|
<main className="flex-grow py-10 pb-20">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[860px]">
|
||||||
|
<h1 className="text-[28px] md:text-[32px] font-bold text-[#1E293B]">
|
||||||
|
About Us
|
||||||
|
</h1>
|
||||||
|
<p className="text-[15px] text-gray-600 leading-relaxed mt-3">
|
||||||
|
{storeName} sells tested refurbished phones and accessories, and books device repairs.
|
||||||
|
You can shop online, get a screen or battery replaced, or exchange the phone you
|
||||||
|
already have.
|
||||||
|
</p>
|
||||||
|
<p className="text-[15px] text-gray-600 leading-relaxed mt-3">
|
||||||
|
Repairs are done at our Bengaluru service center or at your doorstep. Refurbished
|
||||||
|
phones are checked before listing and sold with a limited warranty.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-8">
|
||||||
|
<Link
|
||||||
|
href="/shop"
|
||||||
|
className="bg-white border border-gray-200 rounded-xl p-5 hover:border-primary/40 transition-colors"
|
||||||
|
>
|
||||||
|
<Smartphone className="w-5 h-5 text-primary mb-3" />
|
||||||
|
<h2 className="text-[16px] font-bold text-[#1E293B]">Shop</h2>
|
||||||
|
<p className="text-[13px] text-gray-500 mt-1 leading-relaxed">
|
||||||
|
Refurbished phones, spare parts, and accessories.
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/repair-services"
|
||||||
|
className="bg-white border border-gray-200 rounded-xl p-5 hover:border-primary/40 transition-colors"
|
||||||
|
>
|
||||||
|
<Wrench className="w-5 h-5 text-primary mb-3" />
|
||||||
|
<h2 className="text-[16px] font-bold text-[#1E293B]">Repair</h2>
|
||||||
|
<p className="text-[13px] text-gray-500 mt-1 leading-relaxed">
|
||||||
|
Screens, batteries, charging ports, and other common jobs.
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 bg-white border border-gray-200 rounded-xl p-5 md:p-6 space-y-4">
|
||||||
|
{address ? (
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<MapPin className="w-4 h-4 text-gray-400 mt-0.5 shrink-0" />
|
||||||
|
<p className="text-[14px] text-gray-600 leading-relaxed">{address}</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{phone ? (
|
||||||
|
<a
|
||||||
|
href={`tel:${phone.replace(/[^\d+]/g, '')}`}
|
||||||
|
className="flex gap-3 text-[14px] text-gray-600 hover:text-primary"
|
||||||
|
>
|
||||||
|
<Phone className="w-4 h-4 text-gray-400 mt-0.5 shrink-0" />
|
||||||
|
{phone}
|
||||||
|
</a>
|
||||||
|
) : null}
|
||||||
|
{email ? (
|
||||||
|
<a
|
||||||
|
href={`mailto:${email}`}
|
||||||
|
className="flex gap-3 text-[14px] text-gray-600 hover:text-primary"
|
||||||
|
>
|
||||||
|
<Mail className="w-4 h-4 text-gray-400 mt-0.5 shrink-0" />
|
||||||
|
{email}
|
||||||
|
</a>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
<FloatingButtons />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
app/account/loading.tsx
Normal file
5
app/account/loading.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { PageSkeleton } from '@/components/ui/Skeleton';
|
||||||
|
|
||||||
|
export default function Loading() {
|
||||||
|
return <PageSkeleton variant="simple" />;
|
||||||
|
}
|
||||||
990
app/account/page.tsx
Normal file
990
app/account/page.tsx
Normal file
|
|
@ -0,0 +1,990 @@
|
||||||
|
/**
|
||||||
|
* @page Customer Account & Orders (`app/account/page.tsx`)
|
||||||
|
* @purpose TechShop-styled customer account: profile, addresses, orders, settings.
|
||||||
|
* @dependencies useAuthStore, apiFetch
|
||||||
|
*/
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, useEffect, Suspense } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import {
|
||||||
|
User,
|
||||||
|
Package,
|
||||||
|
MapPin,
|
||||||
|
Settings,
|
||||||
|
LogOut,
|
||||||
|
Plus,
|
||||||
|
Edit2,
|
||||||
|
Trash2,
|
||||||
|
Check,
|
||||||
|
ShieldCheck,
|
||||||
|
ShoppingBag,
|
||||||
|
Loader2,
|
||||||
|
KeyRound,
|
||||||
|
X,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { useAuthStore } from '@/store/authStore';
|
||||||
|
import { apiFetch, clearTokens } from '@/services/api/client';
|
||||||
|
import { formatCurrency } from '@/lib/utils';
|
||||||
|
import GoogleAuthModal from '@/components/auth/GoogleAuthModal';
|
||||||
|
import { AnnouncementBar } from '@/layout/AnnouncementBar';
|
||||||
|
import { Header } from '@/layout/Header';
|
||||||
|
import { Navbar } from '@/layout/Navbar';
|
||||||
|
import { Footer } from '@/layout/Footer';
|
||||||
|
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
|
||||||
|
import { PhoneInput, PincodeInput } from '@/components/PhoneInput';
|
||||||
|
import { validatePhone, validatePincode, validateAddress } from '@/lib/validation';
|
||||||
|
import { FloatingButtons } from '@/components/FloatingButtons';
|
||||||
|
|
||||||
|
type TabType = 'dashboard' | 'orders' | 'addresses' | 'settings';
|
||||||
|
|
||||||
|
const VALID_TABS: TabType[] = [
|
||||||
|
'dashboard',
|
||||||
|
'orders',
|
||||||
|
'addresses',
|
||||||
|
'settings',
|
||||||
|
];
|
||||||
|
|
||||||
|
function parseTab(raw: string | null): TabType {
|
||||||
|
if (raw && VALID_TABS.includes(raw as TabType)) return raw as TabType;
|
||||||
|
return 'dashboard';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Address {
|
||||||
|
address_id?: string;
|
||||||
|
address_type: string;
|
||||||
|
full_name: string;
|
||||||
|
phone: string;
|
||||||
|
street_address: string;
|
||||||
|
city: string;
|
||||||
|
state: string;
|
||||||
|
pincode: string;
|
||||||
|
is_default: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Order {
|
||||||
|
order_id: string;
|
||||||
|
order_no: string;
|
||||||
|
final_amount: number;
|
||||||
|
status: string;
|
||||||
|
payment_status: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TABS: { id: TabType; label: string; icon: typeof User }[] = [
|
||||||
|
{ id: 'dashboard', label: 'Dashboard', icon: User },
|
||||||
|
{ id: 'orders', label: 'Order History', icon: Package },
|
||||||
|
{ id: 'addresses', label: 'Address Book', icon: MapPin },
|
||||||
|
{ id: 'settings', label: 'Settings', icon: Settings },
|
||||||
|
];
|
||||||
|
|
||||||
|
const inputClass =
|
||||||
|
'w-full px-3 py-2.5 bg-white border border-[#e5e5e5] text-[13px] text-[#222] outline-none focus:border-primary transition-colors';
|
||||||
|
|
||||||
|
const labelClass =
|
||||||
|
'block text-[11px] font-semibold text-[#666] uppercase tracking-wide mb-1.5';
|
||||||
|
|
||||||
|
function AccountDashboardPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const {
|
||||||
|
isAuthenticated,
|
||||||
|
clearAuth,
|
||||||
|
setAuth,
|
||||||
|
authReady,
|
||||||
|
bootstrapAuth,
|
||||||
|
} = useAuthStore();
|
||||||
|
const [authModalOpen, setAuthModalOpen] = useState(false);
|
||||||
|
const [activeTab, setActiveTab] = useState<TabType>(() =>
|
||||||
|
parseTab(searchParams.get('tab'))
|
||||||
|
);
|
||||||
|
|
||||||
|
const [profile, setProfile] = useState<any>(null);
|
||||||
|
const [addresses, setAddresses] = useState<Address[]>([]);
|
||||||
|
const [orders, setOrders] = useState<Order[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const [addressModalOpen, setAddressModalOpen] = useState(false);
|
||||||
|
const [editingAddress, setEditingAddress] = useState<Address | null>(null);
|
||||||
|
const [addressForm, setAddressForm] = useState<Address>({
|
||||||
|
address_type: 'SHIPPING',
|
||||||
|
full_name: '',
|
||||||
|
phone: '',
|
||||||
|
street_address: '',
|
||||||
|
city: '',
|
||||||
|
state: '',
|
||||||
|
pincode: '',
|
||||||
|
is_default: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [settingsForm, setSettingsForm] = useState({
|
||||||
|
first_name: '',
|
||||||
|
last_name: '',
|
||||||
|
phone: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [actionLoading, setActionLoading] = useState(false);
|
||||||
|
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void bootstrapAuth();
|
||||||
|
}, [bootstrapAuth]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setActiveTab(parseTab(searchParams.get('tab')));
|
||||||
|
}, [searchParams]);
|
||||||
|
|
||||||
|
const goToTab = (tab: TabType) => {
|
||||||
|
setActiveTab(tab);
|
||||||
|
setErrorMsg(null);
|
||||||
|
const qs = tab === 'dashboard' ? '/account' : `/account?tab=${tab}`;
|
||||||
|
router.replace(qs, { scroll: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!authReady) return;
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
setAuthModalOpen(true);
|
||||||
|
} else {
|
||||||
|
setAuthModalOpen(false);
|
||||||
|
fetchAccountData();
|
||||||
|
}
|
||||||
|
}, [isAuthenticated, authReady]);
|
||||||
|
|
||||||
|
const fetchAccountData = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setErrorMsg(null);
|
||||||
|
try {
|
||||||
|
const prof = await apiFetch<any>('/api/v1/customer/profile');
|
||||||
|
setProfile(prof);
|
||||||
|
setSettingsForm({
|
||||||
|
first_name: prof.first_name || '',
|
||||||
|
last_name: prof.last_name || '',
|
||||||
|
phone: prof.phone || '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const addrs = await apiFetch<Address[]>(
|
||||||
|
'/api/v1/customer/profile/addresses'
|
||||||
|
);
|
||||||
|
setAddresses(addrs);
|
||||||
|
|
||||||
|
const ords = await apiFetch<Order[]>('/api/v1/orders');
|
||||||
|
setOrders(ords);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(err);
|
||||||
|
setErrorMsg(err.message || 'Failed to retrieve account records.');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
try {
|
||||||
|
await apiFetch('/api/v1/customer/auth/logout', {
|
||||||
|
method: 'POST',
|
||||||
|
skipAuth: true,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Logout endpoint fail:', err);
|
||||||
|
} finally {
|
||||||
|
clearTokens();
|
||||||
|
clearAuth();
|
||||||
|
setProfile(null);
|
||||||
|
setAddresses([]);
|
||||||
|
setOrders([]);
|
||||||
|
router.push('/');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleProfileUpdate = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setErrorMsg(null);
|
||||||
|
if (settingsForm.phone) {
|
||||||
|
const phoneErr = validatePhone(settingsForm.phone);
|
||||||
|
if (phoneErr) {
|
||||||
|
setErrorMsg(phoneErr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setActionLoading(true);
|
||||||
|
try {
|
||||||
|
const updated = await apiFetch<any>('/api/v1/customer/profile', {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(settingsForm),
|
||||||
|
});
|
||||||
|
setProfile(updated);
|
||||||
|
setAuth(useAuthStore.getState().accessToken || '', updated);
|
||||||
|
alert('Profile updated successfully!');
|
||||||
|
} catch (err: any) {
|
||||||
|
setErrorMsg(err.message || 'Failed to update profile.');
|
||||||
|
} finally {
|
||||||
|
setActionLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddressSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setErrorMsg(null);
|
||||||
|
|
||||||
|
const phoneErr = validatePhone(addressForm.phone, true);
|
||||||
|
if (phoneErr) {
|
||||||
|
setErrorMsg(phoneErr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pincodeErr = validatePincode(addressForm.pincode, true);
|
||||||
|
if (pincodeErr) {
|
||||||
|
setErrorMsg(pincodeErr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const addressErr = validateAddress(addressForm.street_address, true);
|
||||||
|
if (addressErr) {
|
||||||
|
setErrorMsg(addressErr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setActionLoading(true);
|
||||||
|
try {
|
||||||
|
if (editingAddress?.address_id) {
|
||||||
|
await apiFetch<Address>(
|
||||||
|
`/api/v1/customer/profile/addresses/${editingAddress.address_id}`,
|
||||||
|
{
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(addressForm),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await apiFetch<Address>('/api/v1/customer/profile/addresses', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(addressForm),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setAddressModalOpen(false);
|
||||||
|
setEditingAddress(null);
|
||||||
|
const addrs = await apiFetch<Address[]>(
|
||||||
|
'/api/v1/customer/profile/addresses'
|
||||||
|
);
|
||||||
|
setAddresses(addrs);
|
||||||
|
} catch (err: any) {
|
||||||
|
setErrorMsg(err.message || 'Failed to save address record.');
|
||||||
|
} finally {
|
||||||
|
setActionLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditAddressClick = (addr: Address) => {
|
||||||
|
setEditingAddress(addr);
|
||||||
|
setAddressForm({
|
||||||
|
address_type: addr.address_type,
|
||||||
|
full_name: addr.full_name,
|
||||||
|
phone: addr.phone,
|
||||||
|
street_address: addr.street_address,
|
||||||
|
city: addr.city,
|
||||||
|
state: addr.state,
|
||||||
|
pincode: addr.pincode,
|
||||||
|
is_default: addr.is_default,
|
||||||
|
});
|
||||||
|
setAddressModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteAddress = async (addressId: string) => {
|
||||||
|
if (!confirm('Are you sure you want to delete this address?')) return;
|
||||||
|
setActionLoading(true);
|
||||||
|
try {
|
||||||
|
await apiFetch(`/api/v1/customer/profile/addresses/${addressId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
const addrs = await apiFetch<Address[]>(
|
||||||
|
'/api/v1/customer/profile/addresses'
|
||||||
|
);
|
||||||
|
setAddresses(addrs);
|
||||||
|
} catch (err: any) {
|
||||||
|
setErrorMsg(err.message || 'Failed to delete address.');
|
||||||
|
} finally {
|
||||||
|
setActionLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelOrder = async (orderId: string) => {
|
||||||
|
if (!confirm('Are you sure you want to cancel this order?')) return;
|
||||||
|
setActionLoading(true);
|
||||||
|
try {
|
||||||
|
await apiFetch(`/api/v1/orders/${orderId}/cancel`, { method: 'POST' });
|
||||||
|
setOrders(
|
||||||
|
orders.map((o) =>
|
||||||
|
o.order_id === orderId ? { ...o, status: 'CANCELLED' } : o
|
||||||
|
)
|
||||||
|
);
|
||||||
|
alert('Order cancelled successfully.');
|
||||||
|
} catch (err: any) {
|
||||||
|
setErrorMsg(err.message || 'Failed to cancel order.');
|
||||||
|
} finally {
|
||||||
|
setActionLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openNewAddressModal = () => {
|
||||||
|
setEditingAddress(null);
|
||||||
|
setAddressForm({
|
||||||
|
address_type: 'SHIPPING',
|
||||||
|
full_name: '',
|
||||||
|
phone: '',
|
||||||
|
street_address: '',
|
||||||
|
city: '',
|
||||||
|
state: '',
|
||||||
|
pincode: '',
|
||||||
|
is_default: false,
|
||||||
|
});
|
||||||
|
setAddressModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultAddress = addresses.find((a) => a.is_default);
|
||||||
|
const activeTabMeta = TABS.find((t) => t.id === activeTab);
|
||||||
|
|
||||||
|
const pageShell = (children: React.ReactNode) => (
|
||||||
|
<div className="min-h-screen bg-[#F8F9FB] text-[#1E293B] flex flex-col font-sans">
|
||||||
|
<AnnouncementBar />
|
||||||
|
<Header />
|
||||||
|
<StickyHeaderSpacer />
|
||||||
|
<Navbar />
|
||||||
|
{children}
|
||||||
|
<Footer />
|
||||||
|
<FloatingButtons />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!authReady) {
|
||||||
|
return (
|
||||||
|
<div className="w-full min-h-screen bg-white flex items-center justify-center">
|
||||||
|
<Loader2 className="w-8 h-8 text-primary animate-spin" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return pageShell(
|
||||||
|
<>
|
||||||
|
<main className="flex-grow pb-16">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1400px] py-10 flex justify-center">
|
||||||
|
<div className="max-w-[420px] w-full bg-white border border-[#e5e5e5] overflow-hidden">
|
||||||
|
<div className="px-6 pt-8 pb-2 text-center space-y-3">
|
||||||
|
<div className="text-[24px] font-extrabold tracking-tight leading-none">
|
||||||
|
<span className="text-primary">iFix</span>
|
||||||
|
<span className="text-[#222]">Kart</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-14 h-14 bg-primary/10 text-primary flex items-center justify-center mx-auto">
|
||||||
|
<KeyRound className="w-7 h-7" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<h2 className="text-[20px] font-bold text-[#222]">
|
||||||
|
Sign in to your account
|
||||||
|
</h2>
|
||||||
|
<p className="text-[13px] text-gray-500 leading-relaxed px-2">
|
||||||
|
View your profile, manage shipping addresses, and track
|
||||||
|
order history in one place.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-6 py-6 space-y-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setAuthModalOpen(true)}
|
||||||
|
className="w-full bg-primary hover:bg-primary-hover text-white font-semibold py-3.5 text-[14px] transition cursor-pointer"
|
||||||
|
>
|
||||||
|
Continue with Google
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<GoogleAuthModal
|
||||||
|
isOpen={authModalOpen}
|
||||||
|
onClose={() => setAuthModalOpen(false)}
|
||||||
|
onSuccess={() => {
|
||||||
|
setAuthModalOpen(false);
|
||||||
|
router.push('/');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pageShell(
|
||||||
|
<>
|
||||||
|
<main className="flex-grow pb-16 antialiased">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1400px] py-8">
|
||||||
|
{/* Profile strip */}
|
||||||
|
<div className="bg-white border border-[#e5e5e5] px-5 py-4 mb-6 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||||
|
<div className="flex items-center gap-3.5 min-w-0">
|
||||||
|
<div className="w-12 h-12 shrink-0 bg-primary text-white flex items-center justify-center text-[15px] font-bold select-none">
|
||||||
|
{profile?.first_name?.charAt(0).toUpperCase() || 'U'}
|
||||||
|
{profile?.last_name?.charAt(0).toUpperCase() || ''}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 text-left">
|
||||||
|
<p className="text-[16px] font-bold text-[#222] truncate">
|
||||||
|
{profile?.first_name} {profile?.last_name}
|
||||||
|
</p>
|
||||||
|
<p className="text-[12px] text-gray-500 truncate">
|
||||||
|
{profile?.email}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="inline-flex items-center gap-2 text-[13px] font-medium text-[#666] hover:text-primary transition-colors cursor-pointer self-start sm:self-auto"
|
||||||
|
>
|
||||||
|
<LogOut className="w-4 h-4" />
|
||||||
|
Sign Out
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{errorMsg && (
|
||||||
|
<div className="px-4 py-3 mb-6 text-[13px] text-red-600 bg-red-50 border border-red-200">
|
||||||
|
{errorMsg}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="w-full py-20 flex justify-center items-center bg-white border border-[#e5e5e5]">
|
||||||
|
<Loader2 className="w-8 h-8 animate-spin text-primary" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col lg:flex-row gap-6 lg:gap-8 items-start">
|
||||||
|
{/* Sidebar — matches category rail language */}
|
||||||
|
<aside className="w-full lg:w-[260px] shrink-0 bg-white border border-[#e5e5e5]">
|
||||||
|
<ul className="flex flex-col">
|
||||||
|
{TABS.map((tab, idx) => {
|
||||||
|
const isActive = activeTab === tab.id;
|
||||||
|
return (
|
||||||
|
<li key={tab.id}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
goToTab(tab.id);
|
||||||
|
}}
|
||||||
|
className={`w-full flex items-center gap-3 px-5 py-[13px] text-[13px] font-medium transition-colors text-left cursor-pointer ${
|
||||||
|
idx < TABS.length - 1
|
||||||
|
? 'border-b border-[#ececec]'
|
||||||
|
: ''
|
||||||
|
} ${
|
||||||
|
isActive
|
||||||
|
? 'text-primary bg-[#f7f9fc]'
|
||||||
|
: 'text-[#333] hover:text-primary hover:bg-[#f7f9fc]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<tab.icon
|
||||||
|
className={`w-4 h-4 shrink-0 ${
|
||||||
|
isActive ? 'text-primary' : 'text-gray-400'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* Content panel */}
|
||||||
|
<div className="flex-1 min-w-0 w-full bg-white border border-[#e5e5e5]">
|
||||||
|
<div className="flex items-center justify-between gap-4 px-5 md:px-6 py-4 border-b border-[#e5e5e5]">
|
||||||
|
<h2 className="text-[18px] md:text-[20px] font-bold text-[#222] tracking-tight">
|
||||||
|
{activeTabMeta?.label}
|
||||||
|
</h2>
|
||||||
|
{activeTab === 'addresses' ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={openNewAddressModal}
|
||||||
|
className="inline-flex items-center gap-1.5 bg-primary hover:bg-primary-hover text-white text-[12px] font-semibold px-3.5 py-2 transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<Plus className="w-3.5 h-3.5" />
|
||||||
|
New Address
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-5 md:p-6">
|
||||||
|
{/* Dashboard */}
|
||||||
|
{activeTab === 'dashboard' && (
|
||||||
|
<div className="space-y-5">
|
||||||
|
<p className="text-[13px] text-gray-500">
|
||||||
|
Hello, {profile?.first_name}! View recent activity and
|
||||||
|
manage your account here.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="border border-[#e5e5e5] p-5 flex flex-col justify-between min-h-[160px]">
|
||||||
|
<div>
|
||||||
|
<span className="text-[11px] font-semibold text-[#888] uppercase tracking-wide block mb-2.5">
|
||||||
|
Default Shipping Address
|
||||||
|
</span>
|
||||||
|
{defaultAddress ? (
|
||||||
|
<p className="text-[13px] text-[#444] leading-relaxed">
|
||||||
|
<span className="font-bold text-[#222] block mb-1">
|
||||||
|
{defaultAddress.full_name}
|
||||||
|
</span>
|
||||||
|
{defaultAddress.street_address}
|
||||||
|
<br />
|
||||||
|
{defaultAddress.city}, {defaultAddress.state} –{' '}
|
||||||
|
{defaultAddress.pincode}
|
||||||
|
<br />
|
||||||
|
<span className="text-gray-500">
|
||||||
|
Phone: {defaultAddress.phone}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-[13px] text-gray-500">
|
||||||
|
No default address saved in address book.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => goToTab('addresses')}
|
||||||
|
className="mt-4 text-primary hover:underline text-[13px] font-semibold text-left w-max cursor-pointer"
|
||||||
|
>
|
||||||
|
Manage Addresses
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border border-[#e5e5e5] p-5 flex flex-col justify-between min-h-[160px]">
|
||||||
|
<div>
|
||||||
|
<span className="text-[11px] font-semibold text-[#888] uppercase tracking-wide block mb-2.5">
|
||||||
|
Recent Order Activity
|
||||||
|
</span>
|
||||||
|
{orders.length > 0 ? (
|
||||||
|
<div className="text-[13px] text-[#444] space-y-1">
|
||||||
|
<p className="font-bold text-[#222]">
|
||||||
|
Order {orders[0].order_no}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Total:{' '}
|
||||||
|
{formatCurrency(orders[0].final_amount)}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Status:{' '}
|
||||||
|
<span className="text-primary font-semibold">
|
||||||
|
{orders[0].status.replace('_', ' ')}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-[13px] text-gray-500">
|
||||||
|
No purchase order history recorded.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => goToTab('orders')}
|
||||||
|
className="mt-4 text-primary hover:underline text-[13px] font-semibold text-left w-max cursor-pointer"
|
||||||
|
>
|
||||||
|
Track Purchases
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Orders */}
|
||||||
|
{activeTab === 'orders' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{orders.length === 0 ? (
|
||||||
|
<div className="py-14 text-center border border-dashed border-[#e5e5e5]">
|
||||||
|
<ShoppingBag className="w-10 h-10 mx-auto text-gray-300 mb-3" />
|
||||||
|
<p className="text-[13px] font-semibold text-[#444]">
|
||||||
|
No orders placed yet.
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href="/shop"
|
||||||
|
className="inline-block mt-3 text-[13px] font-semibold text-primary hover:underline"
|
||||||
|
>
|
||||||
|
Continue Shopping
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="border border-[#e5e5e5] divide-y divide-[#e5e5e5]">
|
||||||
|
{orders.map((order) => (
|
||||||
|
<div
|
||||||
|
key={order.order_id}
|
||||||
|
className="px-4 py-4 flex flex-col sm:flex-row sm:items-center justify-between gap-3"
|
||||||
|
>
|
||||||
|
<div className="space-y-0.5 text-left">
|
||||||
|
<span className="font-bold text-[#222] block text-[14px]">
|
||||||
|
{order.order_no}
|
||||||
|
</span>
|
||||||
|
<span className="text-gray-400 block text-[11px]">
|
||||||
|
Placed:{' '}
|
||||||
|
{new Date(
|
||||||
|
order.created_at.endsWith('Z') ||
|
||||||
|
order.created_at.includes('+')
|
||||||
|
? order.created_at
|
||||||
|
: order.created_at + 'Z'
|
||||||
|
).toLocaleDateString('en-IN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap items-center gap-3 sm:gap-5">
|
||||||
|
<div className="text-left sm:text-right">
|
||||||
|
<span className="font-bold text-[#222] block text-[14px]">
|
||||||
|
{formatCurrency(order.final_amount)}
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] text-gray-400 uppercase block font-semibold tracking-wider">
|
||||||
|
{order.payment_status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={`text-[10px] font-bold px-2 py-0.5 uppercase tracking-wide border ${
|
||||||
|
order.status === 'DELIVERED'
|
||||||
|
? 'border-emerald-200 text-emerald-700 bg-emerald-50'
|
||||||
|
: order.status === 'CANCELLED'
|
||||||
|
? 'border-red-200 text-red-700 bg-red-50'
|
||||||
|
: 'border-primary/20 text-primary bg-primary/5'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{order.status.replace('_', ' ')}
|
||||||
|
</span>
|
||||||
|
{(order.status === 'ORDER_CREATED' ||
|
||||||
|
order.status === 'PAYMENT_PENDING') && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
handleCancelOrder(order.order_id)
|
||||||
|
}
|
||||||
|
className="px-3 py-1 border border-red-200 text-red-600 hover:bg-red-50 font-semibold text-[11px] uppercase tracking-wider transition cursor-pointer"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Addresses */}
|
||||||
|
{activeTab === 'addresses' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{addresses.length === 0 ? (
|
||||||
|
<div className="py-14 text-center border border-dashed border-[#e5e5e5]">
|
||||||
|
<MapPin className="w-10 h-10 mx-auto text-gray-300 mb-3" />
|
||||||
|
<p className="text-[13px] font-semibold text-[#444]">
|
||||||
|
No addresses saved yet.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={openNewAddressModal}
|
||||||
|
className="inline-block mt-3 text-[13px] font-semibold text-primary hover:underline cursor-pointer"
|
||||||
|
>
|
||||||
|
Add your first address
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{addresses.map((addr) => (
|
||||||
|
<div
|
||||||
|
key={addr.address_id}
|
||||||
|
className={`p-4 border flex flex-col justify-between ${
|
||||||
|
addr.is_default
|
||||||
|
? 'border-primary'
|
||||||
|
: 'border-[#e5e5e5]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="text-[10px] font-bold uppercase bg-[#f5f5f5] px-2 py-0.5 text-[#666]">
|
||||||
|
{addr.address_type}
|
||||||
|
</span>
|
||||||
|
{addr.is_default && (
|
||||||
|
<span className="text-[10px] font-bold text-primary flex items-center gap-1">
|
||||||
|
<Check className="w-3.5 h-3.5" />
|
||||||
|
Default
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<h4 className="font-bold text-[#222] text-[14px] mb-1">
|
||||||
|
{addr.full_name}
|
||||||
|
</h4>
|
||||||
|
<p className="text-[13px] text-[#555] leading-relaxed">
|
||||||
|
{addr.street_address}
|
||||||
|
<br />
|
||||||
|
{addr.city}, {addr.state} – {addr.pincode}
|
||||||
|
</p>
|
||||||
|
<p className="text-[12px] text-gray-500 mt-2">
|
||||||
|
Phone: {addr.phone}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-4 mt-4 pt-3 border-t border-[#ececec]">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleEditAddressClick(addr)}
|
||||||
|
className="text-[#666] hover:text-primary text-[12px] font-semibold flex items-center gap-1 cursor-pointer"
|
||||||
|
>
|
||||||
|
<Edit2 className="w-3.5 h-3.5" /> Edit
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
addr.address_id &&
|
||||||
|
handleDeleteAddress(addr.address_id)
|
||||||
|
}
|
||||||
|
className="text-[#666] hover:text-red-600 text-[12px] font-semibold flex items-center gap-1 cursor-pointer"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3.5 h-3.5" /> Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Settings */}
|
||||||
|
{activeTab === 'settings' && (
|
||||||
|
<form
|
||||||
|
onSubmit={handleProfileUpdate}
|
||||||
|
className="space-y-4 max-w-lg text-left"
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>First Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={settingsForm.first_name}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSettingsForm({
|
||||||
|
...settingsForm,
|
||||||
|
first_name: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
required
|
||||||
|
className={inputClass}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Last Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={settingsForm.last_name}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSettingsForm({
|
||||||
|
...settingsForm,
|
||||||
|
last_name: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
required
|
||||||
|
className={inputClass}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Email Address</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={profile?.email || ''}
|
||||||
|
disabled
|
||||||
|
className={`${inputClass} bg-[#f5f5f5] text-gray-500 cursor-not-allowed`}
|
||||||
|
/>
|
||||||
|
<span className="text-[11px] text-gray-400 block mt-1.5">
|
||||||
|
Email is verified via Google login and cannot be
|
||||||
|
changed.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PhoneInput
|
||||||
|
label="Phone Number"
|
||||||
|
value={settingsForm.phone}
|
||||||
|
onChange={(val) => setSettingsForm({ ...settingsForm, phone: val })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={actionLoading}
|
||||||
|
className="px-6 py-3 bg-primary hover:bg-primary-hover text-white font-semibold text-[13px] transition cursor-pointer disabled:opacity-60"
|
||||||
|
>
|
||||||
|
{actionLoading
|
||||||
|
? 'Saving Changes...'
|
||||||
|
: 'Save Profile Details'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{addressModalOpen && (
|
||||||
|
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-black/50 p-4 sm:p-6 overflow-y-auto">
|
||||||
|
<div
|
||||||
|
className="relative w-full max-w-md my-auto bg-white border border-[#e5e5e5] shadow-xl flex flex-col max-h-[min(640px,calc(100vh-2rem))]"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="address-modal-title"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between px-5 py-4 border-b border-[#e5e5e5] shrink-0 bg-white">
|
||||||
|
<h3
|
||||||
|
id="address-modal-title"
|
||||||
|
className="text-[16px] font-bold text-[#222]"
|
||||||
|
>
|
||||||
|
{editingAddress ? 'Edit Address' : 'New Address'}
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setAddressModalOpen(false)}
|
||||||
|
className="p-1 text-gray-400 hover:text-[#222] transition cursor-pointer"
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
onSubmit={handleAddressSubmit}
|
||||||
|
className="p-5 space-y-4 text-left overflow-y-auto min-h-0"
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Full Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={addressForm.full_name}
|
||||||
|
onChange={(e) =>
|
||||||
|
setAddressForm({
|
||||||
|
...addressForm,
|
||||||
|
full_name: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
required
|
||||||
|
placeholder="John Doe"
|
||||||
|
className={inputClass}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<PhoneInput
|
||||||
|
label="Phone"
|
||||||
|
required
|
||||||
|
value={addressForm.phone}
|
||||||
|
onChange={(val) => setAddressForm({ ...addressForm, phone: val })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Street Address</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={addressForm.street_address}
|
||||||
|
onChange={(e) =>
|
||||||
|
setAddressForm({
|
||||||
|
...addressForm,
|
||||||
|
street_address: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
required
|
||||||
|
placeholder="123 Technology Park Drive"
|
||||||
|
className={inputClass}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>City</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={addressForm.city}
|
||||||
|
onChange={(e) =>
|
||||||
|
setAddressForm({ ...addressForm, city: e.target.value })
|
||||||
|
}
|
||||||
|
required
|
||||||
|
placeholder="Chennai"
|
||||||
|
className={inputClass}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>State</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={addressForm.state}
|
||||||
|
onChange={(e) =>
|
||||||
|
setAddressForm({ ...addressForm, state: e.target.value })
|
||||||
|
}
|
||||||
|
required
|
||||||
|
placeholder="Tamil Nadu"
|
||||||
|
className={inputClass}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<PincodeInput
|
||||||
|
label="Pincode"
|
||||||
|
required
|
||||||
|
value={addressForm.pincode}
|
||||||
|
onChange={(val) => setAddressForm({ ...addressForm, pincode: val })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label className="flex items-center gap-2 pt-1 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={addressForm.is_default}
|
||||||
|
onChange={(e) =>
|
||||||
|
setAddressForm({
|
||||||
|
...addressForm,
|
||||||
|
is_default: e.target.checked,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="w-4 h-4 accent-[var(--color-primary,#1976F3)] border-[#e5e5e5]"
|
||||||
|
/>
|
||||||
|
<span className="text-[13px] text-[#444] font-medium">
|
||||||
|
Set as default shipping address
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className="flex gap-3 pt-4 border-t border-[#e5e5e5]">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setAddressModalOpen(false)}
|
||||||
|
disabled={actionLoading}
|
||||||
|
className="flex-1 py-2.5 border border-[#e5e5e5] text-[13px] font-semibold text-[#333] hover:bg-[#f7f9fc] transition cursor-pointer"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={actionLoading}
|
||||||
|
className="flex-1 py-2.5 bg-primary hover:bg-primary-hover text-white text-[13px] font-semibold transition disabled:opacity-50 cursor-pointer"
|
||||||
|
>
|
||||||
|
{actionLoading ? 'Saving...' : 'Save Address'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AccountPage() {
|
||||||
|
return (
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<div className="w-full min-h-screen bg-white flex items-center justify-center">
|
||||||
|
<Loader2 className="w-8 h-8 text-primary animate-spin" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<AccountDashboardPage />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
31
app/api/revalidate/route.ts
Normal file
31
app/api/revalidate/route.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { revalidateTag } from 'next/cache';
|
||||||
|
|
||||||
|
// Validate tag pattern matching allowlist: products, categories, layout, or product-<slug>
|
||||||
|
const ALLOWED_TAG_REGEX = /^(products|categories|layout|brands|product-[a-zA-Z0-9_-]+)$/;
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
const authHeader = req.headers.get('authorization');
|
||||||
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||||
|
return NextResponse.json({ message: 'Missing or malformed Authorization header' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = authHeader.substring(7);
|
||||||
|
if (token !== process.env.REVALIDATION_TOKEN) {
|
||||||
|
return NextResponse.json({ message: 'Unauthorized access token' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = await req.json();
|
||||||
|
const tag = body?.tag;
|
||||||
|
|
||||||
|
if (!tag || !ALLOWED_TAG_REGEX.test(tag)) {
|
||||||
|
return NextResponse.json({ message: 'Invalid or restricted revalidation tag' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
revalidateTag(tag);
|
||||||
|
return NextResponse.json({ revalidated: true, tag, timestamp: Date.now() });
|
||||||
|
} catch (e: any) {
|
||||||
|
return NextResponse.json({ message: 'Malformed JSON payload body or internal revalidation error' }, { status: 400 });
|
||||||
|
}
|
||||||
|
}
|
||||||
59
app/blog/[slug]/page.tsx
Normal file
59
app/blog/[slug]/page.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useParams } from 'next/navigation';
|
||||||
|
import { AnnouncementBar } from '@/layout/AnnouncementBar';
|
||||||
|
import { Header } from '@/layout/Header';
|
||||||
|
import { Navbar } from '@/layout/Navbar';
|
||||||
|
import { Footer } from '@/layout/Footer';
|
||||||
|
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
|
||||||
|
import { FloatingButtons } from '@/components/FloatingButtons';
|
||||||
|
import { BlogArticleView } from '@/components/blog/BlogViews';
|
||||||
|
import { DEMO_BLOG_ARTICLES, findBlogArticle } from '@/lib/blogDefaults';
|
||||||
|
|
||||||
|
export default function BlogArticlePage() {
|
||||||
|
const params = useParams<{ slug: string }>();
|
||||||
|
const slug = String(params?.slug || '');
|
||||||
|
const article = useMemo(
|
||||||
|
() => findBlogArticle(DEMO_BLOG_ARTICLES, slug),
|
||||||
|
[slug]
|
||||||
|
);
|
||||||
|
const related = useMemo(() => {
|
||||||
|
if (!article) return [];
|
||||||
|
return DEMO_BLOG_ARTICLES.filter(
|
||||||
|
(a) => a.slug !== article.slug && a.category === article.category
|
||||||
|
).slice(0, 2);
|
||||||
|
}, [article]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[#F8F9FB] text-[#1E293B] flex flex-col font-sans">
|
||||||
|
<AnnouncementBar />
|
||||||
|
<Header />
|
||||||
|
<StickyHeaderSpacer />
|
||||||
|
<Navbar />
|
||||||
|
<main className="flex-grow py-10 pb-20">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1400px]">
|
||||||
|
{article ? (
|
||||||
|
<BlogArticleView article={article} related={related} />
|
||||||
|
) : (
|
||||||
|
<div className="bg-white border border-gray-200 rounded-2xl p-10 text-center max-w-lg mx-auto">
|
||||||
|
<h1 className="text-lg font-bold text-[#1E293B]">Article not found</h1>
|
||||||
|
<p className="text-[13px] text-gray-500 mt-2 mb-6">
|
||||||
|
This story is missing or no longer published.
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href="/blog"
|
||||||
|
className="inline-flex items-center justify-center h-10 px-5 rounded-lg bg-primary text-white text-[13px] font-semibold"
|
||||||
|
>
|
||||||
|
Back to News & Blog
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
<FloatingButtons />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
app/blog/layout.tsx
Normal file
12
app/blog/layout.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'News & Blog | iFixKart',
|
||||||
|
description:
|
||||||
|
'Tech news, refurbished buying guides, and phone repair tips from iFixKart.',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function BlogLayout({ children }: { children: ReactNode }) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
54
app/blog/page.tsx
Normal file
54
app/blog/page.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { AnnouncementBar } from '@/layout/AnnouncementBar';
|
||||||
|
import { Header } from '@/layout/Header';
|
||||||
|
import { Navbar } from '@/layout/Navbar';
|
||||||
|
import { Footer } from '@/layout/Footer';
|
||||||
|
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
|
||||||
|
import { FloatingButtons } from '@/components/FloatingButtons';
|
||||||
|
import { BlogListing } from '@/components/blog/BlogViews';
|
||||||
|
import { DEMO_BLOG_ARTICLES, BlogArticle, BlogCategory } from '@/lib/blogDefaults';
|
||||||
|
import { storefrontService } from '@/services/api/storefrontService';
|
||||||
|
|
||||||
|
export default function BlogIndexPage() {
|
||||||
|
const [articles, setArticles] = useState(DEMO_BLOG_ARTICLES);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
storefrontService.getBlogPosts().then((posts) => {
|
||||||
|
if (!cancelled && posts && posts.length > 0) {
|
||||||
|
const mapped: BlogArticle[] = posts.map((p: any) => ({
|
||||||
|
slug: (p.title || '').toLowerCase().replace(/[^\w\s-]/g, '').replace(/[\s_-]+/g, '-'),
|
||||||
|
title: p.title || '',
|
||||||
|
excerpt: p.excerpt || '',
|
||||||
|
author: typeof p.author === 'string' ? p.author : (p.author?.name || 'iFixKart Team'),
|
||||||
|
date: p.date || '10 Sep 2026',
|
||||||
|
readTime: '5 min read',
|
||||||
|
category: (['News', 'Reviews', 'Guides', 'Repair'].includes(p.category) ? p.category : 'Guides') as BlogCategory,
|
||||||
|
image: p.image_url || 'https://images.unsplash.com/photo-1591799264318-7e6ef8ddb7ea?w=800&q=80',
|
||||||
|
body: [p.content || p.excerpt || ''],
|
||||||
|
}));
|
||||||
|
setArticles(mapped);
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
return () => { cancelled = true; };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[#F8F9FB] text-[#1E293B] flex flex-col font-sans">
|
||||||
|
<AnnouncementBar />
|
||||||
|
<Header />
|
||||||
|
<StickyHeaderSpacer />
|
||||||
|
<Navbar />
|
||||||
|
<main className="flex-grow py-10 pb-20">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1400px]">
|
||||||
|
<BlogListing articles={articles} />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
<FloatingButtons />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
5
app/cart/loading.tsx
Normal file
5
app/cart/loading.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { PageSkeleton } from '@/components/ui/Skeleton';
|
||||||
|
|
||||||
|
export default function Loading() {
|
||||||
|
return <PageSkeleton variant="cart" />;
|
||||||
|
}
|
||||||
300
app/cart/page.tsx
Normal file
300
app/cart/page.tsx
Normal file
|
|
@ -0,0 +1,300 @@
|
||||||
|
/**
|
||||||
|
* @page Full Shopping Cart (`app/cart/page.tsx`)
|
||||||
|
* @purpose Customer full cart page with quantity modification, line totals,
|
||||||
|
* free-shipping progress, promo input, and checkout CTA.
|
||||||
|
*/
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useCartStore } from '@/store/cartStore';
|
||||||
|
import { formatCurrency, getImageUrl } from '@/lib/utils';
|
||||||
|
import { requireLoginForCheckout } from '@/lib/requireLoginForCheckout';
|
||||||
|
import { Header } from '@/layout/Header';
|
||||||
|
import { Footer } from '@/layout/Footer';
|
||||||
|
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
|
||||||
|
import BlurHashImage from '@/components/ui/BlurHashImage';
|
||||||
|
import {
|
||||||
|
Trash2,
|
||||||
|
Plus,
|
||||||
|
Minus,
|
||||||
|
ArrowRight,
|
||||||
|
ShoppingBag,
|
||||||
|
Truck,
|
||||||
|
Tag,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
const FREE_SHIPPING_THRESHOLD = 999;
|
||||||
|
|
||||||
|
function resolveItemImage(item: {
|
||||||
|
product: { images?: { image_url?: string }[]; name?: string };
|
||||||
|
selectedVariant: { images?: { image_url?: string }[] };
|
||||||
|
}): string {
|
||||||
|
const fromVariant = item.selectedVariant?.images?.[0]?.image_url;
|
||||||
|
const fromProduct = item.product?.images?.[0]?.image_url;
|
||||||
|
return getImageUrl(fromVariant || fromProduct || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CartPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const cart = useCartStore((state) => state.cart);
|
||||||
|
const removeFromCart = useCartStore((state) => state.removeFromCart);
|
||||||
|
const updateQuantity = useCartStore((state) => state.updateQuantity);
|
||||||
|
const getCartTotal = useCartStore((state) => state.getCartTotal);
|
||||||
|
|
||||||
|
const subtotal = getCartTotal();
|
||||||
|
const shipping = subtotal > 0 && subtotal < FREE_SHIPPING_THRESHOLD ? 15.0 : 0;
|
||||||
|
const total = subtotal + shipping;
|
||||||
|
const progressPercent = Math.min(100, (subtotal / FREE_SHIPPING_THRESHOLD) * 100);
|
||||||
|
const amountNeeded = Math.max(0, FREE_SHIPPING_THRESHOLD - subtotal);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full min-h-screen bg-[#F8F9FB] flex flex-col">
|
||||||
|
<Header />
|
||||||
|
<StickyHeaderSpacer />
|
||||||
|
|
||||||
|
<main className="flex-grow py-8 md:py-10">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
{cart.length > 0 && (
|
||||||
|
<div className="flex justify-end mb-6">
|
||||||
|
<Link
|
||||||
|
href="/shop"
|
||||||
|
className="text-[13px] font-semibold text-primary hover:underline"
|
||||||
|
>
|
||||||
|
Continue shopping
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{cart.length === 0 ? (
|
||||||
|
<div className="bg-white border border-gray-200 rounded-xl p-10 md:p-14 text-center max-w-lg mx-auto shadow-sm">
|
||||||
|
<div className="w-16 h-16 rounded-full bg-primary-light text-primary flex items-center justify-center mx-auto mb-4">
|
||||||
|
<ShoppingBag className="w-7 h-7" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-lg font-bold text-[#1a1a1a]">
|
||||||
|
Your cart is empty
|
||||||
|
</h2>
|
||||||
|
<p className="text-[13px] text-gray-500 mt-1.5 mb-6">
|
||||||
|
Looks like you haven't added any products yet.
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href="/shop"
|
||||||
|
className="inline-flex items-center gap-1.5 bg-primary hover:brightness-95 text-white text-[13px] font-bold px-6 py-3 rounded-lg transition"
|
||||||
|
>
|
||||||
|
Explore products <ArrowRight className="w-4 h-4" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 lg:gap-8 items-start">
|
||||||
|
{/* Items */}
|
||||||
|
<div className="lg:col-span-2 bg-white border border-gray-200 rounded-xl overflow-hidden shadow-sm">
|
||||||
|
<div className="px-4 md:px-5 py-3.5 border-b border-gray-100 bg-gray-50/80 flex items-center justify-between text-[12px] font-bold text-gray-500 uppercase tracking-wide">
|
||||||
|
<span>Product</span>
|
||||||
|
<span className="hidden sm:inline">Subtotal</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="divide-y divide-gray-100">
|
||||||
|
{cart.map((item) => {
|
||||||
|
const img = resolveItemImage(item);
|
||||||
|
const priceNum = typeof item.selectedVariant.price === 'number'
|
||||||
|
? item.selectedVariant.price
|
||||||
|
: parseFloat(String(item.selectedVariant.price || 0));
|
||||||
|
const lineTotal = priceNum * item.quantity;
|
||||||
|
const atMax =
|
||||||
|
typeof item.selectedVariant.available_stock ===
|
||||||
|
'number' &&
|
||||||
|
item.quantity >= item.selectedVariant.available_stock;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
className="p-4 md:p-5 flex items-start sm:items-center gap-3 md:gap-4"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href={`/products/${item.product.slug}`}
|
||||||
|
className="relative w-20 h-20 rounded-lg border border-gray-100 overflow-hidden bg-gray-50 shrink-0"
|
||||||
|
>
|
||||||
|
{img ? (
|
||||||
|
<BlurHashImage
|
||||||
|
src={img}
|
||||||
|
blurHash={(item.product as any).blur_hash || null}
|
||||||
|
alt={item.product.name}
|
||||||
|
fill
|
||||||
|
sizes="80px"
|
||||||
|
className="object-contain p-1"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-full h-full flex items-center justify-center">
|
||||||
|
<ShoppingBag className="w-5 h-5 text-gray-300" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<Link
|
||||||
|
href={`/products/${item.product.slug}`}
|
||||||
|
className="text-[13px] font-semibold text-[#1a1a1a] hover:text-primary line-clamp-2 transition-colors"
|
||||||
|
>
|
||||||
|
{item.product.name}
|
||||||
|
</Link>
|
||||||
|
<span className="text-[11px] text-gray-400 block mt-0.5">
|
||||||
|
SKU: {item.selectedVariant.sku}
|
||||||
|
</span>
|
||||||
|
<span className="text-[13px] font-bold text-primary mt-1 block">
|
||||||
|
{formatCurrency(item.selectedVariant.price)}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 mt-2.5">
|
||||||
|
<div className="inline-flex items-center border border-gray-200 rounded-lg overflow-hidden bg-white">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
updateQuantity(item.id, item.quantity - 1)
|
||||||
|
}
|
||||||
|
className="p-2 hover:bg-gray-50 text-gray-600 cursor-pointer"
|
||||||
|
aria-label="Decrease quantity"
|
||||||
|
>
|
||||||
|
<Minus className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
<span className="px-3 text-[13px] font-bold text-[#1a1a1a] min-w-[2rem] text-center">
|
||||||
|
{item.quantity}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
updateQuantity(item.id, item.quantity + 1)
|
||||||
|
}
|
||||||
|
disabled={atMax}
|
||||||
|
className="p-2 hover:bg-gray-50 text-gray-600 disabled:opacity-30 cursor-pointer"
|
||||||
|
aria-label="Increase quantity"
|
||||||
|
>
|
||||||
|
<Plus className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeFromCart(item.id)}
|
||||||
|
className="text-gray-400 hover:text-red-500 p-1.5 rounded-md hover:bg-red-50 transition cursor-pointer"
|
||||||
|
aria-label="Remove item"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-[13px] font-bold text-[#1a1a1a] shrink-0 pt-1 sm:pt-0">
|
||||||
|
{formatCurrency(lineTotal)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Summary */}
|
||||||
|
<div className="lg:sticky lg:top-28 space-y-4">
|
||||||
|
<div className="bg-white border border-gray-200 rounded-xl p-5 md:p-6 shadow-sm space-y-4 text-[13px]">
|
||||||
|
<h3 className="text-[14px] font-bold text-[#1a1a1a] border-b border-gray-100 pb-3">
|
||||||
|
Order summary
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Free shipping progress */}
|
||||||
|
<div className="rounded-lg bg-primary/5 border border-primary/10 p-3 space-y-2">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<Truck className="w-4 h-4 text-primary shrink-0 mt-0.5" />
|
||||||
|
<p className="text-[12px] text-gray-600 leading-snug">
|
||||||
|
{shipping === 0 ? (
|
||||||
|
<span className="font-semibold text-emerald-600">
|
||||||
|
You've unlocked free shipping!
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
Add{' '}
|
||||||
|
<span className="font-bold text-primary">
|
||||||
|
{formatCurrency(amountNeeded)}
|
||||||
|
</span>{' '}
|
||||||
|
more for free shipping
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="h-1.5 bg-white rounded-full overflow-hidden border border-primary/10">
|
||||||
|
<div
|
||||||
|
className="h-full bg-primary rounded-full transition-all duration-300"
|
||||||
|
style={{ width: `${progressPercent}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2.5 text-gray-500">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>Subtotal</span>
|
||||||
|
<span className="font-semibold text-[#1a1a1a]">
|
||||||
|
{formatCurrency(subtotal)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span>Shipping</span>
|
||||||
|
<span>
|
||||||
|
{shipping === 0 ? (
|
||||||
|
<strong className="text-emerald-600 text-[11px] font-bold uppercase">
|
||||||
|
Free
|
||||||
|
</strong>
|
||||||
|
) : (
|
||||||
|
<span className="font-semibold text-[#1a1a1a]">
|
||||||
|
{formatCurrency(shipping)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 pt-1 border-t border-gray-100">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Tag className="w-3.5 h-3.5 text-gray-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Promo code"
|
||||||
|
className="w-full bg-gray-50 border border-gray-200 rounded-lg pl-9 pr-3 py-2.5 outline-none text-[13px] focus:border-primary focus:ring-2 focus:ring-primary/15"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="bg-primary hover:brightness-95 text-white font-bold px-4 py-2.5 rounded-lg text-[13px] transition cursor-pointer"
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-gray-200 pt-3 flex justify-between items-center">
|
||||||
|
<span className="font-bold text-[#1a1a1a]">Total</span>
|
||||||
|
<span className="text-lg font-bold text-primary">
|
||||||
|
{formatCurrency(total)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
if (!requireLoginForCheckout('/checkout')) return;
|
||||||
|
router.push('/checkout');
|
||||||
|
}}
|
||||||
|
className="w-full bg-primary hover:brightness-95 text-white font-bold py-3.5 rounded-lg text-center transition flex items-center justify-center gap-1.5 text-[14px] cursor-pointer"
|
||||||
|
>
|
||||||
|
Proceed to checkout <ArrowRight className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<p className="text-[11px] text-gray-400 text-center">
|
||||||
|
Taxes calculated at checkout
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
app/categories/loading.tsx
Normal file
5
app/categories/loading.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { PageSkeleton } from '@/components/ui/Skeleton';
|
||||||
|
|
||||||
|
export default function Loading() {
|
||||||
|
return <PageSkeleton variant="simple" />;
|
||||||
|
}
|
||||||
118
app/categories/page.tsx
Normal file
118
app/categories/page.tsx
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { Layers } from 'lucide-react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { AnnouncementBar } from '@/layout/AnnouncementBar';
|
||||||
|
import { Header } from '@/layout/Header';
|
||||||
|
import { Navbar } from '@/layout/Navbar';
|
||||||
|
import { Footer } from '@/layout/Footer';
|
||||||
|
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
|
||||||
|
import { catalogService, CategoryResponse } from '@/services/api/catalogService';
|
||||||
|
import { getImageUrl } from '@/lib/utils';
|
||||||
|
|
||||||
|
export default function AllCategoriesPage() {
|
||||||
|
const [categories, setCategories] = useState<CategoryResponse[]>([]);
|
||||||
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
|
const [error, setError] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(true);
|
||||||
|
catalogService.getCategories()
|
||||||
|
.then((res) => {
|
||||||
|
setLoading(false);
|
||||||
|
if (res && res.length > 0) {
|
||||||
|
const sorted = [...res].sort((a, b) => {
|
||||||
|
const orderA = parseInt(a.sort_order || '0', 10);
|
||||||
|
const orderB = parseInt(b.sort_order || '0', 10);
|
||||||
|
return orderA - orderB;
|
||||||
|
});
|
||||||
|
setCategories(sorted);
|
||||||
|
} else {
|
||||||
|
setCategories([]);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Failed to load categories:", err);
|
||||||
|
setLoading(false);
|
||||||
|
setError(true);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[#F8F9FB] text-[#1E293B] flex flex-col font-sans">
|
||||||
|
<AnnouncementBar />
|
||||||
|
<Header />
|
||||||
|
<StickyHeaderSpacer />
|
||||||
|
<Navbar />
|
||||||
|
|
||||||
|
<main className="flex-grow py-8 pb-24 text-gray-800 antialiased">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
{loading ? (
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-6">
|
||||||
|
{[1, 2, 3, 4, 5, 6, 7, 8].map((i) => (
|
||||||
|
<div key={i} className="flex flex-col items-center animate-pulse">
|
||||||
|
<div className="w-full aspect-square bg-gray-200 rounded-theme mb-3" />
|
||||||
|
<div className="h-4 bg-gray-200 rounded w-2/3 mb-1" />
|
||||||
|
<div className="h-3 bg-gray-200 rounded w-1/3" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : error || categories.length === 0 ? (
|
||||||
|
<div className="bg-white border border-gray-250 rounded-2xl p-12 text-center max-w-lg mx-auto shadow-xs">
|
||||||
|
<Layers className="w-16 h-16 text-gray-300 mx-auto mb-4 stroke-1" />
|
||||||
|
<h2 className="text-lg font-bold text-gray-800">No Categories Available</h2>
|
||||||
|
<p className="text-xs text-gray-400 mt-1 mb-6">We could not retrieve catalog categories at this moment.</p>
|
||||||
|
<Link href="/" className="bg-[#1877f2] text-white text-xs font-bold px-6 py-3 rounded-lg hover:bg-[#1565c0] transition-colors inline-block">
|
||||||
|
Go to Homepage
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-6">
|
||||||
|
{categories.map((cat: any) => {
|
||||||
|
const catImage = getImageUrl(cat.image_url || cat.image || cat.icon_url);
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={cat.category_id || cat.slug}
|
||||||
|
whileHover={{ y: -6 }}
|
||||||
|
transition={{ type: 'spring', stiffness: 300, damping: 25 }}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href={`/products?category=${cat.slug}`}
|
||||||
|
className="flex flex-col items-center group text-center"
|
||||||
|
>
|
||||||
|
{/* Image Box */}
|
||||||
|
<div className="w-full aspect-square bg-white rounded-theme flex items-center justify-center p-6 border border-gray-200 group-hover:border-blue-200 group-hover:bg-blue-50/10 transition-all duration-300 relative overflow-hidden shadow-xs">
|
||||||
|
{catImage ? (
|
||||||
|
<motion.img
|
||||||
|
whileHover={{ scale: 1.06 }}
|
||||||
|
src={catImage}
|
||||||
|
alt={cat.name}
|
||||||
|
className="max-h-full max-w-full object-contain rounded-lg drop-shadow-md"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Layers className="w-12 h-12 text-gray-400 group-hover:text-primary transition-colors stroke-[1.5]" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Title & Count */}
|
||||||
|
<h3 className="text-[13px] font-bold text-gray-800 mt-3.5 group-hover:text-primary transition-colors line-clamp-1">
|
||||||
|
{cat.name}
|
||||||
|
</h3>
|
||||||
|
<span className="text-[10px] font-bold text-gray-400 mt-0.5 tracking-wide">
|
||||||
|
{cat.product_count !== undefined ? `${cat.product_count} Products` : 'Catalog Item'}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
app/checkout/loading.tsx
Normal file
5
app/checkout/loading.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { PageSkeleton } from '@/components/ui/Skeleton';
|
||||||
|
|
||||||
|
export default function Loading() {
|
||||||
|
return <PageSkeleton variant="cart" />;
|
||||||
|
}
|
||||||
1336
app/checkout/page.tsx
Normal file
1336
app/checkout/page.tsx
Normal file
File diff suppressed because it is too large
Load diff
5
app/compare/loading.tsx
Normal file
5
app/compare/loading.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { PageSkeleton } from '@/components/ui/Skeleton';
|
||||||
|
|
||||||
|
export default function Loading() {
|
||||||
|
return <PageSkeleton variant="simple" />;
|
||||||
|
}
|
||||||
244
app/compare/page.tsx
Normal file
244
app/compare/page.tsx
Normal file
|
|
@ -0,0 +1,244 @@
|
||||||
|
/**
|
||||||
|
* @page Compare Page (`app/compare/page.tsx`)
|
||||||
|
* @purpose Product specification comparison matrix comparing up to 4 selected products.
|
||||||
|
*/
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { useCartStore } from '@/store/cartStore';
|
||||||
|
import { catalogService, ProductResponse } from '@/services/api/catalogService';
|
||||||
|
import { mapProductCardToProductResponse } from '@/utils/productMapper';
|
||||||
|
import { formatCatalogPriceLabel, getImageUrl } from '@/lib/utils';
|
||||||
|
import { ArrowLeftRight, Trash2 } from 'lucide-react';
|
||||||
|
import { AnnouncementBar } from '@/layout/AnnouncementBar';
|
||||||
|
import { Header } from '@/layout/Header';
|
||||||
|
import { Navbar } from '@/layout/Navbar';
|
||||||
|
import { Footer } from '@/layout/Footer';
|
||||||
|
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
|
||||||
|
import { FloatingButtons } from '@/components/FloatingButtons';
|
||||||
|
import { CompareProductPicker } from '@/components/compare/CompareProductPicker';
|
||||||
|
import { COMPARE_LIMIT } from '@/lib/compare';
|
||||||
|
import {
|
||||||
|
buildTypeCompareRows,
|
||||||
|
sameTypeCompareList,
|
||||||
|
type ProductSpecItem,
|
||||||
|
} from '@/lib/compareSpecs';
|
||||||
|
|
||||||
|
export default function ComparePage() {
|
||||||
|
const compareList = useCartStore((state) => state.compareList) || [];
|
||||||
|
const toggleCompare = useCartStore((state) => state.toggleCompare);
|
||||||
|
const addToCart = useCartStore((state) => state.addToCart);
|
||||||
|
const hasHydrated = useCartStore((state) => state._hasHydrated);
|
||||||
|
const [ready, setReady] = useState(false);
|
||||||
|
const [enriched, setEnriched] = useState<Record<string, ProductResponse>>({});
|
||||||
|
const [specMap, setSpecMap] = useState<Record<string, ProductSpecItem[]>>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasHydrated) {
|
||||||
|
const list = useCartStore.getState().compareList;
|
||||||
|
const pruned = sameTypeCompareList(list);
|
||||||
|
if (pruned.length !== list.length) {
|
||||||
|
useCartStore.setState({ compareList: pruned });
|
||||||
|
}
|
||||||
|
setReady(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const t = window.setTimeout(() => setReady(true), 250);
|
||||||
|
return () => window.clearTimeout(t);
|
||||||
|
}, [hasHydrated]);
|
||||||
|
|
||||||
|
const productKey = compareList.map((p) => p.product_id).join('|');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
if (compareList.length === 0) {
|
||||||
|
setEnriched({});
|
||||||
|
setSpecMap({});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise.all(
|
||||||
|
compareList.map(async (product) => {
|
||||||
|
if (!product.slug) return null;
|
||||||
|
try {
|
||||||
|
const data = await catalogService.getProductDetail(product.slug);
|
||||||
|
const raw = data?.product || data;
|
||||||
|
const full = mapProductCardToProductResponse(raw);
|
||||||
|
const specs = Array.isArray(data?.specifications)
|
||||||
|
? data.specifications
|
||||||
|
.map((item: { label?: string; value?: string }) => ({
|
||||||
|
label: String(item?.label || '').trim(),
|
||||||
|
value: String(item?.value || '').trim(),
|
||||||
|
}))
|
||||||
|
.filter((item: ProductSpecItem) => item.label && item.value)
|
||||||
|
: [];
|
||||||
|
return {
|
||||||
|
id: product.product_id,
|
||||||
|
product: {
|
||||||
|
...product,
|
||||||
|
...full,
|
||||||
|
product_id: product.product_id,
|
||||||
|
slug: product.slug || full.slug,
|
||||||
|
brand_name: full.brand_name || product.brand_name,
|
||||||
|
images: full.images?.length ? full.images : product.images,
|
||||||
|
variants: full.variants?.length ? full.variants : product.variants,
|
||||||
|
} as ProductResponse,
|
||||||
|
specs,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).then((rows) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
const nextProducts: Record<string, ProductResponse> = {};
|
||||||
|
const nextSpecs: Record<string, ProductSpecItem[]> = {};
|
||||||
|
rows.forEach((row) => {
|
||||||
|
if (!row) return;
|
||||||
|
nextProducts[row.id] = row.product;
|
||||||
|
nextSpecs[row.id] = row.specs;
|
||||||
|
});
|
||||||
|
setEnriched(nextProducts);
|
||||||
|
setSpecMap(nextSpecs);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [productKey]);
|
||||||
|
|
||||||
|
const products = useMemo(
|
||||||
|
() => compareList.map((p) => enriched[p.product_id] || p),
|
||||||
|
[compareList, enriched]
|
||||||
|
);
|
||||||
|
const specRows = useMemo(
|
||||||
|
() => buildTypeCompareRows(products, specMap),
|
||||||
|
[products, specMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-white text-[#1E293B] flex flex-col font-sans">
|
||||||
|
<AnnouncementBar />
|
||||||
|
<Header />
|
||||||
|
<StickyHeaderSpacer />
|
||||||
|
<Navbar />
|
||||||
|
|
||||||
|
<main className="flex-grow w-full bg-[#f8f9fb] py-8">
|
||||||
|
<div className="max-w-7xl mx-auto px-4">
|
||||||
|
{ready && compareList.length < COMPARE_LIMIT ? (
|
||||||
|
<div className="flex justify-end mb-4">
|
||||||
|
<CompareProductPicker variant="button" />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{!ready ? (
|
||||||
|
<div className="bg-white border border-gray-200 rounded-xl p-8 animate-pulse h-48" />
|
||||||
|
) : compareList.length === 0 ? (
|
||||||
|
<div className="bg-white border border-gray-200 rounded-xl p-12 text-center max-w-lg mx-auto">
|
||||||
|
<ArrowLeftRight className="w-16 h-16 text-gray-300 mx-auto mb-4 stroke-1" />
|
||||||
|
<h2 className="text-lg font-bold text-gray-900">No Products Selected for Comparison</h2>
|
||||||
|
<p className="text-xs text-gray-400 mt-1 mb-6">
|
||||||
|
Click the compare icon on product cards to compare specifications side-by-side.
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href="/shop"
|
||||||
|
className="bg-primary text-white text-xs font-bold px-6 py-3 rounded-lg hover:brightness-95 transition-colors inline-block"
|
||||||
|
>
|
||||||
|
Browse Catalog
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="bg-white border border-gray-200 rounded-xl overflow-x-auto">
|
||||||
|
<table className="w-full text-left text-xs">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-gray-50 border-b border-gray-200 text-gray-900">
|
||||||
|
<th className="p-4 w-44 font-extrabold">Feature</th>
|
||||||
|
{products.map((product) => {
|
||||||
|
const img = getImageUrl(product.images?.[0]?.image_url);
|
||||||
|
return (
|
||||||
|
<th key={product.product_id} className="p-4 min-w-[200px]">
|
||||||
|
<div className="flex flex-col items-center text-center">
|
||||||
|
<div className="relative w-20 h-20 rounded-lg border border-gray-200 overflow-hidden bg-gray-50 mb-2">
|
||||||
|
{img ? (
|
||||||
|
<Image src={img} alt={product.name} fill className="object-contain p-1" unoptimized />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
href={`/products/${product.slug}`}
|
||||||
|
className="font-extrabold text-gray-900 line-clamp-2 hover:text-primary"
|
||||||
|
>
|
||||||
|
{product.name}
|
||||||
|
</Link>
|
||||||
|
<span className="text-xs font-bold text-gray-900 mt-1">
|
||||||
|
{formatCatalogPriceLabel(product)}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleCompare(product)}
|
||||||
|
className="text-[10px] text-red-500 font-bold hover:underline mt-2 flex items-center gap-1 cursor-pointer"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3 h-3" /> Remove
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-100 text-gray-800">
|
||||||
|
<tr>
|
||||||
|
<td className="p-4 font-bold bg-gray-50">Price</td>
|
||||||
|
{products.map((p) => (
|
||||||
|
<td key={p.product_id} className="p-4 text-center font-extrabold">
|
||||||
|
{formatCatalogPriceLabel(p)}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
{specRows.map((row) => (
|
||||||
|
<tr key={row.label}>
|
||||||
|
<td className="p-4 font-bold bg-gray-50">{row.label}</td>
|
||||||
|
{row.values.map((value, index) => (
|
||||||
|
<td key={`${products[index]?.product_id}-${row.label}`} className="p-4 text-center text-gray-700">
|
||||||
|
{value.text}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
<tr>
|
||||||
|
<td className="p-4 font-bold bg-gray-50">Rating</td>
|
||||||
|
{products.map((p) => (
|
||||||
|
<td key={p.product_id} className="p-4 text-center font-bold text-amber-500">
|
||||||
|
★ {p.rating || 4.9} / 5.0
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="p-4 font-bold bg-gray-50">Action</td>
|
||||||
|
{products.map((p) => (
|
||||||
|
<td key={p.product_id} className="p-4 text-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
const variant = p.variants?.[0];
|
||||||
|
if (variant) void addToCart(p, variant, 1);
|
||||||
|
}}
|
||||||
|
className="bg-primary hover:brightness-95 text-white text-[11px] font-bold px-3 py-1.5 rounded-md transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
Add to Cart
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
<FloatingButtons />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
app/contact/layout.tsx
Normal file
12
app/contact/layout.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Contact Us | iFixKart',
|
||||||
|
description:
|
||||||
|
'Call, email, or visit iFixKart. Find our support phone, email, and service center address.',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ContactLayout({ children }: { children: ReactNode }) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
152
app/contact/page.tsx
Normal file
152
app/contact/page.tsx
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { Mail, MapPin, Phone } from 'lucide-react';
|
||||||
|
import { AnnouncementBar } from '@/layout/AnnouncementBar';
|
||||||
|
import { Header } from '@/layout/Header';
|
||||||
|
import { Navbar } from '@/layout/Navbar';
|
||||||
|
import { Footer } from '@/layout/Footer';
|
||||||
|
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
|
||||||
|
import { FloatingButtons } from '@/components/FloatingButtons';
|
||||||
|
import {
|
||||||
|
storefrontService,
|
||||||
|
type FooterInfo,
|
||||||
|
type StorefrontSettings,
|
||||||
|
MOCK_FOOTER_INFO,
|
||||||
|
} from '@/services/api/storefrontService';
|
||||||
|
|
||||||
|
function telHref(phone: string): string {
|
||||||
|
return `tel:${phone.replace(/[^\d+]/g, '')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapsHref(address: string): string {
|
||||||
|
return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(address)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ContactPage() {
|
||||||
|
const [footer, setFooter] = useState<FooterInfo>(MOCK_FOOTER_INFO);
|
||||||
|
const [settings, setSettings] = useState<StorefrontSettings>({ store_name: 'iFixKart' });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
Promise.all([
|
||||||
|
storefrontService.getFooterInfo(),
|
||||||
|
storefrontService.getSettings(),
|
||||||
|
]).then(([info, store]) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
if (info) setFooter(info);
|
||||||
|
if (store) setSettings(store);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const storeName = settings.store_name || 'iFixKart';
|
||||||
|
const phone = footer.phone || settings.support_phone || '';
|
||||||
|
const email = footer.email || '';
|
||||||
|
const address = footer.address || '';
|
||||||
|
const socials = (footer.social_links || []).filter((s) => s?.url);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[#F8F9FB] text-[#1E293B] flex flex-col font-sans">
|
||||||
|
<AnnouncementBar />
|
||||||
|
<Header />
|
||||||
|
<StickyHeaderSpacer />
|
||||||
|
<Navbar />
|
||||||
|
<main className="flex-grow py-10 pb-20">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[900px]">
|
||||||
|
<h1 className="text-[28px] md:text-[32px] font-bold text-[#1E293B]">
|
||||||
|
Contact Us
|
||||||
|
</h1>
|
||||||
|
<p className="text-[14px] text-gray-500 mt-2 max-w-xl">
|
||||||
|
Reach {storeName} for orders, repairs, and product support. We typically respond within one business day.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-8">
|
||||||
|
{phone ? (
|
||||||
|
<a
|
||||||
|
href={telHref(phone)}
|
||||||
|
className="bg-white border border-gray-200 rounded-xl p-5 hover:border-primary/40 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-primary/10 text-primary flex items-center justify-center mb-3">
|
||||||
|
<Phone className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-[12px] font-bold uppercase tracking-wide text-gray-400">
|
||||||
|
Phone
|
||||||
|
</h2>
|
||||||
|
<p className="text-[16px] font-bold text-[#1E293B] mt-1">{phone}</p>
|
||||||
|
<p className="text-[12px] text-gray-500 mt-1">Call customer support</p>
|
||||||
|
</a>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{email ? (
|
||||||
|
<a
|
||||||
|
href={`mailto:${email}`}
|
||||||
|
className="bg-white border border-gray-200 rounded-xl p-5 hover:border-primary/40 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-primary/10 text-primary flex items-center justify-center mb-3">
|
||||||
|
<Mail className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-[12px] font-bold uppercase tracking-wide text-gray-400">
|
||||||
|
Email
|
||||||
|
</h2>
|
||||||
|
<p className="text-[16px] font-bold text-[#1E293B] mt-1 break-all">{email}</p>
|
||||||
|
<p className="text-[12px] text-gray-500 mt-1">Write to our support team</p>
|
||||||
|
</a>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{address ? (
|
||||||
|
<a
|
||||||
|
href={mapsHref(address)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="bg-white border border-gray-200 rounded-xl p-5 hover:border-primary/40 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-primary/10 text-primary flex items-center justify-center mb-3">
|
||||||
|
<MapPin className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-[12px] font-bold uppercase tracking-wide text-gray-400">
|
||||||
|
Address
|
||||||
|
</h2>
|
||||||
|
<p className="text-[14px] font-semibold text-[#1E293B] mt-1 leading-relaxed">
|
||||||
|
{address}
|
||||||
|
</p>
|
||||||
|
<p className="text-[12px] text-primary font-semibold mt-2">View on map →</p>
|
||||||
|
</a>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{socials.length > 0 ? (
|
||||||
|
<div className="mt-8 bg-white border border-gray-200 rounded-xl p-5">
|
||||||
|
<h2 className="text-[13px] font-bold text-[#1E293B]">Follow us</h2>
|
||||||
|
<div className="flex flex-wrap gap-2 mt-3">
|
||||||
|
{socials.map((s, i) => (
|
||||||
|
<a
|
||||||
|
key={`${s.platform}-${i}`}
|
||||||
|
href={s.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center h-9 px-3 rounded-lg border border-gray-200 text-[13px] font-semibold text-gray-700 hover:border-primary hover:text-primary"
|
||||||
|
>
|
||||||
|
{s.platform}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<p className="text-[13px] text-gray-500 mt-8">
|
||||||
|
Looking for quick answers?{' '}
|
||||||
|
<Link href="/faqs" className="text-primary font-semibold hover:underline">
|
||||||
|
Browse FAQs
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
<FloatingButtons />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
app/error.tsx
Normal file
36
app/error.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { AlertTriangle, RefreshCw } from 'lucide-react';
|
||||||
|
|
||||||
|
interface ErrorProps {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Error({ error, reset }: ErrorProps) {
|
||||||
|
useEffect(() => {
|
||||||
|
console.error('Storefront error boundary caught:', error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full min-h-screen bg-[#F8F9FB] flex flex-col items-center justify-center p-6 font-sans">
|
||||||
|
<div className="max-w-md w-full bg-white border border-gray-250 rounded-2xl p-8 text-center shadow-soft">
|
||||||
|
<div className="w-12 h-12 bg-red-50 text-red-500 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<AlertTriangle size={24} className="stroke-[2.5]" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-lg font-extrabold text-gray-900 mb-2 font-outfit">Something Went Wrong</h2>
|
||||||
|
<p className="text-xs text-gray-500 font-medium mb-6">
|
||||||
|
{error.message || 'We ran into an unexpected error loading this page. Please try refreshing.'}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => reset()}
|
||||||
|
className="inline-flex items-center justify-center gap-2 bg-[#0073EC] hover:bg-[#1565C0] text-white font-bold text-xs uppercase tracking-wider py-3.5 px-6 rounded-xl transition-all shadow-md active:scale-[0.99] cursor-pointer"
|
||||||
|
>
|
||||||
|
<RefreshCw size={14} className="animate-spin-slow" />
|
||||||
|
<span>Try Again</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
71
app/faqs/[category]/page.tsx
Normal file
71
app/faqs/[category]/page.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useParams } from 'next/navigation';
|
||||||
|
import { AnnouncementBar } from '@/layout/AnnouncementBar';
|
||||||
|
import { Header } from '@/layout/Header';
|
||||||
|
import { Navbar } from '@/layout/Navbar';
|
||||||
|
import { Footer } from '@/layout/Footer';
|
||||||
|
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
|
||||||
|
import { FloatingButtons } from '@/components/FloatingButtons';
|
||||||
|
import { FaqCategoryPage } from '@/components/faq/FaqPageView';
|
||||||
|
import { storefrontService } from '@/services/api/storefrontService';
|
||||||
|
import { parseFaqPageData } from '@/lib/faqPageLayout';
|
||||||
|
import {
|
||||||
|
DEFAULT_FAQ_PAGE,
|
||||||
|
matchFaqCategory,
|
||||||
|
type FaqPageData,
|
||||||
|
} from '@/lib/faqDefaults';
|
||||||
|
|
||||||
|
export default function FaqCategoryRoutePage() {
|
||||||
|
const params = useParams<{ category: string }>();
|
||||||
|
const slug = String(params?.category || '');
|
||||||
|
const [data, setData] = useState<FaqPageData>(DEFAULT_FAQ_PAGE);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
storefrontService.getFaqLayout().then((raw) => {
|
||||||
|
if (!cancelled) setData(parseFaqPageData(raw));
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const category = useMemo(
|
||||||
|
() => matchFaqCategory(data.categories, slug),
|
||||||
|
[data.categories, slug]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[#F8F9FB] text-[#1E293B] flex flex-col font-sans">
|
||||||
|
<AnnouncementBar />
|
||||||
|
<Header />
|
||||||
|
<StickyHeaderSpacer />
|
||||||
|
<Navbar />
|
||||||
|
<main className="flex-grow py-10 pb-20">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1400px]">
|
||||||
|
{category ? (
|
||||||
|
<FaqCategoryPage data={data} category={category} />
|
||||||
|
) : (
|
||||||
|
<div className="bg-white border border-gray-200 rounded-2xl p-10 text-center max-w-lg mx-auto">
|
||||||
|
<h1 className="text-lg font-bold text-[#1E293B]">FAQ category not found</h1>
|
||||||
|
<p className="text-[13px] text-gray-500 mt-2 mb-6">
|
||||||
|
This category is missing or no longer published.
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href="/faqs"
|
||||||
|
className="inline-flex items-center justify-center h-10 px-5 rounded-lg bg-primary text-white text-[13px] font-semibold hover:bg-primary-hover"
|
||||||
|
>
|
||||||
|
Browse all FAQs
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
<FloatingButtons />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
app/faqs/layout.tsx
Normal file
16
app/faqs/layout.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'FAQs | iFixKart',
|
||||||
|
description:
|
||||||
|
'Find answers about refurbished mobiles, repairs, exchange, accessories, and buyback on iFixKart.',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function FaqsLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
43
app/faqs/page.tsx
Normal file
43
app/faqs/page.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { AnnouncementBar } from '@/layout/AnnouncementBar';
|
||||||
|
import { Header } from '@/layout/Header';
|
||||||
|
import { Navbar } from '@/layout/Navbar';
|
||||||
|
import { Footer } from '@/layout/Footer';
|
||||||
|
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
|
||||||
|
import { FloatingButtons } from '@/components/FloatingButtons';
|
||||||
|
import { FaqHub } from '@/components/faq/FaqPageView';
|
||||||
|
import { storefrontService } from '@/services/api/storefrontService';
|
||||||
|
import { parseFaqPageData } from '@/lib/faqPageLayout';
|
||||||
|
import { DEFAULT_FAQ_PAGE, type FaqPageData } from '@/lib/faqDefaults';
|
||||||
|
|
||||||
|
export default function FaqsPage() {
|
||||||
|
const [data, setData] = useState<FaqPageData>(DEFAULT_FAQ_PAGE);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
storefrontService.getFaqLayout().then((raw) => {
|
||||||
|
if (!cancelled) setData(parseFaqPageData(raw));
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[#F8F9FB] text-[#1E293B] flex flex-col font-sans">
|
||||||
|
<AnnouncementBar />
|
||||||
|
<Header />
|
||||||
|
<StickyHeaderSpacer />
|
||||||
|
<Navbar />
|
||||||
|
<main className="flex-grow py-10 pb-20">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1400px]">
|
||||||
|
<FaqHub data={data} />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
<FloatingButtons />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
app/feedback/layout.tsx
Normal file
12
app/feedback/layout.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Feedback | iFixKart',
|
||||||
|
description:
|
||||||
|
'Send iFixKart feedback about orders, repairs, payments, or the website. We read every message.',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function FeedbackLayout({ children }: { children: ReactNode }) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
280
app/feedback/page.tsx
Normal file
280
app/feedback/page.tsx
Normal file
|
|
@ -0,0 +1,280 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { FormEvent, useEffect, useMemo, useState } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { CheckCircle2, Lightbulb, MessageSquare, Star, TriangleAlert } from 'lucide-react';
|
||||||
|
import { AnnouncementBar } from '@/layout/AnnouncementBar';
|
||||||
|
import { Header } from '@/layout/Header';
|
||||||
|
import { Navbar } from '@/layout/Navbar';
|
||||||
|
import { Footer } from '@/layout/Footer';
|
||||||
|
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
|
||||||
|
import { FloatingButtons } from '@/components/FloatingButtons';
|
||||||
|
import {
|
||||||
|
storefrontService,
|
||||||
|
type FooterInfo,
|
||||||
|
type StorefrontSettings,
|
||||||
|
MOCK_FOOTER_INFO,
|
||||||
|
} from '@/services/api/storefrontService';
|
||||||
|
import { buildFeedbackPage } from '@/lib/feedbackContent';
|
||||||
|
|
||||||
|
const HIGHLIGHT_ICONS = {
|
||||||
|
idea: Lightbulb,
|
||||||
|
issue: TriangleAlert,
|
||||||
|
praise: MessageSquare,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const fieldClass =
|
||||||
|
'w-full px-3 py-2.5 bg-white border border-[#e5e5e5] rounded-lg text-[13px] text-[#222] outline-none focus:border-primary transition-colors';
|
||||||
|
|
||||||
|
const labelClass = 'block text-[12px] font-bold text-[#1E293B] mb-1.5';
|
||||||
|
|
||||||
|
export default function FeedbackPage() {
|
||||||
|
const [footer, setFooter] = useState<FooterInfo>(MOCK_FOOTER_INFO);
|
||||||
|
const [settings, setSettings] = useState<StorefrontSettings>({ store_name: 'iFixKart' });
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [topic, setTopic] = useState('suggestion');
|
||||||
|
const [orderNo, setOrderNo] = useState('');
|
||||||
|
const [rating, setRating] = useState(0);
|
||||||
|
const [message, setMessage] = useState('');
|
||||||
|
const [submitted, setSubmitted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
Promise.all([
|
||||||
|
storefrontService.getFooterInfo(),
|
||||||
|
storefrontService.getSettings(),
|
||||||
|
]).then(([info, store]) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
if (info) setFooter(info);
|
||||||
|
if (store) setSettings(store);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const storeName = settings.store_name || 'iFixKart';
|
||||||
|
const supportEmail = footer.email || 'support@ifixkart.com';
|
||||||
|
const doc = useMemo(() => buildFeedbackPage(storeName), [storeName]);
|
||||||
|
|
||||||
|
const handleSubmit = (e: FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const topicLabel = doc.topics.find((t) => t.id === topic)?.label || topic;
|
||||||
|
const body = [
|
||||||
|
`Name: ${name}`,
|
||||||
|
`Email: ${email}`,
|
||||||
|
`Topic: ${topicLabel}`,
|
||||||
|
orderNo ? `Order / job: ${orderNo}` : '',
|
||||||
|
rating ? `Rating: ${rating} / 5` : '',
|
||||||
|
'',
|
||||||
|
message,
|
||||||
|
]
|
||||||
|
.filter((line) => line !== '')
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
const href = `mailto:${supportEmail}?subject=${encodeURIComponent(
|
||||||
|
`${storeName} feedback — ${topicLabel}`
|
||||||
|
)}&body=${encodeURIComponent(body)}`;
|
||||||
|
window.location.href = href;
|
||||||
|
setSubmitted(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[#F8F9FB] text-[#1E293B] flex flex-col font-sans">
|
||||||
|
<AnnouncementBar />
|
||||||
|
<Header />
|
||||||
|
<StickyHeaderSpacer />
|
||||||
|
<Navbar />
|
||||||
|
|
||||||
|
<main className="flex-grow py-10 pb-20">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[900px]">
|
||||||
|
<h1 className="text-[28px] md:text-[32px] font-bold text-[#1E293B]">
|
||||||
|
{doc.heading}
|
||||||
|
</h1>
|
||||||
|
<p className="text-[14px] text-gray-500 mt-2 max-w-2xl">{doc.intro}</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-8">
|
||||||
|
{doc.highlights.map((item) => {
|
||||||
|
const Icon =
|
||||||
|
HIGHLIGHT_ICONS[item.id as keyof typeof HIGHLIGHT_ICONS] || MessageSquare;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
className="bg-white border border-gray-200 rounded-xl p-5"
|
||||||
|
>
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-primary/10 text-primary flex items-center justify-center mb-3">
|
||||||
|
<Icon className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-[16px] font-bold text-[#1E293B]">{item.title}</h2>
|
||||||
|
<p className="text-[13px] text-gray-500 mt-1 leading-relaxed">
|
||||||
|
{item.detail}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<article className="mt-8 bg-white border border-gray-200 rounded-xl overflow-hidden">
|
||||||
|
<div className="p-6 md:p-8">
|
||||||
|
{submitted ? (
|
||||||
|
<div className="py-6 text-center">
|
||||||
|
<CheckCircle2 className="w-10 h-10 text-primary mx-auto mb-3" />
|
||||||
|
<h2 className="text-[20px] font-bold text-[#1E293B]">Thanks for the feedback</h2>
|
||||||
|
<p className="text-[14px] text-gray-600 mt-2 max-w-md mx-auto leading-relaxed">
|
||||||
|
Your mail app should open with the message ready to send to {supportEmail}.
|
||||||
|
If it did not, copy your note and email us directly.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setSubmitted(false);
|
||||||
|
setMessage('');
|
||||||
|
}}
|
||||||
|
className="mt-6 text-[13px] font-semibold text-primary hover:underline cursor-pointer"
|
||||||
|
>
|
||||||
|
Send another message
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<h2 className="text-[16px] font-bold text-[#1E293B] mb-1">Send a message</h2>
|
||||||
|
<p className="text-[13px] text-gray-500 mb-4">
|
||||||
|
Fields marked with * are required.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="fb-name" className={labelClass}>
|
||||||
|
Name *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="fb-name"
|
||||||
|
required
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
className={fieldClass}
|
||||||
|
autoComplete="name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="fb-email" className={labelClass}>
|
||||||
|
Email *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="fb-email"
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
className={fieldClass}
|
||||||
|
autoComplete="email"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="fb-topic" className={labelClass}>
|
||||||
|
Topic *
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="fb-topic"
|
||||||
|
required
|
||||||
|
value={topic}
|
||||||
|
onChange={(e) => setTopic(e.target.value)}
|
||||||
|
className={`${fieldClass} cursor-pointer`}
|
||||||
|
>
|
||||||
|
{doc.topics.map((t) => (
|
||||||
|
<option key={t.id} value={t.id}>
|
||||||
|
{t.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="fb-order" className={labelClass}>
|
||||||
|
Order or job ID
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="fb-order"
|
||||||
|
value={orderNo}
|
||||||
|
onChange={(e) => setOrderNo(e.target.value)}
|
||||||
|
className={fieldClass}
|
||||||
|
placeholder="Optional"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className={labelClass}>How was your experience?</p>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{[1, 2, 3, 4, 5].map((stars) => (
|
||||||
|
<button
|
||||||
|
key={stars}
|
||||||
|
type="button"
|
||||||
|
aria-label={`${stars} star${stars === 1 ? '' : 's'}`}
|
||||||
|
onClick={() => setRating(rating === stars ? 0 : stars)}
|
||||||
|
className="p-0.5 cursor-pointer"
|
||||||
|
>
|
||||||
|
<Star
|
||||||
|
size={22}
|
||||||
|
className={
|
||||||
|
stars <= rating
|
||||||
|
? 'fill-amber-400 text-amber-400'
|
||||||
|
: 'text-gray-200'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="fb-message" className={labelClass}>
|
||||||
|
Message *
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="fb-message"
|
||||||
|
required
|
||||||
|
rows={6}
|
||||||
|
value={message}
|
||||||
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
className={`${fieldClass} resize-y min-h-[140px]`}
|
||||||
|
placeholder="Tell us what happened or what we should improve."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full sm:w-auto px-6 py-3 bg-primary text-white text-[14px] font-semibold rounded-lg hover:brightness-95 cursor-pointer"
|
||||||
|
>
|
||||||
|
Send feedback
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p className="text-[13px] text-gray-500 mt-8 pt-6 border-t border-gray-100">
|
||||||
|
For a return or payment issue, use{' '}
|
||||||
|
<Link href="/returns" className="text-primary font-semibold hover:underline">
|
||||||
|
Returns
|
||||||
|
</Link>{' '}
|
||||||
|
or{' '}
|
||||||
|
<Link href="/payments" className="text-primary font-semibold hover:underline">
|
||||||
|
Payments
|
||||||
|
</Link>
|
||||||
|
. Need answers first?{' '}
|
||||||
|
<Link href="/help" className="text-primary font-semibold hover:underline">
|
||||||
|
Help Center
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
<FloatingButtons />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
199
app/globals.css
Normal file
199
app/globals.css
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Outfit:wght@300;400;500;600;700;800;900&display=swap');
|
||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: #F8F9FB;
|
||||||
|
color: #1E293B;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Swiper Styles */
|
||||||
|
.swiper-pagination-bullet-active {
|
||||||
|
background: #1976F3 !important;
|
||||||
|
width: 24px !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swiper-button-next,
|
||||||
|
.swiper-button-prev {
|
||||||
|
color: #1976F3 !important;
|
||||||
|
background: white;
|
||||||
|
width: 40px !important;
|
||||||
|
height: 40px !important;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.swiper-button-next::after,
|
||||||
|
.swiper-button-prev::after {
|
||||||
|
font-size: 16px !important;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TechShop-style hero carousel (Slider Revolution-like) */
|
||||||
|
.techshop-hero .swiper-wrapper {
|
||||||
|
transition-timing-function: cubic-bezier(0.22, 0.61, 0.36, 1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.techshop-hero-pagination .swiper-pagination-bullet {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.55);
|
||||||
|
opacity: 1;
|
||||||
|
margin: 0 4px !important;
|
||||||
|
border-radius: 999px;
|
||||||
|
transition: width 0.25s ease, background 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.techshop-hero-pagination .swiper-pagination-bullet-active {
|
||||||
|
width: 22px !important;
|
||||||
|
background: #1976F3 !important;
|
||||||
|
border-radius: 999px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layer entrance animations on active slide */
|
||||||
|
.techshop-layer {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(22px);
|
||||||
|
transition:
|
||||||
|
opacity 0.55s cubic-bezier(0.22, 0.61, 0.36, 1),
|
||||||
|
transform 0.55s cubic-bezier(0.22, 0.61, 0.36, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.techshop-layer-media {
|
||||||
|
transform: translateX(36px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.techshop-hero-bg {
|
||||||
|
transform: scale(1.06);
|
||||||
|
transition: transform 0.85s cubic-bezier(0.22, 0.61, 0.36, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.swiper-slide-active .techshop-layer,
|
||||||
|
.swiper-slide-duplicate-active .techshop-layer {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.swiper-slide-active .techshop-layer-1,
|
||||||
|
.swiper-slide-duplicate-active .techshop-layer-1 {
|
||||||
|
transition-delay: 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swiper-slide-active .techshop-layer-2,
|
||||||
|
.swiper-slide-duplicate-active .techshop-layer-2 {
|
||||||
|
transition-delay: 0.22s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swiper-slide-active .techshop-layer-3,
|
||||||
|
.swiper-slide-duplicate-active .techshop-layer-3 {
|
||||||
|
transition-delay: 0.32s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swiper-slide-active .techshop-layer-4,
|
||||||
|
.swiper-slide-duplicate-active .techshop-layer-4 {
|
||||||
|
transition-delay: 0.42s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swiper-slide-active .techshop-layer-media,
|
||||||
|
.swiper-slide-duplicate-active .techshop-layer-media {
|
||||||
|
transition-delay: 0.18s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swiper-slide-active .techshop-hero-bg,
|
||||||
|
.swiper-slide-duplicate-active .techshop-hero-bg {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.techshop-hero .swiper-wrapper {
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.techshop-layer,
|
||||||
|
.techshop-hero-bg {
|
||||||
|
opacity: 1 !important;
|
||||||
|
transform: none !important;
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide scrollbars but keep scrolling */
|
||||||
|
.no-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.no-scrollbar {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Direct font overrides */
|
||||||
|
body, p, span, a, input, select, button {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6, .font-outfit {
|
||||||
|
font-family: 'Outfit', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Force container max-width to 1400px on large displays */
|
||||||
|
@media (min-width: 1400px) {
|
||||||
|
.container {
|
||||||
|
max-width: 1400px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Utilities for Purchased Template Colors & Shadows */
|
||||||
|
.bg-bgLight { background-color: #F8F9FB; }
|
||||||
|
.bg-cardLight { background-color: #FFFFFF; }
|
||||||
|
.text-textMain { color: #1E293B; }
|
||||||
|
.text-textMuted { color: #64748B; }
|
||||||
|
.bg-primary { background-color: #1976F3; }
|
||||||
|
.bg-primary-hover:hover { background-color: #1565C0; }
|
||||||
|
.bg-primary-light { background-color: #E3F2FD; }
|
||||||
|
.text-primary { color: #1976F3; }
|
||||||
|
.border-primary { border-color: #1976F3; }
|
||||||
|
.shadow-soft { box-shadow: 0 4px 20px -2px rgba(0, 0, 0, 0.05), 0 2px 10px -1px rgba(0, 0, 0, 0.03); }
|
||||||
|
.shadow-soft-hover:hover { box-shadow: 0 10px 30px -5px rgba(0, 0, 0, 0.08), 0 4px 15px -2px rgba(0, 0, 0, 0.04); }
|
||||||
|
|
||||||
|
/* Rich HTML product descriptions (bulk import + admin editor) */
|
||||||
|
.product-description-html ul,
|
||||||
|
.product-description-html ul li {
|
||||||
|
list-style-type: disc !important;
|
||||||
|
}
|
||||||
|
.product-description-html ol,
|
||||||
|
.product-description-html ol li {
|
||||||
|
list-style-type: decimal !important;
|
||||||
|
}
|
||||||
|
.product-description-html ul,
|
||||||
|
.product-description-html ol {
|
||||||
|
padding-left: 1.75rem !important;
|
||||||
|
margin-top: 0.5rem !important;
|
||||||
|
margin-bottom: 0.5rem !important;
|
||||||
|
}
|
||||||
|
.product-description-html li {
|
||||||
|
display: list-item !important;
|
||||||
|
margin-bottom: 0.25rem !important;
|
||||||
|
}
|
||||||
|
.product-description-html [style*="text-align: center"],
|
||||||
|
.product-description-html p[style*="text-align: center"] {
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
.product-description-html [style*="text-align: right"],
|
||||||
|
.product-description-html p[style*="text-align: right"] {
|
||||||
|
text-align: right !important;
|
||||||
|
}
|
||||||
|
.product-description-html [style*="text-align: justify"],
|
||||||
|
.product-description-html p[style*="text-align: justify"] {
|
||||||
|
text-align: justify !important;
|
||||||
|
}
|
||||||
|
.product-description-html strong,
|
||||||
|
.product-description-html b {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
12
app/help/layout.tsx
Normal file
12
app/help/layout.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Help Center | iFixKart',
|
||||||
|
description:
|
||||||
|
'Get help with iFixKart orders, repairs, payments, returns, and FAQs. Contact support or browse help topics.',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function HelpLayout({ children }: { children: ReactNode }) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
197
app/help/page.tsx
Normal file
197
app/help/page.tsx
Normal file
|
|
@ -0,0 +1,197 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import {
|
||||||
|
CircleHelp,
|
||||||
|
CreditCard,
|
||||||
|
LifeBuoy,
|
||||||
|
Mail,
|
||||||
|
Phone,
|
||||||
|
RotateCcw,
|
||||||
|
ShoppingBag,
|
||||||
|
Truck,
|
||||||
|
Wrench,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { AnnouncementBar } from '@/layout/AnnouncementBar';
|
||||||
|
import { Header } from '@/layout/Header';
|
||||||
|
import { Navbar } from '@/layout/Navbar';
|
||||||
|
import { Footer } from '@/layout/Footer';
|
||||||
|
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
|
||||||
|
import { FloatingButtons } from '@/components/FloatingButtons';
|
||||||
|
import {
|
||||||
|
storefrontService,
|
||||||
|
type FooterInfo,
|
||||||
|
type StorefrontSettings,
|
||||||
|
MOCK_FOOTER_INFO,
|
||||||
|
} from '@/services/api/storefrontService';
|
||||||
|
import { buildHelpDoc } from '@/lib/helpCenterContent';
|
||||||
|
|
||||||
|
const TOPIC_ICONS = {
|
||||||
|
faqs: CircleHelp,
|
||||||
|
orders: Truck,
|
||||||
|
payments: CreditCard,
|
||||||
|
returns: RotateCcw,
|
||||||
|
repairs: Wrench,
|
||||||
|
shop: ShoppingBag,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
function telHref(phone: string): string {
|
||||||
|
return `tel:${phone.replace(/[^\d+]/g, '')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HelpCenterPage() {
|
||||||
|
const [footer, setFooter] = useState<FooterInfo>(MOCK_FOOTER_INFO);
|
||||||
|
const [settings, setSettings] = useState<StorefrontSettings>({ store_name: 'iFixKart' });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
Promise.all([
|
||||||
|
storefrontService.getFooterInfo(),
|
||||||
|
storefrontService.getSettings(),
|
||||||
|
]).then(([info, store]) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
if (info) setFooter(info);
|
||||||
|
if (store) setSettings(store);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const storeName = settings.store_name || 'iFixKart';
|
||||||
|
const phone = footer.phone || settings.support_phone || '';
|
||||||
|
const email = footer.email || '';
|
||||||
|
const doc = useMemo(() => buildHelpDoc(storeName), [storeName]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[#F8F9FB] text-[#1E293B] flex flex-col font-sans">
|
||||||
|
<AnnouncementBar />
|
||||||
|
<Header />
|
||||||
|
<StickyHeaderSpacer />
|
||||||
|
<Navbar />
|
||||||
|
|
||||||
|
<main className="flex-grow py-10 pb-20">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[900px]">
|
||||||
|
<h1 className="text-[28px] md:text-[32px] font-bold text-[#1E293B]">
|
||||||
|
{doc.heading}
|
||||||
|
</h1>
|
||||||
|
<p className="text-[14px] text-gray-500 mt-2 max-w-2xl">{doc.intro}</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-8">
|
||||||
|
{doc.topics.map((item) => {
|
||||||
|
const Icon = TOPIC_ICONS[item.id as keyof typeof TOPIC_ICONS] || CircleHelp;
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={item.id}
|
||||||
|
href={item.href}
|
||||||
|
className="bg-white border border-gray-200 rounded-xl p-5 hover:border-primary/40 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-primary/10 text-primary flex items-center justify-center mb-3">
|
||||||
|
<Icon className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-[16px] font-bold text-[#1E293B]">{item.title}</h2>
|
||||||
|
<p className="text-[13px] text-gray-500 mt-1 leading-relaxed">
|
||||||
|
{item.detail}
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 bg-white border border-gray-200 rounded-xl p-5 md:p-6">
|
||||||
|
<h2 className="text-[13px] font-bold text-[#1E293B]">Browse FAQs</h2>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 mt-3">
|
||||||
|
{doc.faqLinks.map((item) => (
|
||||||
|
<Link
|
||||||
|
key={item.href}
|
||||||
|
href={item.href}
|
||||||
|
className="rounded-lg border border-gray-200 px-4 py-3 hover:border-primary/40 transition-colors"
|
||||||
|
>
|
||||||
|
<p className="text-[14px] font-bold text-[#1E293B]">{item.title}</p>
|
||||||
|
<p className="text-[12px] text-gray-500 mt-0.5 leading-relaxed">
|
||||||
|
{item.detail}
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(phone || email) && (
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4">
|
||||||
|
{phone ? (
|
||||||
|
<a
|
||||||
|
href={telHref(phone)}
|
||||||
|
className="bg-white border border-gray-200 rounded-xl p-5 hover:border-primary/40 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-primary/10 text-primary flex items-center justify-center mb-3">
|
||||||
|
<Phone className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-[12px] font-bold uppercase tracking-wide text-gray-400">
|
||||||
|
Phone
|
||||||
|
</h2>
|
||||||
|
<p className="text-[16px] font-bold text-[#1E293B] mt-1">{phone}</p>
|
||||||
|
<p className="text-[12px] text-gray-500 mt-1">Call customer support</p>
|
||||||
|
</a>
|
||||||
|
) : null}
|
||||||
|
{email ? (
|
||||||
|
<a
|
||||||
|
href={`mailto:${email}`}
|
||||||
|
className="bg-white border border-gray-200 rounded-xl p-5 hover:border-primary/40 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-primary/10 text-primary flex items-center justify-center mb-3">
|
||||||
|
<Mail className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-[12px] font-bold uppercase tracking-wide text-gray-400">
|
||||||
|
Email
|
||||||
|
</h2>
|
||||||
|
<p className="text-[16px] font-bold text-[#1E293B] mt-1 break-all">{email}</p>
|
||||||
|
<p className="text-[12px] text-gray-500 mt-1">Write to our support team</p>
|
||||||
|
</a>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<article className="mt-8 bg-white border border-gray-200 rounded-xl overflow-hidden">
|
||||||
|
<div className="p-6 md:p-8">
|
||||||
|
<div className="flex items-center gap-2 text-primary mb-6">
|
||||||
|
<LifeBuoy className="w-5 h-5" />
|
||||||
|
<p className="text-[13px] font-semibold">
|
||||||
|
Support is available Monday to Saturday, 10:00 AM – 6:30 PM
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-7">
|
||||||
|
{doc.sections.map((section) => (
|
||||||
|
<section key={section.id} id={section.id}>
|
||||||
|
<h3 className="text-[16px] font-bold text-[#1E293B] mb-2">
|
||||||
|
{section.title}
|
||||||
|
</h3>
|
||||||
|
{section.paragraphs.map((p, i) => (
|
||||||
|
<p
|
||||||
|
key={i}
|
||||||
|
className="text-[14px] text-gray-600 leading-relaxed mb-3 last:mb-0"
|
||||||
|
>
|
||||||
|
{p}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-[13px] text-gray-500 mt-8 pt-6 border-t border-gray-100">
|
||||||
|
Prefer a full contact page?{' '}
|
||||||
|
<Link href="/contact" className="text-primary font-semibold hover:underline">
|
||||||
|
Contact Us
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
<FloatingButtons />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
BIN
app/icon.png
Normal file
BIN
app/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 176 KiB |
37
app/layout.tsx
Normal file
37
app/layout.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
import './globals.css';
|
||||||
|
import '../styles/mobile-overrides.css';
|
||||||
|
import { AuthBootstrap } from '@/components/auth/AuthBootstrap';
|
||||||
|
import { StorefrontChrome } from '@/components/store/StorefrontChrome';
|
||||||
|
import { ClientModalsContainer } from '@/components/ClientModalsContainer';
|
||||||
|
import { Toaster } from 'sonner';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'iFixKart TechShop | Electronics & Certified Repair Hub',
|
||||||
|
description:
|
||||||
|
'Shop top smartphones, laptops, smartwatches, OEM replacement spare parts, and book certified device repair services.',
|
||||||
|
openGraph: {
|
||||||
|
title: 'iFixKart TechShop | Electronics & Certified Repair Hub',
|
||||||
|
description:
|
||||||
|
'Shop top smartphones, laptops, smartwatches, OEM replacement spare parts, and book certified device repair services.',
|
||||||
|
type: 'website',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<html lang="en" className="h-full">
|
||||||
|
<body className="flex flex-col min-h-screen bg-[#F8F9FB] text-[#1E293B] font-sans antialiased">
|
||||||
|
<Toaster position="top-right" richColors closeButton expand={true} />
|
||||||
|
<AuthBootstrap />
|
||||||
|
<StorefrontChrome />
|
||||||
|
<ClientModalsContainer />
|
||||||
|
{children}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
app/loading.tsx
Normal file
5
app/loading.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { HomePageSkeleton } from '@/components/ui/Skeleton';
|
||||||
|
|
||||||
|
export default function Loading() {
|
||||||
|
return <HomePageSkeleton />;
|
||||||
|
}
|
||||||
194
app/page.tsx
Normal file
194
app/page.tsx
Normal file
|
|
@ -0,0 +1,194 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { AnnouncementBar } from '../layout/AnnouncementBar';
|
||||||
|
import { Header } from '../layout/Header';
|
||||||
|
import { Navbar } from '../layout/Navbar';
|
||||||
|
import { Footer } from '../layout/Footer';
|
||||||
|
import { FloatingButtons } from '../components/FloatingButtons';
|
||||||
|
import { StickyHeaderSpacer } from '../components/StickyHeaderSpacer';
|
||||||
|
|
||||||
|
import { HomeHeroRow } from '../sections/HomeHeroRow';
|
||||||
|
import { PromoCardsSection } from '../sections/PromoCardsSection';
|
||||||
|
import { CategoriesSection } from '../sections/CategoriesSection';
|
||||||
|
import { NewArrivalsSection } from '../sections/NewArrivalsSection';
|
||||||
|
import { PromoBannersSection } from '../sections/PromoBannersSection';
|
||||||
|
import { HotDealsSection } from '../sections/HotDealsSection';
|
||||||
|
import { RecentlyLaunchedSection } from '../sections/RecentlyLaunchedSection';
|
||||||
|
import { BrandsSection } from '../sections/BrandsSection';
|
||||||
|
import { UltimateTechSection } from '../sections/UltimateTechSection';
|
||||||
|
import { ArticlesSection } from '../sections/ArticlesSection';
|
||||||
|
import { FeaturesSection } from '../sections/FeaturesSection';
|
||||||
|
|
||||||
|
import { catalogServerService } from '../services/api/catalogServerService';
|
||||||
|
import { mapCatalogProductToProduct } from '../utils/productMapper';
|
||||||
|
import { Product } from '../types';
|
||||||
|
import {
|
||||||
|
buildHomepageSections,
|
||||||
|
isSectionVisible,
|
||||||
|
normalizeLayoutPayload,
|
||||||
|
} from '../lib/layoutConfig';
|
||||||
|
import { sortHomeSectionsByOrder } from '../lib/techshopHome';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Always fetch fresh layout so admin Visible/Hidden + display_order apply.
|
||||||
|
* Homepage stack matches TechShop demo:
|
||||||
|
* https://demo.templatemonster.com/demo/501755.html
|
||||||
|
*/
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
export const revalidate = 0;
|
||||||
|
|
||||||
|
export default async function Home() {
|
||||||
|
let layoutData: any[] = [];
|
||||||
|
let dbProducts: any[] = [];
|
||||||
|
let categoriesData: any[] = [];
|
||||||
|
let brandsData: any[] = [];
|
||||||
|
let productsError = false;
|
||||||
|
|
||||||
|
const [productsResult, layoutResult, categoriesResult, brandsResult] =
|
||||||
|
await Promise.allSettled([
|
||||||
|
catalogServerService.getProducts(),
|
||||||
|
catalogServerService.fetchLayoutConfigOnServer(),
|
||||||
|
catalogServerService.getCategories(),
|
||||||
|
catalogServerService.getBrands(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (productsResult.status === 'fulfilled') {
|
||||||
|
dbProducts = productsResult.value || [];
|
||||||
|
} else {
|
||||||
|
console.error('Homepage products fetch failed:', productsResult.reason);
|
||||||
|
productsError = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layoutResult.status === 'fulfilled') {
|
||||||
|
layoutData = normalizeLayoutPayload(layoutResult.value);
|
||||||
|
} else {
|
||||||
|
console.error('Homepage layout fetch failed:', layoutResult.reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (categoriesResult.status === 'fulfilled') {
|
||||||
|
categoriesData = categoriesResult.value || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (brandsResult.status === 'fulfilled') {
|
||||||
|
brandsData = brandsResult.value || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const catalogProducts: Product[] =
|
||||||
|
dbProducts.length > 0 ? dbProducts.map(mapCatalogProductToProduct) : [];
|
||||||
|
|
||||||
|
const { activeSections, sectionsData, layoutLoaded } =
|
||||||
|
buildHomepageSections(layoutData);
|
||||||
|
|
||||||
|
/** Honor dashboard Visible/Hidden (`metadata_json.is_active`) for every section. */
|
||||||
|
const show = (key: string) =>
|
||||||
|
isSectionVisible(activeSections, key, layoutLoaded);
|
||||||
|
|
||||||
|
const orderedKeys = sortHomeSectionsByOrder(sectionsData).map((s) => s.key);
|
||||||
|
const productsLoading = false;
|
||||||
|
|
||||||
|
const sectionNodes: Record<string, React.ReactNode> = {
|
||||||
|
hero_slider: show('hero_slider') ? (
|
||||||
|
<HomeHeroRow
|
||||||
|
key="hero_slider"
|
||||||
|
heroConfig={sectionsData.hero_slider}
|
||||||
|
showHero
|
||||||
|
initialCategories={categoriesData}
|
||||||
|
/>
|
||||||
|
) : null,
|
||||||
|
promo_cards: show('promo_cards') ? (
|
||||||
|
<PromoCardsSection
|
||||||
|
key="promo_cards"
|
||||||
|
config={sectionsData.promo_cards}
|
||||||
|
/>
|
||||||
|
) : null,
|
||||||
|
featured_categories: show('featured_categories') ? (
|
||||||
|
<CategoriesSection
|
||||||
|
key="featured_categories"
|
||||||
|
config={sectionsData.featured_categories}
|
||||||
|
initialCategories={categoriesData}
|
||||||
|
/>
|
||||||
|
) : null,
|
||||||
|
editorial_story: show('editorial_story') ? (
|
||||||
|
<NewArrivalsSection
|
||||||
|
key="editorial_story"
|
||||||
|
config={sectionsData.editorial_story}
|
||||||
|
products={catalogProducts}
|
||||||
|
loading={productsLoading}
|
||||||
|
error={productsError}
|
||||||
|
/>
|
||||||
|
) : null,
|
||||||
|
promo_banners: show('promo_banners') ? (
|
||||||
|
<PromoBannersSection
|
||||||
|
key="promo_banners"
|
||||||
|
config={sectionsData.promo_banners}
|
||||||
|
/>
|
||||||
|
) : null,
|
||||||
|
deal_of_the_day: show('deal_of_the_day') ? (
|
||||||
|
<HotDealsSection
|
||||||
|
key="deal_of_the_day"
|
||||||
|
config={sectionsData.deal_of_the_day}
|
||||||
|
products={catalogProducts}
|
||||||
|
loading={productsLoading}
|
||||||
|
error={productsError}
|
||||||
|
/>
|
||||||
|
) : null,
|
||||||
|
recently_launched: show('recently_launched') ? (
|
||||||
|
<RecentlyLaunchedSection
|
||||||
|
key="recently_launched"
|
||||||
|
config={sectionsData.recently_launched}
|
||||||
|
products={catalogProducts}
|
||||||
|
loading={productsLoading}
|
||||||
|
error={productsError}
|
||||||
|
/>
|
||||||
|
) : null,
|
||||||
|
official_brands: show('official_brands') ? (
|
||||||
|
<BrandsSection
|
||||||
|
key="official_brands"
|
||||||
|
config={sectionsData.official_brands}
|
||||||
|
initialBrands={brandsData}
|
||||||
|
/>
|
||||||
|
) : null,
|
||||||
|
ultimate_tech: show('ultimate_tech') ? (
|
||||||
|
<UltimateTechSection
|
||||||
|
key="ultimate_tech"
|
||||||
|
config={sectionsData.ultimate_tech}
|
||||||
|
products={catalogProducts}
|
||||||
|
loading={productsLoading}
|
||||||
|
error={productsError}
|
||||||
|
/>
|
||||||
|
) : null,
|
||||||
|
tech_blog: show('tech_blog') ? (
|
||||||
|
<ArticlesSection key="tech_blog" config={sectionsData.tech_blog} />
|
||||||
|
) : null,
|
||||||
|
trust_badges: show('trust_badges') ? (
|
||||||
|
<FeaturesSection
|
||||||
|
key="trust_badges"
|
||||||
|
config={sectionsData.trust_badges}
|
||||||
|
/>
|
||||||
|
) : null,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[#F8F9FB] text-[#1E293B] flex flex-col font-sans">
|
||||||
|
<AnnouncementBar
|
||||||
|
config={sectionsData.announcement_bar}
|
||||||
|
isActive={show('announcement_bar')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Header />
|
||||||
|
|
||||||
|
<Navbar
|
||||||
|
config={sectionsData.nav_links}
|
||||||
|
isActive={show('nav_links')}
|
||||||
|
/>
|
||||||
|
{/* After Header + Navbar so sticky chrome reserves full height */}
|
||||||
|
<StickyHeaderSpacer />
|
||||||
|
|
||||||
|
<main className="flex-grow">
|
||||||
|
{orderedKeys.map((key) => sectionNodes[key] ?? null)}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
<FloatingButtons />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
app/payments/layout.tsx
Normal file
12
app/payments/layout.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Payments | iFixKart',
|
||||||
|
description:
|
||||||
|
'How to pay on iFixKart: cards, UPI, net banking, wallets, repair advances, refunds, and payment security.',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PaymentsLayout({ children }: { children: ReactNode }) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
176
app/payments/page.tsx
Normal file
176
app/payments/page.tsx
Normal file
|
|
@ -0,0 +1,176 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { CreditCard, Landmark, RefreshCw, ShieldCheck, Smartphone, Wallet } from 'lucide-react';
|
||||||
|
import { AnnouncementBar } from '@/layout/AnnouncementBar';
|
||||||
|
import { Header } from '@/layout/Header';
|
||||||
|
import { Navbar } from '@/layout/Navbar';
|
||||||
|
import { Footer } from '@/layout/Footer';
|
||||||
|
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
|
||||||
|
import { FloatingButtons } from '@/components/FloatingButtons';
|
||||||
|
import {
|
||||||
|
storefrontService,
|
||||||
|
type FooterInfo,
|
||||||
|
type StorefrontSettings,
|
||||||
|
MOCK_FOOTER_INFO,
|
||||||
|
} from '@/services/api/storefrontService';
|
||||||
|
import { buildPaymentsDoc } from '@/lib/paymentsContent';
|
||||||
|
import { PaymentMethodBadges } from '@/components/PaymentMethodBadges';
|
||||||
|
|
||||||
|
const HIGHLIGHT_ICONS = {
|
||||||
|
cards: CreditCard,
|
||||||
|
upi: Smartphone,
|
||||||
|
netbanking: Landmark,
|
||||||
|
wallets: Wallet,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
function telHref(phone: string): string {
|
||||||
|
return `tel:${phone.replace(/[^\d+]/g, '')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PaymentsPage() {
|
||||||
|
const [footer, setFooter] = useState<FooterInfo>(MOCK_FOOTER_INFO);
|
||||||
|
const [settings, setSettings] = useState<StorefrontSettings>({ store_name: 'iFixKart' });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
Promise.all([
|
||||||
|
storefrontService.getFooterInfo(),
|
||||||
|
storefrontService.getSettings(),
|
||||||
|
]).then(([info, store]) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
if (info) setFooter(info);
|
||||||
|
if (store) setSettings(store);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const storeName = settings.store_name || 'iFixKart';
|
||||||
|
const phone = footer.phone || settings.support_phone || '';
|
||||||
|
const email = footer.email || '';
|
||||||
|
const doc = useMemo(() => buildPaymentsDoc(storeName), [storeName]);
|
||||||
|
const methods = (footer.payment_methods || []).filter((p) => p?.name || p?.icon_url);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[#F8F9FB] text-[#1E293B] flex flex-col font-sans">
|
||||||
|
<AnnouncementBar />
|
||||||
|
<Header />
|
||||||
|
<StickyHeaderSpacer />
|
||||||
|
<Navbar />
|
||||||
|
|
||||||
|
<main className="flex-grow py-10 pb-20">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[900px]">
|
||||||
|
<h1 className="text-[28px] md:text-[32px] font-bold text-[#1E293B]">
|
||||||
|
{doc.heading}
|
||||||
|
</h1>
|
||||||
|
<p className="text-[14px] text-gray-500 mt-2 max-w-2xl">{doc.intro}</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-8">
|
||||||
|
{doc.highlights.map((item) => {
|
||||||
|
const Icon =
|
||||||
|
HIGHLIGHT_ICONS[item.id as keyof typeof HIGHLIGHT_ICONS] || CreditCard;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
className="bg-white border border-gray-200 rounded-xl p-5"
|
||||||
|
>
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-primary/10 text-primary flex items-center justify-center mb-3">
|
||||||
|
<Icon className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-[16px] font-bold text-[#1E293B]">{item.title}</h2>
|
||||||
|
<p className="text-[13px] text-gray-500 mt-1 leading-relaxed">
|
||||||
|
{item.detail}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 bg-white border border-gray-200 rounded-xl p-5">
|
||||||
|
<h2 className="text-[13px] font-bold text-[#1E293B]">We accept</h2>
|
||||||
|
<PaymentMethodBadges methods={methods} className="mt-3" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<article className="mt-8 bg-white border border-gray-200 rounded-xl overflow-hidden">
|
||||||
|
<div className="p-6 md:p-8">
|
||||||
|
<div className="flex items-center gap-2 text-primary mb-6">
|
||||||
|
<ShieldCheck className="w-5 h-5" />
|
||||||
|
<p className="text-[13px] font-semibold">
|
||||||
|
Checkout is processed on a secure payment page. Last updated: 11 September 2026
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-7">
|
||||||
|
{doc.sections.map((section) => (
|
||||||
|
<section key={section.id} id={section.id}>
|
||||||
|
<h3 className="text-[16px] font-bold text-[#1E293B] mb-2">
|
||||||
|
{section.title}
|
||||||
|
</h3>
|
||||||
|
{section.paragraphs.map((p, i) => (
|
||||||
|
<p
|
||||||
|
key={i}
|
||||||
|
className="text-[14px] text-gray-600 leading-relaxed mb-3 last:mb-0"
|
||||||
|
>
|
||||||
|
{p}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section className="mt-8 pt-6 border-t border-gray-100">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<RefreshCw className="w-4 h-4 text-gray-400" />
|
||||||
|
<h3 className="text-[16px] font-bold text-[#1E293B]">Need help with a payment?</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-[14px] text-gray-600 leading-relaxed">
|
||||||
|
Share your order number and payment reference with {storeName} support. You can
|
||||||
|
also read{' '}
|
||||||
|
<Link href="/faqs" className="text-primary font-semibold hover:underline">
|
||||||
|
payment FAQs
|
||||||
|
</Link>{' '}
|
||||||
|
or our{' '}
|
||||||
|
<Link href="/terms" className="text-primary font-semibold hover:underline">
|
||||||
|
Terms & Policy
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
<div className="mt-3 text-[14px] text-[#1E293B] space-y-1">
|
||||||
|
{email ? (
|
||||||
|
<p>
|
||||||
|
Email:{' '}
|
||||||
|
<a
|
||||||
|
href={`mailto:${email}`}
|
||||||
|
className="text-primary font-semibold hover:underline"
|
||||||
|
>
|
||||||
|
{email}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
{phone ? (
|
||||||
|
<p>
|
||||||
|
Phone:{' '}
|
||||||
|
<a
|
||||||
|
href={telHref(phone)}
|
||||||
|
className="text-primary font-semibold hover:underline"
|
||||||
|
>
|
||||||
|
{phone}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
<p className="text-gray-500">Monday to Saturday, 10:00 AM – 6:30 PM</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
<FloatingButtons />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
app/privacy/layout.tsx
Normal file
12
app/privacy/layout.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Privacy Policy | iFixKart',
|
||||||
|
description:
|
||||||
|
'How iFixKart collects, uses, and protects your personal information when you shop or book a repair.',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PrivacyLayout({ children }: { children: ReactNode }) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
7
app/privacy/page.tsx
Normal file
7
app/privacy/page.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { LegalPage } from '@/components/legal/LegalPage';
|
||||||
|
|
||||||
|
export default function PrivacyRoutePage() {
|
||||||
|
return <LegalPage initialTab="privacy" />;
|
||||||
|
}
|
||||||
5
app/products/[slug]/loading.tsx
Normal file
5
app/products/[slug]/loading.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { PageSkeleton } from '@/components/ui/Skeleton';
|
||||||
|
|
||||||
|
export default function Loading() {
|
||||||
|
return <PageSkeleton variant="product" />;
|
||||||
|
}
|
||||||
426
app/products/[slug]/page.tsx
Normal file
426
app/products/[slug]/page.tsx
Normal file
|
|
@ -0,0 +1,426 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
|
import { AnnouncementBar } from '@/layout/AnnouncementBar';
|
||||||
|
import { Header } from '@/layout/Header';
|
||||||
|
import { Navbar } from '@/layout/Navbar';
|
||||||
|
import { Footer } from '@/layout/Footer';
|
||||||
|
import { FloatingButtons } from '@/components/FloatingButtons';
|
||||||
|
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
|
||||||
|
|
||||||
|
import { catalogServerService } from '@/services/api/catalogServerService';
|
||||||
|
|
||||||
|
|
||||||
|
import { ProductInteractiveGrid } from '@/components/pdp/ProductInteractiveGrid';
|
||||||
|
import { ProductTabsWrapper } from '@/components/pdp/ProductTabsWrapper';
|
||||||
|
import { QuickComparisonTable } from '@/components/pdp/QuickComparisonTable';
|
||||||
|
import { YouMayAlsoLike } from '@/components/pdp/YouMayAlsoLike';
|
||||||
|
|
||||||
|
interface ProductDetailPageProps {
|
||||||
|
params: Promise<{ slug: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HIDDEN_SPEC_KEYS = [
|
||||||
|
'cost_price',
|
||||||
|
'costprice',
|
||||||
|
'parent_media_key',
|
||||||
|
'parent_category',
|
||||||
|
'parent_id',
|
||||||
|
'media_key',
|
||||||
|
'sku',
|
||||||
|
'barcode',
|
||||||
|
'compare_price',
|
||||||
|
];
|
||||||
|
|
||||||
|
function normalizeSpecKey(code: string): string {
|
||||||
|
return String(code || '').trim().toLowerCase().replace(/[\s-]+/g, '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
function humanizeSpecLabel(code: string): string {
|
||||||
|
const key = normalizeSpecKey(code);
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
quality: 'Quality',
|
||||||
|
qul: 'Quality',
|
||||||
|
qual: 'Quality',
|
||||||
|
ram: 'RAM',
|
||||||
|
storage: 'Storage',
|
||||||
|
color: 'Color',
|
||||||
|
colour: 'Color',
|
||||||
|
clr: 'Color',
|
||||||
|
size: 'Size',
|
||||||
|
condition: 'Condition',
|
||||||
|
in_box_contents: 'In the box',
|
||||||
|
warranty_months: 'Warranty',
|
||||||
|
country_of_origin: 'Country of origin',
|
||||||
|
device_model: 'Device model',
|
||||||
|
device_series: 'Device series',
|
||||||
|
};
|
||||||
|
if (labels[key]) return labels[key];
|
||||||
|
return code
|
||||||
|
.replace(/[_-]+/g, ' ')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim()
|
||||||
|
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function looksLikeId(value?: string | null): boolean {
|
||||||
|
if (!value) return true;
|
||||||
|
if (value.length > 18 && /[0-9]/.test(value)) return true;
|
||||||
|
if (/^[0-9A-Z]{16,}$/i.test(value.replace(/[_-]/g, ''))) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractDeviceModel(product: any, specifications: any[] = []): string {
|
||||||
|
if (product.device_model_name && !looksLikeId(product.device_model_name)) {
|
||||||
|
return product.device_model_name;
|
||||||
|
}
|
||||||
|
if (typeof product.device_model === 'object' && product.device_model?.name) {
|
||||||
|
return product.device_model.name;
|
||||||
|
}
|
||||||
|
if (typeof product.device_model === 'string' && product.device_model && !looksLikeId(product.device_model)) {
|
||||||
|
return product.device_model;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(specifications)) {
|
||||||
|
const modelRef = specifications.find((s: any) =>
|
||||||
|
['model reference', 'device model', 'model', 'device_model', 'model_reference'].includes(
|
||||||
|
String(s.label || s.code || s.name || '').toLowerCase()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (modelRef && modelRef.value && modelRef.value !== 'N/A' && !looksLikeId(modelRef.value)) {
|
||||||
|
return modelRef.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const seriesRef = specifications.find((s: any) =>
|
||||||
|
['series', 'device series', 'device_series'].includes(
|
||||||
|
String(s.label || s.code || s.name || '').toLowerCase()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (seriesRef && seriesRef.value && seriesRef.value !== 'N/A' && !looksLikeId(seriesRef.value)) {
|
||||||
|
return seriesRef.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (product.device_series_name && !looksLikeId(product.device_series_name)) {
|
||||||
|
return product.device_series_name;
|
||||||
|
}
|
||||||
|
if (typeof product.device_series === 'object' && product.device_series?.name) {
|
||||||
|
return product.device_series.name;
|
||||||
|
}
|
||||||
|
if (typeof product.device_series === 'string' && product.device_series && !looksLikeId(product.device_series)) {
|
||||||
|
return product.device_series;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attrRows = variantAttributeRows(product);
|
||||||
|
const devAttr = attrRows.find((a) =>
|
||||||
|
['device_model', 'device_series', 'compatible_device', 'supported_device', 'model_reference', 'model'].includes(a.key)
|
||||||
|
);
|
||||||
|
if (devAttr && devAttr.value && !looksLikeId(devAttr.value)) {
|
||||||
|
return devAttr.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = String(product.name || product.title || '').trim();
|
||||||
|
if (title) {
|
||||||
|
const cleaned = title
|
||||||
|
.replace(/\s*(?:with\s+touch\s+screen|with\s+touch|display|screen|touch\s+glass|combo|battery|panel|housing|back\s+glass|charging\s+port|flex\s+cable|spare\s+parts|replacement)\b.*/i, '')
|
||||||
|
.replace(/^buy\s+/i, '')
|
||||||
|
.trim();
|
||||||
|
if (cleaned) return cleaned;
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function variantAttributeRows(product: any): { key: string; label: string; value: string }[] {
|
||||||
|
const collected = new Map<string, Set<string>>();
|
||||||
|
(product.variants || []).forEach((variant: any) => {
|
||||||
|
(variant.attributes || []).forEach((attr: any) => {
|
||||||
|
const code = String(attr.attribute_code || '');
|
||||||
|
const key = normalizeSpecKey(code);
|
||||||
|
if (!code || HIDDEN_SPEC_KEYS.some((h) => key === h || key.includes(h))) return;
|
||||||
|
if (key.startsWith('parent_') || key.includes('cost')) return;
|
||||||
|
const value = String(attr.attribute_value || '').trim();
|
||||||
|
if (!value) return;
|
||||||
|
if (!collected.has(key)) collected.set(key, new Set());
|
||||||
|
collected.get(key)!.add(value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(collected.entries()).map(([key, values]) => ({
|
||||||
|
key,
|
||||||
|
label: humanizeSpecLabel(key),
|
||||||
|
value: Array.from(values).join(', '),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupSpecs(specifications: any[], product: any) {
|
||||||
|
const groups: { title: string; rows: { label: string; value: string }[] }[] = [];
|
||||||
|
const attrRows = variantAttributeRows(product);
|
||||||
|
|
||||||
|
const mapped = (specifications || []).map((spec: any) => ({
|
||||||
|
group: String(spec.group || spec.section || spec.category || '').trim(),
|
||||||
|
label: String(spec.label || spec.name || spec.key || ''),
|
||||||
|
value: String(spec.value ?? spec.val ?? ''),
|
||||||
|
})).filter((s) => s.label);
|
||||||
|
|
||||||
|
const brandValue = !looksLikeId(product.brand_name)
|
||||||
|
? product.brand_name
|
||||||
|
: !looksLikeId(product.brand_id)
|
||||||
|
? product.brand_id
|
||||||
|
: 'Gallery';
|
||||||
|
|
||||||
|
const used = new Set<string>();
|
||||||
|
const take = (keys: string[]) =>
|
||||||
|
attrRows
|
||||||
|
.filter((row) => keys.includes(row.key))
|
||||||
|
.map((row) => {
|
||||||
|
used.add(row.key);
|
||||||
|
return { label: row.label, value: row.value };
|
||||||
|
});
|
||||||
|
|
||||||
|
const general = [
|
||||||
|
{ label: 'Brand', value: brandValue },
|
||||||
|
{ label: 'Model', value: product.name },
|
||||||
|
...take(['device_series', 'device_model', 'condition', 'country_of_origin']),
|
||||||
|
];
|
||||||
|
const memory = take(['ram', 'storage', 'memory']);
|
||||||
|
const appearance = take(['color', 'colour']);
|
||||||
|
const pack = [
|
||||||
|
...(product.warranty_type ? [{ label: 'Warranty Type', value: product.warranty_type }] : []),
|
||||||
|
...(product.warranty_summary ? [{ label: 'Warranty Details', value: product.warranty_summary }] : []),
|
||||||
|
...take(['in_box_contents', 'warranty_months', 'warranty']),
|
||||||
|
];
|
||||||
|
const leftover = attrRows
|
||||||
|
.filter((row) => !used.has(row.key))
|
||||||
|
.map((row) => ({ label: row.label, value: row.value }));
|
||||||
|
|
||||||
|
groups.push({ title: 'GENERAL INFORMATION', rows: general });
|
||||||
|
if (memory.length) groups.push({ title: 'MEMORY & STORAGE', rows: memory });
|
||||||
|
if (appearance.length) groups.push({ title: 'APPEARANCE', rows: appearance });
|
||||||
|
if (pack.length) groups.push({ title: 'PACKAGE & WARRANTY', rows: pack });
|
||||||
|
if (leftover.length) groups.push({ title: 'ADDITIONAL DETAILS', rows: leftover });
|
||||||
|
|
||||||
|
if (mapped.some((s) => s.group)) {
|
||||||
|
const bucket: Record<string, { label: string; value: string }[]> = {};
|
||||||
|
mapped.forEach((s) => {
|
||||||
|
const key = (s.group || 'PRODUCT DETAILS').toUpperCase();
|
||||||
|
if (!bucket[key]) bucket[key] = [];
|
||||||
|
bucket[key].push({ label: s.label, value: s.value });
|
||||||
|
});
|
||||||
|
Object.entries(bucket).forEach(([title, rows]) => {
|
||||||
|
if (rows.length) groups.push({ title, rows });
|
||||||
|
});
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapped.length) {
|
||||||
|
groups.push({
|
||||||
|
title: 'PRODUCT DETAILS',
|
||||||
|
rows: mapped.map((s) => ({ label: s.label, value: s.value })),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function ProductDetailPage({ params }: ProductDetailPageProps) {
|
||||||
|
const { slug } = await params;
|
||||||
|
|
||||||
|
if (!slug) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
let data: any = null;
|
||||||
|
try {
|
||||||
|
data = await catalogServerService.getProductDetail(slug);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load product details on server:', err);
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data || !data.product) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { product, reviews = [], related_products = [], specifications = [] } = data;
|
||||||
|
const related = related_products.slice(0, 5);
|
||||||
|
const comparisonProducts = [product, ...related_products.filter((p: any) => p.product_id !== product.product_id)].slice(0, 5);
|
||||||
|
const specGroups = groupSpecs(specifications, product);
|
||||||
|
|
||||||
|
const additionalInfoRows: { label: string; value: string }[] = [];
|
||||||
|
|
||||||
|
// Supported Brand (auto-fetched)
|
||||||
|
let brandVal = product.brand_name || product.brand?.name || '';
|
||||||
|
if (looksLikeId(brandVal)) {
|
||||||
|
brandVal = '';
|
||||||
|
}
|
||||||
|
if (!brandVal) {
|
||||||
|
const brandAttr = variantAttributeRows(product).find((a) => a.key === 'brand' || a.key === 'brand_name');
|
||||||
|
if (brandAttr && !looksLikeId(brandAttr.value)) {
|
||||||
|
brandVal = brandAttr.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (brandVal) {
|
||||||
|
additionalInfoRows.push({ label: 'Supported Brand', value: brandVal });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supported Device (auto-fetched exact device model)
|
||||||
|
let deviceVal = extractDeviceModel(product, specifications);
|
||||||
|
if (deviceVal) {
|
||||||
|
additionalInfoRows.push({ label: 'Supported Device', value: deviceVal });
|
||||||
|
}
|
||||||
|
|
||||||
|
const wType = product.warranty_type || 'Brand Warranty';
|
||||||
|
additionalInfoRows.push({ label: 'Warranty Type', value: wType });
|
||||||
|
|
||||||
|
if (product.warranty_summary || product.warranty_details) {
|
||||||
|
additionalInfoRows.push({
|
||||||
|
label: 'Warranty Details',
|
||||||
|
value: product.warranty_summary || product.warranty_details,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const attrRowsForPack = variantAttributeRows(product);
|
||||||
|
attrRowsForPack.forEach((attr) => {
|
||||||
|
if (['in_box_contents', 'warranty_months', 'warranty'].includes(attr.key)) {
|
||||||
|
if (!additionalInfoRows.some((p) => p.label.toLowerCase() === attr.label.toLowerCase())) {
|
||||||
|
additionalInfoRows.push({ label: attr.label, value: attr.value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const descriptionHtml = (
|
||||||
|
<div>
|
||||||
|
{product.description ? (
|
||||||
|
<div className="text-[13px] text-[#555] leading-relaxed px-5 py-4">
|
||||||
|
<div
|
||||||
|
className="product-description-html prose prose-sm max-w-none text-[13px] text-[#555] leading-relaxed"
|
||||||
|
dangerouslySetInnerHTML={{ __html: product.description }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-[13px] text-gray-400 px-5 py-6">
|
||||||
|
No description available for this product.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{additionalInfoRows.length > 0 && (
|
||||||
|
<div className="border-t border-[#e0e0e0]">
|
||||||
|
<table className="w-full border-collapse text-[13px]">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
colSpan={2}
|
||||||
|
className="text-left text-primary font-bold uppercase tracking-wide text-[13px] bg-[#f6f7f9] px-5 py-3 border-b border-[#e0e0e0]"
|
||||||
|
>
|
||||||
|
ADDITIONAL INFORMATION
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
{additionalInfoRows.map((row, idx) => {
|
||||||
|
const isLast = idx === additionalInfoRows.length - 1;
|
||||||
|
return (
|
||||||
|
<tr key={`pw-${row.label}`}>
|
||||||
|
<td
|
||||||
|
className={`w-[28%] align-top font-semibold text-[#444] px-5 py-3.5 ${
|
||||||
|
isLast ? '' : 'border-b border-[#e0e0e0]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{row.label}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className={`align-top text-[#555] px-5 py-3.5 ${
|
||||||
|
isLast ? '' : 'border-b border-[#e0e0e0]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{row.value}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const specsHtml = (
|
||||||
|
<table className="w-full border-collapse text-[13px]">
|
||||||
|
<tbody>
|
||||||
|
{specGroups.map((group, groupIdx) => (
|
||||||
|
<React.Fragment key={group.title}>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
colSpan={2}
|
||||||
|
className={`text-left text-primary font-bold uppercase tracking-wide text-[13px] bg-[#f6f7f9] px-5 py-3 border-b border-[#e0e0e0] ${
|
||||||
|
groupIdx > 0 ? 'border-t border-[#e0e0e0]' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{group.title}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
{group.rows.map((row, rowIdx) => {
|
||||||
|
const isLast =
|
||||||
|
groupIdx === specGroups.length - 1 && rowIdx === group.rows.length - 1;
|
||||||
|
return (
|
||||||
|
<tr key={`${group.title}-${row.label}`}>
|
||||||
|
<td
|
||||||
|
className={`w-[28%] align-top font-semibold text-[#444] px-5 py-3.5 ${
|
||||||
|
isLast ? '' : 'border-b border-[#e0e0e0]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{row.label}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className={`align-top text-[#555] px-5 py-3.5 ${
|
||||||
|
isLast ? '' : 'border-b border-[#e0e0e0]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{row.value}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-white text-[#1E293B] flex flex-col font-sans">
|
||||||
|
<AnnouncementBar />
|
||||||
|
<Header />
|
||||||
|
<StickyHeaderSpacer />
|
||||||
|
<Navbar />
|
||||||
|
|
||||||
|
<main className="flex-grow pb-24 text-gray-800 antialiased">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-5">
|
||||||
|
<ProductInteractiveGrid
|
||||||
|
product={product}
|
||||||
|
initialReviewsCount={reviews.length}
|
||||||
|
relatedProducts={related}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mt-10">
|
||||||
|
<ProductTabsWrapper
|
||||||
|
descriptionHtml={descriptionHtml}
|
||||||
|
specsHtml={specsHtml}
|
||||||
|
showSpecifications={product.show_specifications ?? true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{related.length > 0 && <YouMayAlsoLike products={related} />}
|
||||||
|
|
||||||
|
{comparisonProducts.length > 0 && (
|
||||||
|
<QuickComparisonTable products={comparisonProducts} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
<FloatingButtons />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
app/products/loading.tsx
Normal file
5
app/products/loading.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { PageSkeleton } from '@/components/ui/Skeleton';
|
||||||
|
|
||||||
|
export default function Loading() {
|
||||||
|
return <PageSkeleton variant="catalog" />;
|
||||||
|
}
|
||||||
91
app/products/page.tsx
Normal file
91
app/products/page.tsx
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
/**
|
||||||
|
* @page Products Catalog Router Shell (`app/products/page.tsx`)
|
||||||
|
* @purpose Evaluates whether the selected category has `is_parent_feature = True`.
|
||||||
|
* If enabled, renders <ParentFeatureCatalog category={category} /> (Brand -> Series -> Model hierarchical mode).
|
||||||
|
* Otherwise, renders <StandardCatalog /> (Standard sidebar filter mode).
|
||||||
|
*/
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect, Suspense } from 'react';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
import { catalogService, CategoryResponse } from '@/services/api/catalogService';
|
||||||
|
import { ParentFeatureCatalog } from '@/components/catalog/ParentFeatureCatalog';
|
||||||
|
import { StandardCatalog } from '@/components/catalog/StandardCatalog';
|
||||||
|
|
||||||
|
function CatalogPageContent() {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const categoryParam = searchParams.get('category');
|
||||||
|
const brandParam = searchParams.get('brand');
|
||||||
|
const searchQuery = searchParams.get('search');
|
||||||
|
|
||||||
|
const [categories, setCategories] = useState<CategoryResponse[]>([]);
|
||||||
|
const [activeCategory, setActiveCategory] = useState<CategoryResponse | null>(null);
|
||||||
|
const [isLoadingCategory, setIsLoadingCategory] = useState<boolean>(true);
|
||||||
|
|
||||||
|
// Fetch Categories and evaluate parent feature flag
|
||||||
|
useEffect(() => {
|
||||||
|
setIsLoadingCategory(true);
|
||||||
|
catalogService
|
||||||
|
.getCategories()
|
||||||
|
.then((catData) => {
|
||||||
|
setCategories(catData);
|
||||||
|
if (categoryParam) {
|
||||||
|
const found = catData.find(
|
||||||
|
(c) =>
|
||||||
|
c.category_id === categoryParam ||
|
||||||
|
c.slug.toLowerCase() === categoryParam.toLowerCase()
|
||||||
|
);
|
||||||
|
setActiveCategory(found || null);
|
||||||
|
} else {
|
||||||
|
setActiveCategory(null);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setCategories([]);
|
||||||
|
setActiveCategory(null);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoadingCategory(false);
|
||||||
|
});
|
||||||
|
}, [categoryParam]);
|
||||||
|
|
||||||
|
if (isLoadingCategory) {
|
||||||
|
return (
|
||||||
|
<div className="w-full bg-[#f8f9fa] min-h-screen py-12 font-sans flex items-center justify-center">
|
||||||
|
<div className="flex flex-col items-center gap-3">
|
||||||
|
<div className="w-8 h-8 border-3 border-red-600 border-t-transparent rounded-full animate-spin" />
|
||||||
|
<p className="text-xs font-semibold text-gray-500">Loading catalog...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a category is selected AND has is_parent_feature enabled -> Render ParentFeatureCatalog
|
||||||
|
if (activeCategory && activeCategory.is_parent_feature) {
|
||||||
|
return <ParentFeatureCatalog category={activeCategory} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise -> Render StandardCatalog
|
||||||
|
return (
|
||||||
|
<StandardCatalog
|
||||||
|
initialCategory={activeCategory}
|
||||||
|
categoryParam={categoryParam}
|
||||||
|
brandParam={brandParam}
|
||||||
|
searchQuery={searchQuery}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProductsPage() {
|
||||||
|
return (
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<div className="w-full bg-[#f8f9fa] min-h-screen py-12 font-sans flex items-center justify-center">
|
||||||
|
<div className="w-8 h-8 border-3 border-red-600 border-t-transparent rounded-full animate-spin" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CatalogPageContent />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
app/repair-services/loading.tsx
Normal file
5
app/repair-services/loading.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { PageSkeleton } from '@/components/ui/Skeleton';
|
||||||
|
|
||||||
|
export default function Loading() {
|
||||||
|
return <PageSkeleton variant="simple" />;
|
||||||
|
}
|
||||||
9
app/repair-services/page.tsx
Normal file
9
app/repair-services/page.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
/**
|
||||||
|
* @page Repair Services Route (`app/repair-services/page.tsx`)
|
||||||
|
* @purpose Renders device repair booking portal for /repair-services URLs.
|
||||||
|
*/
|
||||||
|
import RepairPortalPage from '../services/page';
|
||||||
|
|
||||||
|
export default function RepairServicesRoute() {
|
||||||
|
return <RepairPortalPage />;
|
||||||
|
}
|
||||||
12
app/returns/layout.tsx
Normal file
12
app/returns/layout.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Returns | iFixKart',
|
||||||
|
description:
|
||||||
|
'Return unused accessories, report a defective delivery, or start a warranty claim on an iFixKart order.',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ReturnsLayout({ children }: { children: ReactNode }) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
179
app/returns/page.tsx
Normal file
179
app/returns/page.tsx
Normal file
|
|
@ -0,0 +1,179 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { Clock, Package, RefreshCw, ShieldCheck, Undo2 } from 'lucide-react';
|
||||||
|
import { AnnouncementBar } from '@/layout/AnnouncementBar';
|
||||||
|
import { Header } from '@/layout/Header';
|
||||||
|
import { Navbar } from '@/layout/Navbar';
|
||||||
|
import { Footer } from '@/layout/Footer';
|
||||||
|
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
|
||||||
|
import { FloatingButtons } from '@/components/FloatingButtons';
|
||||||
|
import {
|
||||||
|
storefrontService,
|
||||||
|
type FooterInfo,
|
||||||
|
type StorefrontSettings,
|
||||||
|
MOCK_FOOTER_INFO,
|
||||||
|
} from '@/services/api/storefrontService';
|
||||||
|
import { buildReturnsDoc } from '@/lib/returnsContent';
|
||||||
|
|
||||||
|
const HIGHLIGHT_ICONS = {
|
||||||
|
window: Clock,
|
||||||
|
doa: Package,
|
||||||
|
refund: RefreshCw,
|
||||||
|
warranty: ShieldCheck,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
function telHref(phone: string): string {
|
||||||
|
return `tel:${phone.replace(/[^\d+]/g, '')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ReturnsPage() {
|
||||||
|
const [footer, setFooter] = useState<FooterInfo>(MOCK_FOOTER_INFO);
|
||||||
|
const [settings, setSettings] = useState<StorefrontSettings>({ store_name: 'iFixKart' });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
Promise.all([
|
||||||
|
storefrontService.getFooterInfo(),
|
||||||
|
storefrontService.getSettings(),
|
||||||
|
]).then(([info, store]) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
if (info) setFooter(info);
|
||||||
|
if (store) setSettings(store);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const storeName = settings.store_name || 'iFixKart';
|
||||||
|
const phone = footer.phone || settings.support_phone || '';
|
||||||
|
const email = footer.email || '';
|
||||||
|
const doc = useMemo(() => buildReturnsDoc(storeName), [storeName]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[#F8F9FB] text-[#1E293B] flex flex-col font-sans">
|
||||||
|
<AnnouncementBar />
|
||||||
|
<Header />
|
||||||
|
<StickyHeaderSpacer />
|
||||||
|
<Navbar />
|
||||||
|
|
||||||
|
<main className="flex-grow py-10 pb-20">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[900px]">
|
||||||
|
<h1 className="text-[28px] md:text-[32px] font-bold text-[#1E293B]">
|
||||||
|
{doc.heading}
|
||||||
|
</h1>
|
||||||
|
<p className="text-[14px] text-gray-500 mt-2 max-w-2xl">{doc.intro}</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-8">
|
||||||
|
{doc.highlights.map((item) => {
|
||||||
|
const Icon =
|
||||||
|
HIGHLIGHT_ICONS[item.id as keyof typeof HIGHLIGHT_ICONS] || Undo2;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
className="bg-white border border-gray-200 rounded-xl p-5"
|
||||||
|
>
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-primary/10 text-primary flex items-center justify-center mb-3">
|
||||||
|
<Icon className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-[16px] font-bold text-[#1E293B]">{item.title}</h2>
|
||||||
|
<p className="text-[13px] text-gray-500 mt-1 leading-relaxed">
|
||||||
|
{item.detail}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<article className="mt-8 bg-white border border-gray-200 rounded-xl overflow-hidden">
|
||||||
|
<div className="p-6 md:p-8">
|
||||||
|
<div className="flex items-center gap-2 text-primary mb-6">
|
||||||
|
<Undo2 className="w-5 h-5" />
|
||||||
|
<p className="text-[13px] font-semibold">
|
||||||
|
Approved pickups need an original invoice. Last updated: 11 September 2026
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-7">
|
||||||
|
{doc.sections.map((section) => (
|
||||||
|
<section key={section.id} id={section.id}>
|
||||||
|
<h3 className="text-[16px] font-bold text-[#1E293B] mb-2">
|
||||||
|
{section.title}
|
||||||
|
</h3>
|
||||||
|
{section.paragraphs.map((p, i) => (
|
||||||
|
<p
|
||||||
|
key={i}
|
||||||
|
className="text-[14px] text-gray-600 leading-relaxed mb-3 last:mb-0"
|
||||||
|
>
|
||||||
|
{p}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section className="mt-8 pt-6 border-t border-gray-100">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<RefreshCw className="w-4 h-4 text-gray-400" />
|
||||||
|
<h3 className="text-[16px] font-bold text-[#1E293B]">Need help with a return?</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-[14px] text-gray-600 leading-relaxed">
|
||||||
|
Write to{' '}
|
||||||
|
<a
|
||||||
|
href="mailto:returns@ifixkart.com"
|
||||||
|
className="text-primary font-semibold hover:underline"
|
||||||
|
>
|
||||||
|
returns@ifixkart.com
|
||||||
|
</a>{' '}
|
||||||
|
with your order number. You can also read{' '}
|
||||||
|
<Link href="/faqs" className="text-primary font-semibold hover:underline">
|
||||||
|
FAQs
|
||||||
|
</Link>
|
||||||
|
,{' '}
|
||||||
|
<Link href="/payments" className="text-primary font-semibold hover:underline">
|
||||||
|
Payments
|
||||||
|
</Link>
|
||||||
|
, or our{' '}
|
||||||
|
<Link href="/terms" className="text-primary font-semibold hover:underline">
|
||||||
|
Terms & Policy
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
<div className="mt-3 text-[14px] text-[#1E293B] space-y-1">
|
||||||
|
{email ? (
|
||||||
|
<p>
|
||||||
|
Support:{' '}
|
||||||
|
<a
|
||||||
|
href={`mailto:${email}`}
|
||||||
|
className="text-primary font-semibold hover:underline"
|
||||||
|
>
|
||||||
|
{email}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
{phone ? (
|
||||||
|
<p>
|
||||||
|
Phone:{' '}
|
||||||
|
<a
|
||||||
|
href={telHref(phone)}
|
||||||
|
className="text-primary font-semibold hover:underline"
|
||||||
|
>
|
||||||
|
{phone}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
<p className="text-gray-500">Monday to Saturday, 10:00 AM – 6:30 PM</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
<FloatingButtons />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
app/services/loading.tsx
Normal file
5
app/services/loading.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { PageSkeleton } from '@/components/ui/Skeleton';
|
||||||
|
|
||||||
|
export default function Loading() {
|
||||||
|
return <PageSkeleton variant="simple" />;
|
||||||
|
}
|
||||||
1612
app/services/page.tsx
Normal file
1612
app/services/page.tsx
Normal file
File diff suppressed because it is too large
Load diff
554
app/services/success/page.tsx
Normal file
554
app/services/success/page.tsx
Normal file
|
|
@ -0,0 +1,554 @@
|
||||||
|
/**
|
||||||
|
* @page Service Booking Success & Payment Confirmation (`app/services/success/page.tsx`)
|
||||||
|
* @purpose Storefront-chrome confirmation with booking + deposit payment summary.
|
||||||
|
*/
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Suspense, useEffect, useMemo, useState, type ReactNode } from 'react';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { formatCurrency } from '@/lib/utils';
|
||||||
|
import { serviceBookingService } from '@/services/api/serviceBooking';
|
||||||
|
import { setAccessToken } from '@/services/api/client';
|
||||||
|
import { useAuthStore } from '@/store/authStore';
|
||||||
|
import { parseMetadata } from '@/lib/layoutConfig';
|
||||||
|
import {
|
||||||
|
findServiceSection,
|
||||||
|
isServiceSectionVisible,
|
||||||
|
normalizeServiceLayout,
|
||||||
|
} from '@/lib/servicePageLayout';
|
||||||
|
import {
|
||||||
|
CheckCircle2,
|
||||||
|
Clock,
|
||||||
|
Home,
|
||||||
|
Loader2,
|
||||||
|
CreditCard,
|
||||||
|
ShieldCheck,
|
||||||
|
Smartphone,
|
||||||
|
Wrench,
|
||||||
|
ArrowRight,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { AnnouncementBar } from '@/layout/AnnouncementBar';
|
||||||
|
import { Header } from '@/layout/Header';
|
||||||
|
import { Navbar } from '@/layout/Navbar';
|
||||||
|
import { Footer } from '@/layout/Footer';
|
||||||
|
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
|
||||||
|
import { FloatingButtons } from '@/components/FloatingButtons';
|
||||||
|
import { ClientModalsContainer } from '@/components/ClientModalsContainer';
|
||||||
|
|
||||||
|
function PageShell({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[#F8F9FB] text-[#1E293B] flex flex-col font-sans">
|
||||||
|
<AnnouncementBar />
|
||||||
|
<Header />
|
||||||
|
<StickyHeaderSpacer />
|
||||||
|
<Navbar />
|
||||||
|
<main className="flex-grow pb-16">{children}</main>
|
||||||
|
<Footer />
|
||||||
|
<FloatingButtons />
|
||||||
|
<ClientModalsContainer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unwrapJob(raw: any): any {
|
||||||
|
if (!raw) return null;
|
||||||
|
if (raw.job && typeof raw.job === 'object') return { ...raw, ...raw.job };
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstPayment(job: any): any | null {
|
||||||
|
if (!job) return null;
|
||||||
|
const list = Array.isArray(job.payments)
|
||||||
|
? job.payments
|
||||||
|
: Array.isArray(job.payment_records)
|
||||||
|
? job.payment_records
|
||||||
|
: job.payment
|
||||||
|
? [job.payment]
|
||||||
|
: [];
|
||||||
|
return list[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function alreadyVerifiedMessage(message?: string) {
|
||||||
|
const text = String(message || '').toLowerCase();
|
||||||
|
return (
|
||||||
|
text.includes('already verified') ||
|
||||||
|
text.includes('already captured') ||
|
||||||
|
text.includes('already processed')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BookingSuccessContent() {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const { accessToken, authReady, bootstrapAuth } = useAuthStore();
|
||||||
|
|
||||||
|
const jobNoParam = searchParams.get('job_no') || 'SRV-BOOKING';
|
||||||
|
const jobId = searchParams.get('job_id') || '';
|
||||||
|
const statusParam = searchParams.get('status') || '';
|
||||||
|
const qPaymentId = searchParams.get('payment_id') || '';
|
||||||
|
const qRzpPaymentId = searchParams.get('rzp_payment_id') || '';
|
||||||
|
const qRzpOrderId = searchParams.get('rzp_order_id') || '';
|
||||||
|
const qRzpSignature = searchParams.get('rzp_signature') || '';
|
||||||
|
const qAmount = Number(searchParams.get('amount') || 0);
|
||||||
|
const qMethod = searchParams.get('method') || '';
|
||||||
|
const needsVerify = searchParams.get('verify') === '1';
|
||||||
|
|
||||||
|
const [jobDetails, setJobDetails] = useState<any | null>(null);
|
||||||
|
const [layoutCMS, setLayoutCMS] = useState<any[]>([]);
|
||||||
|
const [verifyState, setVerifyState] = useState<'idle' | 'verifying' | 'done'>(
|
||||||
|
needsVerify ? 'verifying' : 'idle'
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void bootstrapAuth();
|
||||||
|
}, [bootstrapAuth]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (accessToken) setAccessToken(accessToken);
|
||||||
|
}, [accessToken]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
toast.dismiss();
|
||||||
|
serviceBookingService.rememberLocalJob({
|
||||||
|
job_id: jobId || undefined,
|
||||||
|
job_no: jobNoParam !== 'SRV-BOOKING' ? jobNoParam : undefined,
|
||||||
|
});
|
||||||
|
}, [jobId, jobNoParam]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!needsVerify) {
|
||||||
|
setVerifyState('done');
|
||||||
|
if (jobId) void loadJobDetails(jobId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!authReady) return;
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
const finish = (verified: boolean) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
setVerifyState('done');
|
||||||
|
const next = new URLSearchParams(searchParams.toString());
|
||||||
|
next.delete('verify');
|
||||||
|
next.delete('rzp_signature');
|
||||||
|
if (verified) next.set('status', 'PAID');
|
||||||
|
router.replace(`/services/success?${next.toString()}`);
|
||||||
|
if (jobId) void loadJobDetails(jobId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const runVerify = async () => {
|
||||||
|
const token = useAuthStore.getState().accessToken;
|
||||||
|
if (token) setAccessToken(token);
|
||||||
|
|
||||||
|
if (!qPaymentId || !qRzpOrderId || !qRzpPaymentId || !qRzpSignature) {
|
||||||
|
finish(Boolean(qRzpPaymentId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let timer = 0;
|
||||||
|
const verifyPromise = serviceBookingService.verifyServicePayment({
|
||||||
|
payment_id: qPaymentId,
|
||||||
|
razorpay_order_id: qRzpOrderId,
|
||||||
|
razorpay_payment_id: qRzpPaymentId,
|
||||||
|
razorpay_signature: qRzpSignature,
|
||||||
|
});
|
||||||
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||||
|
timer = window.setTimeout(() => reject(new Error('timeout')), 10000);
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await Promise.race([verifyPromise, timeoutPromise]);
|
||||||
|
} finally {
|
||||||
|
window.clearTimeout(timer);
|
||||||
|
}
|
||||||
|
finish(true);
|
||||||
|
} catch (err: any) {
|
||||||
|
if (alreadyVerifiedMessage(err?.message) || err?.message === 'timeout') {
|
||||||
|
finish(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toast.error(err?.message || 'Payment confirmation is taking longer than expected.');
|
||||||
|
finish(Boolean(qRzpPaymentId));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void runVerify();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [authReady, needsVerify, jobId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadLayout = async () => {
|
||||||
|
try {
|
||||||
|
const cms = await serviceBookingService.fetchServicePageLayout();
|
||||||
|
setLayoutCMS(normalizeServiceLayout(cms));
|
||||||
|
} catch {
|
||||||
|
/* defaults */
|
||||||
|
}
|
||||||
|
};
|
||||||
|
void loadLayout();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadJobDetails = async (id: string) => {
|
||||||
|
try {
|
||||||
|
const data = await serviceBookingService.fetchServiceJob(id);
|
||||||
|
setJobDetails(data);
|
||||||
|
const item = unwrapJob(data);
|
||||||
|
serviceBookingService.rememberLocalJob({
|
||||||
|
job_id: item?.job_id || id,
|
||||||
|
job_no: item?.job_no || jobNoParam,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
/* query-param fallback */
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const job = unwrapJob(jobDetails);
|
||||||
|
const payment = firstPayment(job);
|
||||||
|
const isPaid =
|
||||||
|
statusParam === 'PAID' ||
|
||||||
|
Boolean(qRzpPaymentId) ||
|
||||||
|
String(payment?.status || payment?.payment_status || '').toUpperCase() ===
|
||||||
|
'CAPTURED';
|
||||||
|
const isPending = statusParam === 'PENDING' && !isPaid;
|
||||||
|
|
||||||
|
const jobNo = job?.job_no || jobNoParam;
|
||||||
|
const status = job?.status || (isPending ? 'PENDING' : 'BOOKED');
|
||||||
|
const deviceLabel = [job?.device_brand, job?.device_model].filter(Boolean).join(' ');
|
||||||
|
const serviceName =
|
||||||
|
job?.service_name ||
|
||||||
|
job?.custom_service_name ||
|
||||||
|
job?.repair_category ||
|
||||||
|
'';
|
||||||
|
const variantName =
|
||||||
|
job?.variant_name ||
|
||||||
|
job?.repair_variant_name ||
|
||||||
|
payment?.variant_name ||
|
||||||
|
'';
|
||||||
|
|
||||||
|
const basePrice = Number(
|
||||||
|
job?.base_price ?? job?.quoted_price ?? payment?.base_amount ?? 0
|
||||||
|
);
|
||||||
|
const paidFromPayment = Number(payment?.amount ?? payment?.paid_amount ?? 0);
|
||||||
|
const paidFromQuery = qAmount > 1000 ? qAmount / 100 : qAmount;
|
||||||
|
const paidAmount =
|
||||||
|
paidFromPayment > 0
|
||||||
|
? paidFromPayment
|
||||||
|
: paidFromQuery > 0
|
||||||
|
? paidFromQuery
|
||||||
|
: basePrice > 0
|
||||||
|
? basePrice * 0.2
|
||||||
|
: 0;
|
||||||
|
const doorstepFee = job?.fulfillment_type === 'DOORSTEP_PICKUP' ? 250 : 0;
|
||||||
|
const totalQuote = basePrice + doorstepFee;
|
||||||
|
const remaining = Math.max(totalQuote - paidAmount, 0);
|
||||||
|
const paymentMethod =
|
||||||
|
payment?.provider ||
|
||||||
|
payment?.payment_method ||
|
||||||
|
qMethod ||
|
||||||
|
(isPending ? '—' : 'Razorpay');
|
||||||
|
const paymentType = payment?.payment_type || (isPending ? 'ADVANCE' : 'ADVANCE');
|
||||||
|
const paymentStatus =
|
||||||
|
payment?.status ||
|
||||||
|
payment?.payment_status ||
|
||||||
|
(isPaid ? 'CAPTURED' : isPending ? 'PENDING' : 'CAPTURED');
|
||||||
|
const gatewayPaymentId =
|
||||||
|
payment?.razorpay_payment_id ||
|
||||||
|
payment?.gateway_payment_id ||
|
||||||
|
payment?.provider_payment_id ||
|
||||||
|
qRzpPaymentId ||
|
||||||
|
'';
|
||||||
|
const internalPaymentId = payment?.payment_id || qPaymentId || '';
|
||||||
|
const paidAt = payment?.paid_at || payment?.created_at || payment?.verified_at;
|
||||||
|
const appointmentStart =
|
||||||
|
job?.appointment?.scheduled_start || job?.scheduled_start;
|
||||||
|
|
||||||
|
const howItWorksCMS = findServiceSection(layoutCMS, 'service_how_it_works');
|
||||||
|
const howMeta = parseMetadata(howItWorksCMS);
|
||||||
|
const showHow = isServiceSectionVisible(layoutCMS, 'service_how_it_works');
|
||||||
|
const nextSteps = howMeta.steps || [
|
||||||
|
{
|
||||||
|
number: '1',
|
||||||
|
title: 'Technician Assignment',
|
||||||
|
desc: 'Our system matches a certified repair specialist for your device model.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: '2',
|
||||||
|
title: 'On-Site Service',
|
||||||
|
desc: 'Technician visits your location at the requested time slot to fix your device.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: '3',
|
||||||
|
title: 'Inspect & Settle',
|
||||||
|
desc: 'Verify your device after repair, pay remaining balance, and get warranty cover.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const nextTitle = howItWorksCMS?.title || 'What happens next?';
|
||||||
|
const nextSubtitle =
|
||||||
|
howItWorksCMS?.subtitle || 'Your repair journey continues with these steps';
|
||||||
|
|
||||||
|
const infoRows = useMemo(() => {
|
||||||
|
const rows: { label: string; value: ReactNode }[] = [
|
||||||
|
{ label: 'Booking reference', value: <span className="font-mono">{jobNo}</span> },
|
||||||
|
{ label: 'Job status', value: status },
|
||||||
|
];
|
||||||
|
if (deviceLabel) rows.push({ label: 'Device', value: deviceLabel });
|
||||||
|
if (serviceName) rows.push({ label: 'Service', value: serviceName });
|
||||||
|
if (variantName) rows.push({ label: 'Price variant', value: variantName });
|
||||||
|
if (appointmentStart) {
|
||||||
|
rows.push({
|
||||||
|
label: 'Scheduled slot',
|
||||||
|
value: new Date(appointmentStart).toLocaleString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}, [jobNo, status, deviceLabel, serviceName, variantName, appointmentStart]);
|
||||||
|
|
||||||
|
if (verifyState === 'verifying' || (needsVerify && !authReady)) {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1400px] py-24 flex flex-col items-center justify-center gap-3 text-center">
|
||||||
|
<Loader2 className="w-8 h-8 animate-spin text-primary" />
|
||||||
|
<p className="text-[14px] font-semibold text-[#1a1a1a]">
|
||||||
|
Verifying payment
|
||||||
|
</p>
|
||||||
|
<p className="text-[12px] text-gray-500">
|
||||||
|
Please wait a moment. Do not refresh this page.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1400px] py-8 space-y-6">
|
||||||
|
<div className="bg-white border border-[#e5e5e5] p-4 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<div className="text-[11px] font-semibold text-primary uppercase tracking-wider">
|
||||||
|
Repair booking
|
||||||
|
</div>
|
||||||
|
<h1 className="text-xl font-bold text-[#222] mt-0.5">
|
||||||
|
{isPending ? 'Payment confirmation' : 'Payment verified'}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className="text-[12px] text-gray-500 font-semibold">
|
||||||
|
Reference:{' '}
|
||||||
|
<span className="font-mono font-bold text-[#222]">{jobNo}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white border border-[#e5e5e5] p-6 sm:p-8 space-y-6">
|
||||||
|
<div className="flex flex-col md:flex-row md:items-center gap-5">
|
||||||
|
<div
|
||||||
|
className={`w-14 h-14 shrink-0 flex items-center justify-center border ${
|
||||||
|
isPending
|
||||||
|
? 'bg-amber-50 border-amber-200 text-amber-600'
|
||||||
|
: 'bg-emerald-50 border-emerald-200 text-emerald-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isPending ? (
|
||||||
|
<Clock className="w-7 h-7" />
|
||||||
|
) : (
|
||||||
|
<CheckCircle2 className="w-7 h-7" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h2 className="text-2xl font-bold text-[#222] tracking-tight">
|
||||||
|
{isPending ? 'Booking received' : 'Payment verified'}
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-500 text-sm mt-1 max-w-xl leading-relaxed">
|
||||||
|
Service order{' '}
|
||||||
|
<span className="font-mono font-bold text-[#222]">{jobNo}</span>{' '}
|
||||||
|
{isPending
|
||||||
|
? 'is saved. Complete the advance payment to lock your technician slot.'
|
||||||
|
: 'is confirmed. Your advance payment is complete and our team is preparing your repair schedule.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
||||||
|
<div className="border border-[#e5e5e5] p-5 space-y-4">
|
||||||
|
<div className="flex items-center gap-2 text-[11px] font-semibold uppercase tracking-wider text-[#222] border-b border-[#e5e5e5] pb-2">
|
||||||
|
<Smartphone className="w-4 h-4 text-primary" />
|
||||||
|
Booking details
|
||||||
|
</div>
|
||||||
|
<dl className="space-y-3 text-[13px]">
|
||||||
|
{infoRows.map((row) => (
|
||||||
|
<div key={row.label} className="flex justify-between gap-4">
|
||||||
|
<dt className="text-gray-500">{row.label}</dt>
|
||||||
|
<dd className="font-semibold text-[#222] text-right">{row.value}</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border border-[#e5e5e5] p-5 space-y-4">
|
||||||
|
<div className="flex items-center gap-2 text-[11px] font-semibold uppercase tracking-wider text-[#222] border-b border-[#e5e5e5] pb-2">
|
||||||
|
<CreditCard className="w-4 h-4 text-primary" />
|
||||||
|
Payment information
|
||||||
|
</div>
|
||||||
|
<dl className="space-y-3 text-[13px]">
|
||||||
|
<div className="flex justify-between gap-4">
|
||||||
|
<dt className="text-gray-500">Payment type</dt>
|
||||||
|
<dd className="font-semibold text-[#222]">{paymentType}</dd>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between gap-4">
|
||||||
|
<dt className="text-gray-500">Method</dt>
|
||||||
|
<dd className="font-semibold text-[#222]">{paymentMethod}</dd>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between gap-4">
|
||||||
|
<dt className="text-gray-500">Payment status</dt>
|
||||||
|
<dd
|
||||||
|
className={`font-bold ${
|
||||||
|
isPending ? 'text-amber-600' : 'text-emerald-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{['CAPTURED', 'PAID', 'SUCCESS', 'COMPLETED'].includes(String(paymentStatus).toUpperCase()) ? 'PAID' : String(paymentStatus).replace(/_/g, ' ')}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
{basePrice > 0 && (
|
||||||
|
<div className="flex justify-between gap-4">
|
||||||
|
<dt className="text-gray-500">Base service quote</dt>
|
||||||
|
<dd className="font-semibold text-[#222]">
|
||||||
|
{formatCurrency(basePrice)}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex justify-between gap-4">
|
||||||
|
<dt className="text-gray-500">
|
||||||
|
{isPending ? 'Advance due (20%)' : 'Advance paid (20%)'}
|
||||||
|
</dt>
|
||||||
|
<dd className="font-bold text-primary">
|
||||||
|
{paidAmount > 0 ? formatCurrency(paidAmount) : '—'}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
{basePrice > 0 && (
|
||||||
|
<div className="flex justify-between gap-4">
|
||||||
|
<dt className="text-gray-500">Balance due at pickup</dt>
|
||||||
|
<dd className="font-semibold text-[#222]">
|
||||||
|
{formatCurrency(remaining)}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{internalPaymentId ? (
|
||||||
|
<div className="flex justify-between gap-4">
|
||||||
|
<dt className="text-gray-500">Payment ID</dt>
|
||||||
|
<dd className="font-mono text-[12px] font-semibold text-[#222] break-all text-right">
|
||||||
|
{internalPaymentId}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{gatewayPaymentId ? (
|
||||||
|
<div className="flex justify-between gap-4">
|
||||||
|
<dt className="text-gray-500">Razorpay payment ID</dt>
|
||||||
|
<dd className="font-mono text-[12px] font-semibold text-[#222] break-all text-right">
|
||||||
|
{gatewayPaymentId}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{paidAt ? (
|
||||||
|
<div className="flex justify-between gap-4">
|
||||||
|
<dt className="text-gray-500">Paid at</dt>
|
||||||
|
<dd className="font-semibold text-[#222] text-right">
|
||||||
|
{new Date(paidAt).toLocaleString()}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-2 bg-[#F8F9FB] border border-[#e5e5e5] p-3 text-[12px] text-gray-500">
|
||||||
|
<ShieldCheck className="w-4 h-4 text-primary shrink-0 mt-0.5" />
|
||||||
|
<span>
|
||||||
|
Remaining balance is collected after the repair is completed
|
||||||
|
and verified. Warranty applies to the selected variant.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showHow && (
|
||||||
|
<div className="space-y-4 pt-2 border-t border-[#e5e5e5]">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2 text-primary font-bold text-xs uppercase tracking-wider">
|
||||||
|
<Wrench className="w-4 h-4" />
|
||||||
|
Next steps
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-bold text-[#222] mt-1">{nextTitle}</h3>
|
||||||
|
{nextSubtitle ? (
|
||||||
|
<p className="text-gray-500 text-xs mt-1">{nextSubtitle}</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`grid grid-cols-1 gap-4 ${
|
||||||
|
nextSteps.length >= 3
|
||||||
|
? 'md:grid-cols-3'
|
||||||
|
: nextSteps.length === 2
|
||||||
|
? 'md:grid-cols-2'
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{nextSteps.map((step: any, idx: number) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="bg-[#F8F9FB] p-5 border border-[#e5e5e5] space-y-2"
|
||||||
|
>
|
||||||
|
<span className="w-8 h-8 bg-primary text-white font-bold text-sm flex items-center justify-center">
|
||||||
|
{step.number || idx + 1}
|
||||||
|
</span>
|
||||||
|
<div className="font-bold text-[#222] text-sm">{step.title}</div>
|
||||||
|
<div className="text-xs text-gray-500 leading-relaxed">
|
||||||
|
{step.desc}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row gap-3 pt-2 border-t border-[#e5e5e5]">
|
||||||
|
<Link
|
||||||
|
href={`/services/tracking/${encodeURIComponent(jobId || jobNo)}`}
|
||||||
|
className="px-6 py-3 bg-primary hover:bg-primary-hover text-white font-bold text-[13px] flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<Clock className="w-4 h-4" />
|
||||||
|
Track repair status
|
||||||
|
<ArrowRight className="w-4 h-4" />
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/services"
|
||||||
|
className="px-6 py-3 border border-[#e5e5e5] text-[#333] font-semibold text-[13px] hover:bg-[#f7f9fc] flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
Book another repair
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="px-6 py-3 border border-[#e5e5e5] text-[#333] font-semibold text-[13px] hover:bg-[#f7f9fc] flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<Home className="w-4 h-4" />
|
||||||
|
Return to storefront
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ServiceBookingSuccessPage() {
|
||||||
|
return (
|
||||||
|
<PageShell>
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<div className="py-20 flex flex-col items-center justify-center text-gray-400 space-y-3">
|
||||||
|
<Loader2 className="w-8 h-8 animate-spin text-primary" />
|
||||||
|
<span className="text-[13px] font-semibold">
|
||||||
|
Verifying payment
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<BookingSuccessContent />
|
||||||
|
</Suspense>
|
||||||
|
</PageShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue