Initial commit - Storefront
This commit is contained in:
commit
96772bf541
247 changed files with 49059 additions and 0 deletions
4
.env.local
Normal file
4
.env.local
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
NEXT_PUBLIC_API_URL=https://ifixkartbe.trionixsolution.com
|
||||||
|
API_URL=http://127.0.0.1:8010
|
||||||
|
NEXT_PUBLIC_GOOGLE_CLIENT_ID=395995422555-3gf9b434jqf471qbhumdme72noudqptg.apps.googleusercontent.com
|
||||||
|
NEXT_PUBLIC_RAZORPAY_KEY_ID=rzp_test_TTUzPFYF0hRV89
|
||||||
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
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules/
|
||||||
|
.next/
|
||||||
|
.env
|
||||||
BIN
Archive (1).zip
Normal file
BIN
Archive (1).zip
Normal file
Binary file not shown.
BIN
Archive (2).zip
Normal file
BIN
Archive (2).zip
Normal file
Binary file not shown.
BIN
Archive.zip
Normal file
BIN
Archive.zip
Normal file
Binary file not shown.
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
BIN
Storefront.zip
Normal file
BIN
Storefront.zip
Normal file
Binary file not shown.
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 />;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue