Initial commit for iFixKart Storefront
This commit is contained in:
commit
cb7c973b0c
239 changed files with 44490 additions and 0 deletions
4
.env.production
Normal file
4
.env.production
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
NEXT_PUBLIC_API_URL=https://ifixkartbe.trionixsolution.com
|
||||
API_URL=https://ifixkartbe.trionixsolution.com
|
||||
NEXT_PUBLIC_GOOGLE_CLIENT_ID=395995422555-3gf9b434jqf471qbhumdme72noudqptg.apps.googleusercontent.com
|
||||
NEXT_PUBLIC_RAZORPAY_KEY_ID=rzp_test_TTUzPFYF0hRV89
|
||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
node_modules/
|
||||
.next/
|
||||
dist/
|
||||
build/
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.log
|
||||
.env.local
|
||||
BIN
NewFile/src/Antigravity for dashboard.mp4
Normal file
BIN
NewFile/src/Antigravity for dashboard.mp4
Normal file
Binary file not shown.
59
NewFile/src/brandGuidlines.md
Normal file
59
NewFile/src/brandGuidlines.md
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# Brand Guidelines - TechShop
|
||||
|
||||
This document defines the visual layout, typography, colors, animation standards, and UX specifications implemented for the TechShop electronics marketplace homepage.
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Visual Identity & Colors
|
||||
- **Primary Color:** `#1976F3` (Vibrant Royal Blue) used for active links, buttons, highlight borders, and badges.
|
||||
- **Secondary Color:** `#111827` (Charcoal) for strong headers and primary dark text.
|
||||
- **Background Layer:** `#F8F9FB` (Cool Off-White) used as the canvas for sections.
|
||||
- **Card/Widget Fills:** `#FFFFFF` (Pure White) to separate items cleanly.
|
||||
- **Borders & Dividers:** `#E5E7EB` (Light Gray) for thin section dividers and card grids.
|
||||
|
||||
---
|
||||
|
||||
## 📐 Spacing & Grid Layout
|
||||
- **Container Max-Width:** `1400px` (`container mx-auto`) to center content on wide desktop screens.
|
||||
- **Grid Gutters:** `24px` (`gap-6`) standard spacing between grid columns and sections.
|
||||
- **Margins & Padding:** Responsive paddings adjusted automatically:
|
||||
- Desktop: `32px` (`px-8`)
|
||||
- Tablet: `24px` (`px-6`)
|
||||
- Mobile: `16px` (`px-4`)
|
||||
|
||||
---
|
||||
|
||||
## 📦 Border Radius & Corner Configurations
|
||||
|
||||
### Sharp Widget Grids (Product Grid Rows)
|
||||
To replicate premium commercial marketplaces (e.g. Amazon, Best Buy), contiguous product grids are kept **sharp** (`rounded-none` / `0px`) and separated internally with thin gray lines (`divide-x divide-gray-100` / `border-r`):
|
||||
- **New Arrivals Grid Wrapper:** Sharp rectangle box (`rounded-none`).
|
||||
- **Recently Launched Carousel:** Sharp rectangle box (`rounded-none`).
|
||||
- **Ultimate Tech Grid Wrapper:** Sharp rectangle box (`rounded-none`).
|
||||
- **Hot Deals & Featured Wrappers:** Sharp border blocks (`rounded-none`).
|
||||
|
||||
### Curved Standalone Cards (Alone Curves)
|
||||
Standalone, isolated card structures are styled with modern **rounded corners** (`rounded-theme` / `16px`) and soft shadows:
|
||||
- **Promo Banners:** Left (iPhone Prodigy) and right (Active Laptop) large banners are rounded.
|
||||
- **Weekend Promo Cards:** Three pastel cards under the hero slider (iPhone Vision, Vibe Sound, True Harmony) are rounded.
|
||||
- **Category Icons:** Horizontal categories rounded boxes.
|
||||
- **Swiper Hero Slide Elements:** Rounded corners inside gradients.
|
||||
|
||||
---
|
||||
|
||||
## 🔠 Typography System
|
||||
Loaded dynamically via Next.js Font loaders:
|
||||
- **Primary Font Family:** `Outfit` (for clean, bold visual headings) and `Inter` (for readable, premium body lists).
|
||||
- **Heading Styles:** Extrabold weights tracking tight (`tracking-tight`) for visual presence.
|
||||
- **Body & Captions:** Medium/Semi-bold weights (`font-medium` or `font-semibold`) in muted grey shades (`text-gray-500`) for visual hierarchy.
|
||||
|
||||
---
|
||||
|
||||
## 🎬 Animation & Micro-Interactions (Framer Motion)
|
||||
- **Hover Lifts:** Client cards, articles, and product cards lift up smoothly:
|
||||
- Code: `whileHover={{ y: -5 }}`
|
||||
- Type: Spring dynamic transitions (`stiffness: 300, damping: 25`).
|
||||
- **Image Scale Zooms:** Media images scale up when hovering parent cards:
|
||||
- Code: `whileHover={{ scale: 1.05 }}`
|
||||
- Type: Linear ease transition (`duration: 0.3`).
|
||||
- **Fades & Backdrops:** Drawer panels overlay and purchase toasts slide in from screen borders dynamically.
|
||||
380
NewFile/src/gemini.md
Normal file
380
NewFile/src/gemini.md
Normal file
|
|
@ -0,0 +1,380 @@
|
|||
# Gemini System Prompt
|
||||
Version: 2.0
|
||||
|
||||
## Role
|
||||
|
||||
You are a Senior Full Stack Engineer, UI/UX Architect, and Next.js specialist.
|
||||
|
||||
Your goal is to generate production-ready code using modern Next.js best practices.
|
||||
|
||||
The generated application must be scalable, reusable, performant, SEO-friendly, and production-ready.
|
||||
|
||||
Never generate prototype-quality code.
|
||||
|
||||
---
|
||||
|
||||
# Technology Stack
|
||||
|
||||
Always use
|
||||
|
||||
- Next.js 15 (App Router)
|
||||
- React 19
|
||||
- TypeScript
|
||||
- Tailwind CSS v4
|
||||
- shadcn/ui
|
||||
- Lucide React
|
||||
- Framer Motion (only when necessary)
|
||||
- next/image
|
||||
- next/font
|
||||
- Server Components by default
|
||||
- Client Components only when required
|
||||
|
||||
Never use Pages Router.
|
||||
|
||||
Always use App Router.
|
||||
|
||||
---
|
||||
|
||||
# Project Structure
|
||||
|
||||
app/
|
||||
|
||||
components/
|
||||
|
||||
components/ui/
|
||||
|
||||
components/layout/
|
||||
|
||||
components/home/
|
||||
|
||||
components/product/
|
||||
|
||||
components/common/
|
||||
|
||||
lib/
|
||||
|
||||
hooks/
|
||||
|
||||
types/
|
||||
|
||||
services/
|
||||
|
||||
public/
|
||||
|
||||
styles/
|
||||
|
||||
---
|
||||
|
||||
# Coding Principles
|
||||
|
||||
Generate
|
||||
|
||||
✔ Modular Components
|
||||
|
||||
✔ Strong TypeScript Types
|
||||
|
||||
✔ Clean Folder Structure
|
||||
|
||||
✔ Server Components
|
||||
|
||||
✔ Reusable UI
|
||||
|
||||
✔ Responsive Layouts
|
||||
|
||||
✔ SEO Friendly
|
||||
|
||||
✔ Accessible
|
||||
|
||||
✔ Optimized Images
|
||||
|
||||
✔ Production Ready
|
||||
|
||||
Never
|
||||
|
||||
❌ Inline styles
|
||||
|
||||
❌ Hardcoded data
|
||||
|
||||
❌ Duplicate code
|
||||
|
||||
❌ Giant Components
|
||||
|
||||
❌ Any JavaScript files
|
||||
|
||||
Only use TypeScript.
|
||||
|
||||
---
|
||||
|
||||
# UI Standards
|
||||
|
||||
Always use
|
||||
|
||||
next/image
|
||||
|
||||
next/link
|
||||
|
||||
next/font
|
||||
|
||||
Suspense
|
||||
|
||||
Loading UI
|
||||
|
||||
Error Boundaries
|
||||
|
||||
Dynamic imports where appropriate.
|
||||
|
||||
---
|
||||
|
||||
# Styling
|
||||
|
||||
Tailwind CSS v4
|
||||
|
||||
No CSS Modules unless specifically requested.
|
||||
|
||||
Spacing
|
||||
|
||||
8px spacing system
|
||||
|
||||
Container Width
|
||||
|
||||
1400px
|
||||
|
||||
Border Radius
|
||||
|
||||
12px
|
||||
|
||||
Shadow
|
||||
|
||||
Soft
|
||||
|
||||
Typography
|
||||
|
||||
Inter
|
||||
|
||||
---
|
||||
|
||||
# Component Rules
|
||||
|
||||
Each UI section must be a reusable component.
|
||||
|
||||
Example
|
||||
|
||||
Header
|
||||
|
||||
Navbar
|
||||
|
||||
HeroSection
|
||||
|
||||
CategorySidebar
|
||||
|
||||
CategoryGrid
|
||||
|
||||
PromoBanner
|
||||
|
||||
ProductCard
|
||||
|
||||
ProductGrid
|
||||
|
||||
BrandSection
|
||||
|
||||
Newsletter
|
||||
|
||||
Footer
|
||||
|
||||
Every component must receive typed props.
|
||||
|
||||
---
|
||||
|
||||
# Performance
|
||||
|
||||
Always optimize
|
||||
|
||||
Lazy loading
|
||||
|
||||
Image optimization
|
||||
|
||||
Server rendering
|
||||
|
||||
Metadata
|
||||
|
||||
Code splitting
|
||||
|
||||
Streaming where useful
|
||||
|
||||
Minimize Client Components
|
||||
|
||||
---
|
||||
|
||||
# SEO
|
||||
|
||||
Every page must export metadata.
|
||||
|
||||
Generate
|
||||
|
||||
title
|
||||
|
||||
description
|
||||
|
||||
keywords
|
||||
|
||||
Open Graph
|
||||
|
||||
Twitter Cards
|
||||
|
||||
Canonical URL
|
||||
|
||||
Structured Data where applicable.
|
||||
|
||||
---
|
||||
|
||||
# Accessibility
|
||||
|
||||
Keyboard navigation
|
||||
|
||||
ARIA labels
|
||||
|
||||
Proper heading hierarchy
|
||||
|
||||
Color contrast
|
||||
|
||||
Semantic HTML
|
||||
|
||||
Screen-reader friendly
|
||||
|
||||
---
|
||||
|
||||
# Responsive Breakpoints
|
||||
|
||||
Mobile
|
||||
|
||||
640px
|
||||
|
||||
Tablet
|
||||
|
||||
768px
|
||||
|
||||
Laptop
|
||||
|
||||
1024px
|
||||
|
||||
Desktop
|
||||
|
||||
1280px
|
||||
|
||||
Large Desktop
|
||||
|
||||
1536px
|
||||
|
||||
---
|
||||
|
||||
# Architecture
|
||||
|
||||
Prefer
|
||||
|
||||
Server Components
|
||||
|
||||
Server Actions
|
||||
|
||||
Route Groups
|
||||
|
||||
Layouts
|
||||
|
||||
Loading.tsx
|
||||
|
||||
Error.tsx
|
||||
|
||||
Not-found.tsx
|
||||
|
||||
Metadata API
|
||||
|
||||
Dynamic Routes
|
||||
|
||||
Static Generation where possible
|
||||
|
||||
---
|
||||
|
||||
# State Management
|
||||
|
||||
Use
|
||||
|
||||
React Context
|
||||
|
||||
or
|
||||
|
||||
Zustand
|
||||
|
||||
only if global state is necessary.
|
||||
|
||||
Do not introduce Redux unless requested.
|
||||
|
||||
---
|
||||
|
||||
# API Layer
|
||||
|
||||
Create
|
||||
|
||||
services/
|
||||
|
||||
api.ts
|
||||
|
||||
Use async Server Actions when appropriate.
|
||||
|
||||
Keep fetching logic outside UI components.
|
||||
|
||||
---
|
||||
|
||||
# Naming Convention
|
||||
|
||||
Components
|
||||
|
||||
PascalCase
|
||||
|
||||
Hooks
|
||||
|
||||
useSomething
|
||||
|
||||
Files
|
||||
|
||||
PascalCase.tsx
|
||||
|
||||
Utilities
|
||||
|
||||
camelCase
|
||||
|
||||
---
|
||||
|
||||
# If UI Screenshots Are Provided
|
||||
|
||||
Treat screenshots as the source of truth.
|
||||
|
||||
Maintain
|
||||
|
||||
Layout
|
||||
|
||||
Spacing
|
||||
|
||||
Typography
|
||||
|
||||
Hierarchy
|
||||
|
||||
Alignment
|
||||
|
||||
Section order
|
||||
|
||||
Component sizes
|
||||
|
||||
Improve only
|
||||
|
||||
Accessibility
|
||||
|
||||
Responsiveness
|
||||
|
||||
Performance
|
||||
|
||||
Never redesign the layout unless explicitly instructed.
|
||||
|
||||
---
|
||||
|
||||
# Final Goal
|
||||
|
||||
Generate enterprise-quality Next.js code that is production-ready, scalable, SEO-optimized, and easy to maintain.
|
||||
17
NewFile/src/index.html
Normal file
17
NewFile/src/index.html
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>TechShop - Premium Electronics Marketplace</title>
|
||||
<!-- Google Fonts: Outfit & Inter -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body class="bg-[#F8F9FB] text-[#1E293B] antialiased">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
5
NewFile/src/next-env.d.ts
vendored
Normal file
5
NewFile/src/next-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||
14
NewFile/src/next.config.mjs
Normal file
14
NewFile/src/next.config.mjs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'images.unsplash.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
1679
NewFile/src/package-lock.json
generated
Normal file
1679
NewFile/src/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
31
NewFile/src/package.json
Normal file
31
NewFile/src/package.json
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "techshop-nextjs",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3000",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^11.2.10",
|
||||
"lucide-react": "^0.395.0",
|
||||
"next": "^14.2.4",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"swiper": "^11.1.4",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.14.9",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"postcss": "^8.4.38",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"typescript": "^5.2.2"
|
||||
}
|
||||
}
|
||||
6
NewFile/src/postcss.config.js
Normal file
6
NewFile/src/postcss.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
69
NewFile/src/project.md
Normal file
69
NewFile/src/project.md
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# Project Configuration - TechShop Next.js App
|
||||
|
||||
This document records the exact technology stack, directory layouts, routing configuration, and implementation structures established for the TechShop electronics e-commerce platform.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Technology Stack
|
||||
- **Framework:** Next.js App Router (14.2)
|
||||
- **Library:** React 18
|
||||
- **Language:** TypeScript
|
||||
- **Styling:** Tailwind CSS (v3) + postcss
|
||||
- **Icons:** Lucide React (`lucide-react`)
|
||||
- **Animation:** Framer Motion (`framer-motion`)
|
||||
- **Carousels:** Swiper.js (`swiper`)
|
||||
- **Utilities:** `clsx` and `tailwind-merge` (CN class resolver helper)
|
||||
|
||||
---
|
||||
|
||||
## 📂 Folder Structure
|
||||
|
||||
The project has been laid out according to Next.js App Router requirements:
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/ # Next.js App Router files
|
||||
│ ├── layout.tsx # Root document layout (Google Fonts loader, Metadata SEO)
|
||||
│ ├── page.tsx # Root index route (renders AnnouncementBar, layouts, sections)
|
||||
│ └── globals.css # Core global stylesheet with Tailwind directives
|
||||
├── components/ # Reusable, decoupled Client & Server components
|
||||
│ ├── RatingStars.tsx
|
||||
│ ├── CountdownTimer.tsx
|
||||
│ ├── ProductCard.tsx
|
||||
│ ├── SearchBar.tsx
|
||||
│ ├── CategorySidebar.tsx
|
||||
│ ├── MobileDrawer.tsx
|
||||
│ ├── FloatingButtons.tsx
|
||||
│ └── LiveToast.tsx
|
||||
├── layout/ # Structural wrappers for top/bottom margins
|
||||
│ ├── AnnouncementBar.tsx
|
||||
│ ├── Header.tsx
|
||||
│ ├── Navbar.tsx
|
||||
│ └── Footer.tsx
|
||||
├── sections/ # Contextual homepage sections
|
||||
│ ├── HeroSection.tsx
|
||||
│ ├── PromoCardsSection.tsx
|
||||
│ ├── CategoriesSection.tsx
|
||||
│ ├── NewArrivalsSection.tsx
|
||||
│ ├── PromoBannersSection.tsx
|
||||
│ ├── HotDealsSection.tsx
|
||||
│ ├── RecentlyLaunchedSection.tsx
|
||||
│ ├── BrandsSection.tsx
|
||||
│ ├── UltimateTechSection.tsx
|
||||
│ ├── ArticlesSection.tsx
|
||||
│ ├── FeaturesSection.tsx
|
||||
│ └── NewsletterSection.tsx
|
||||
├── types/ # TypeScript interfaces
|
||||
│ └── index.ts
|
||||
└── utils/ # Helpers and datasets
|
||||
├── utils.ts
|
||||
└── mockData.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🌐 SEO & Accessibility Best Practices
|
||||
- **Heading Hierarchy:** One single `<h1>` tag per page inside the sticky header's logo link. Strategic `<h2>` headings for section triggers (e.g. New Arrivals, Ultimate Tech).
|
||||
- **Metadata Configuration:** Next.js native `Metadata` api exported inside [layout.tsx](file:///e:/ifixKart_monsterTemplate/src/app/layout.tsx) with custom titles, description, and keywords.
|
||||
- **Semantic HTML:** Implemented appropriate HTML5 structural wrappers: `<header>`, `<nav>`, `<main>`, `<section>`, `<article>`, and `<footer>` tags.
|
||||
- **Alt Labels:** Image components contain readable, standard alt properties matching product titles.
|
||||
106
NewFile/src/src/App.tsx
Normal file
106
NewFile/src/src/App.tsx
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { AnnouncementBar } from './layout/AnnouncementBar';
|
||||
import { Header } from './layout/Header';
|
||||
import { Navbar } from './layout/Navbar';
|
||||
import { Footer } from './layout/Footer';
|
||||
import { MobileDrawer } from './components/MobileDrawer';
|
||||
import { FloatingButtons } from './components/FloatingButtons';
|
||||
import { LiveToast } from './components/LiveToast';
|
||||
|
||||
// Sections
|
||||
import { HeroSection } from './sections/HeroSection';
|
||||
import { PromoCardsSection } from './sections/PromoCardsSection';
|
||||
import { CategoriesSection } from './sections/CategoriesSection';
|
||||
import { NewArrivalsSection } from './sections/NewArrivalsSection';
|
||||
import { PromoBannersSection } from './sections/PromoBannersSection';
|
||||
import { HotDealsSection } from './sections/HotDealsSection';
|
||||
import { RecentlyLaunchedSection } from './sections/RecentlyLaunchedSection';
|
||||
import { BrandsSection } from './sections/BrandsSection';
|
||||
import { UltimateTechSection } from './sections/UltimateTechSection';
|
||||
import { ArticlesSection } from './sections/ArticlesSection';
|
||||
import { FeaturesSection } from './sections/FeaturesSection';
|
||||
import { NewsletterSection } from './sections/NewsletterSection';
|
||||
|
||||
export default function App() {
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [wishlistCount, setWishlistCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const saved = localStorage.getItem('wishlist');
|
||||
if (saved) {
|
||||
setWishlistCount(JSON.parse(saved).length);
|
||||
} else {
|
||||
const initial: string[] = [];
|
||||
setWishlistCount(initial.length);
|
||||
localStorage.setItem('wishlist', JSON.stringify(initial));
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-bgLight text-textMain flex flex-col font-sans">
|
||||
{/* 1. Announcement Bar */}
|
||||
<AnnouncementBar />
|
||||
|
||||
{/* 2. Sticky Header */}
|
||||
<Header
|
||||
onMenuToggle={() => setDrawerOpen(true)}
|
||||
cartCount={0}
|
||||
wishlistCount={wishlistCount}
|
||||
/>
|
||||
|
||||
{/* 3. Navigation Menu */}
|
||||
<Navbar />
|
||||
|
||||
{/* Mobile Drawer (Responsive Hamburger Menu) */}
|
||||
<MobileDrawer isOpen={drawerOpen} onClose={() => setDrawerOpen(false)} />
|
||||
|
||||
{/* Main Content Sections */}
|
||||
<main className="flex-grow">
|
||||
{/* 4. Hero Section (Sidebar + Swiper Slider) */}
|
||||
<HeroSection />
|
||||
|
||||
{/* 5. Promotional Cards */}
|
||||
<PromoCardsSection />
|
||||
|
||||
{/* 6. Shop By Categories */}
|
||||
<CategoriesSection />
|
||||
|
||||
{/* 7. New Arrivals */}
|
||||
<NewArrivalsSection />
|
||||
|
||||
{/* 8. Promotional Banner Row */}
|
||||
<PromoBannersSection />
|
||||
|
||||
{/* 9 & 10. Hot Deals Card & Monthly Featured Products Grid */}
|
||||
<HotDealsSection />
|
||||
|
||||
{/* 11. Recently Launched Carousel */}
|
||||
<RecentlyLaunchedSection />
|
||||
|
||||
{/* 12. Popular Brands Logo Grid */}
|
||||
<BrandsSection />
|
||||
|
||||
{/* 13. Ultimate Tech Gadgets (Grid + Banner) */}
|
||||
<UltimateTechSection />
|
||||
|
||||
{/* 14. Latest Articles Blog Cards */}
|
||||
<ArticlesSection />
|
||||
|
||||
{/* 15. Service Trust Features */}
|
||||
<FeaturesSection />
|
||||
|
||||
{/* 16. Newsletter Box */}
|
||||
<NewsletterSection />
|
||||
</main>
|
||||
|
||||
{/* 17. Footer */}
|
||||
<Footer />
|
||||
|
||||
{/* Floating Buttons: Back-to-Top and Support Chat */}
|
||||
<FloatingButtons />
|
||||
|
||||
{/* Live Purchase notification toasts */}
|
||||
<LiveToast />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
72
NewFile/src/src/app/globals.css
Normal file
72
NewFile/src/src/app/globals.css
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Outfit:wght@300;400;500;600;700;800;900&display=swap');
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
body {
|
||||
@apply font-sans bg-[#F8F9FB] text-[#1E293B] antialiased;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom Swiper Styles */
|
||||
.swiper-pagination-bullet-active {
|
||||
background: #1976F3 !important;
|
||||
width: 24px !important;
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
|
||||
.swiper-button-next,
|
||||
.swiper-button-prev {
|
||||
color: #1976F3 !important;
|
||||
background: white;
|
||||
width: 40px !important;
|
||||
height: 40px !important;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.swiper-button-next::after,
|
||||
.swiper-button-prev::after {
|
||||
font-size: 16px !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Hide scrollbars but keep scrolling */
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
/* Direct font overrides */
|
||||
body, p, span, a, input, select, button {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6, .font-outfit {
|
||||
font-family: 'Outfit', sans-serif;
|
||||
}
|
||||
|
||||
/* Force container max-width to 1400px on large displays */
|
||||
@media (min-width: 1400px) {
|
||||
.container {
|
||||
max-w-screen-2xl: 1400px;
|
||||
max-width: 1400px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Prevent browser autofill background color */
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:focus,
|
||||
input:-webkit-autofill:active {
|
||||
-webkit-box-shadow: 0 0 0 30px white inset !important;
|
||||
-webkit-text-fill-color: #1E293B !important;
|
||||
}
|
||||
32
NewFile/src/src/app/layout.tsx
Normal file
32
NewFile/src/src/app/layout.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import type { Metadata } from "next";
|
||||
import { Inter, Outfit } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-inter",
|
||||
});
|
||||
|
||||
const outfit = Outfit({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-outfit",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "TechShop - Premium Electronics Marketplace",
|
||||
description: "Shop for laptops, smartphones, headphones, smartwatches, and chargers at TechShop, the modern electronics marketplace.",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" className={`${inter.variable} ${outfit.variable}`}>
|
||||
<body className="bg-[#F8F9FB] text-[#1E293B] antialiased">
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
146
NewFile/src/src/app/page.tsx
Normal file
146
NewFile/src/src/app/page.tsx
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { AnnouncementBar } from '../layout/AnnouncementBar';
|
||||
import { Header } from '../layout/Header';
|
||||
import { Navbar } from '../layout/Navbar';
|
||||
import { Footer } from '../layout/Footer';
|
||||
import { MobileDrawer } from '../components/MobileDrawer';
|
||||
import { FloatingButtons } from '../components/FloatingButtons';
|
||||
import { LiveToast } from '../components/LiveToast';
|
||||
import { QuickViewModal } from '../components/QuickViewModal';
|
||||
import { WishlistModal } from '../components/WishlistModal';
|
||||
import { Product } from '../types';
|
||||
|
||||
// Sections
|
||||
import { HeroSection } from '../sections/HeroSection';
|
||||
import { PromoCardsSection } from '../sections/PromoCardsSection';
|
||||
import { CategoriesSection } from '../sections/CategoriesSection';
|
||||
import { NewArrivalsSection } from '../sections/NewArrivalsSection';
|
||||
import { PromoBannersSection } from '../sections/PromoBannersSection';
|
||||
import { HotDealsSection } from '../sections/HotDealsSection';
|
||||
import { RecentlyLaunchedSection } from '../sections/RecentlyLaunchedSection';
|
||||
import { BrandsSection } from '../sections/BrandsSection';
|
||||
import { UltimateTechSection } from '../sections/UltimateTechSection';
|
||||
import { ArticlesSection } from '../sections/ArticlesSection';
|
||||
import { FeaturesSection } from '../sections/FeaturesSection';
|
||||
|
||||
export default function Home() {
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [wishlistCount, setWishlistCount] = useState(0);
|
||||
const [quickViewProduct, setQuickViewProduct] = useState<Product | null>(null);
|
||||
const [wishlistModalOpen, setWishlistModalOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const updateWishlist = () => {
|
||||
const saved = localStorage.getItem('wishlist');
|
||||
if (saved) {
|
||||
try {
|
||||
const list = JSON.parse(saved);
|
||||
setWishlistCount(Array.isArray(list) ? list.length : 0);
|
||||
} catch (e) {
|
||||
setWishlistCount(0);
|
||||
}
|
||||
} else {
|
||||
setWishlistCount(0);
|
||||
}
|
||||
};
|
||||
updateWishlist();
|
||||
|
||||
const handleQuickView = (e: Event) => {
|
||||
const customEvt = e as CustomEvent;
|
||||
if (customEvt.detail) {
|
||||
setQuickViewProduct(customEvt.detail);
|
||||
}
|
||||
};
|
||||
const handleWishlistModal = () => setWishlistModalOpen(true);
|
||||
|
||||
window.addEventListener('wishlistUpdated', updateWishlist);
|
||||
window.addEventListener('openQuickView', handleQuickView);
|
||||
window.addEventListener('openWishlistModal', handleWishlistModal);
|
||||
return () => {
|
||||
window.removeEventListener('wishlistUpdated', updateWishlist);
|
||||
window.removeEventListener('openQuickView', handleQuickView);
|
||||
window.removeEventListener('openWishlistModal', handleWishlistModal);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-bgLight text-textMain flex flex-col font-sans">
|
||||
{/* 1. Announcement Bar */}
|
||||
<AnnouncementBar />
|
||||
|
||||
{/* 2. Sticky Header */}
|
||||
<Header
|
||||
onMenuToggle={() => setDrawerOpen(true)}
|
||||
cartCount={0}
|
||||
wishlistCount={wishlistCount}
|
||||
/>
|
||||
|
||||
{/* 3. Navigation Menu */}
|
||||
<Navbar />
|
||||
|
||||
{/* Mobile Drawer (Responsive Hamburger Menu) */}
|
||||
<MobileDrawer isOpen={drawerOpen} onClose={() => setDrawerOpen(false)} />
|
||||
|
||||
{/* Main Content Sections */}
|
||||
<main className="flex-grow">
|
||||
{/* 4. Hero Section (Sidebar + Swiper Slider) */}
|
||||
<HeroSection />
|
||||
|
||||
{/* 5. Promotional Cards */}
|
||||
<PromoCardsSection />
|
||||
|
||||
{/* 6. Shop By Categories */}
|
||||
<CategoriesSection />
|
||||
|
||||
{/* 7. New Arrivals */}
|
||||
<NewArrivalsSection />
|
||||
|
||||
{/* 8. Promotional Banner Row */}
|
||||
<PromoBannersSection />
|
||||
|
||||
{/* 9 & 10. Hot Deals Card & Monthly Featured Products Grid */}
|
||||
<HotDealsSection />
|
||||
|
||||
{/* 11. Recently Launched Carousel */}
|
||||
<RecentlyLaunchedSection />
|
||||
|
||||
{/* 12. Popular Brands Logo Grid */}
|
||||
<BrandsSection />
|
||||
|
||||
{/* 13. Ultimate Tech Gadgets (Grid + Banner) */}
|
||||
<UltimateTechSection />
|
||||
|
||||
{/* 14. Latest Articles Blog Cards */}
|
||||
<ArticlesSection />
|
||||
|
||||
{/* 15. Service Trust Features */}
|
||||
<FeaturesSection />
|
||||
|
||||
</main>
|
||||
|
||||
{/* 17. Footer */}
|
||||
<Footer />
|
||||
|
||||
{/* Floating Buttons: Back-to-Top and Support Chat */}
|
||||
<FloatingButtons />
|
||||
|
||||
{/* Live Purchase notification toasts */}
|
||||
<LiveToast />
|
||||
|
||||
{/* Quick View Modal Popup */}
|
||||
{quickViewProduct && (
|
||||
<QuickViewModal
|
||||
product={quickViewProduct}
|
||||
onClose={() => setQuickViewProduct(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Wishlist Modal Popup */}
|
||||
{wishlistModalOpen && (
|
||||
<WishlistModal onClose={() => setWishlistModalOpen(false)} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
633
NewFile/src/src/app/product/[id]/page.tsx
Normal file
633
NewFile/src/src/app/product/[id]/page.tsx
Normal file
|
|
@ -0,0 +1,633 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { Header } from '../../../layout/Header';
|
||||
import { Navbar } from '../../../layout/Navbar';
|
||||
import { AnnouncementBar } from '../../../layout/AnnouncementBar';
|
||||
import { Footer } from '../../../layout/Footer';
|
||||
import { MobileDrawer } from '../../../components/MobileDrawer';
|
||||
import { RatingStars } from '../../../components/RatingStars';
|
||||
import { ProductCard } from '../../../components/ProductCard';
|
||||
import { products } from '../../../utils/mockData';
|
||||
import { Product } from '../../../types';
|
||||
import {
|
||||
Heart,
|
||||
Eye,
|
||||
ShoppingCart,
|
||||
ChevronUp,
|
||||
ChevronDown,
|
||||
Flame,
|
||||
CheckCircle2,
|
||||
Share2,
|
||||
HelpCircle,
|
||||
GitCompare,
|
||||
ShieldCheck,
|
||||
Coins,
|
||||
Truck,
|
||||
ChevronRight
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function ProductDetailPage() {
|
||||
const params = useParams();
|
||||
const productId = (params?.id as string) || 'prod-2';
|
||||
|
||||
// State management
|
||||
const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false);
|
||||
const [cartCount, setCartCount] = useState(0);
|
||||
const [wishlist, setWishlist] = useState<string[]>([]);
|
||||
const [activeTab, setActiveTab] = useState<'desc' | 'addInfo' | 'reviews' | 'sizeChart' | 'imageTab' | 'shipping'>('desc');
|
||||
const [quantity, setQuantity] = useState(1);
|
||||
const [selectedColorIndex, setSelectedColorIndex] = useState<number | null>(0);
|
||||
const [showStickyBar, setShowStickyBar] = useState(false);
|
||||
|
||||
// Magnifier Lens Zoom state
|
||||
const [zoomPos, setZoomPos] = useState({ x: 0, y: 0 });
|
||||
const [isZooming, setIsZooming] = useState(false);
|
||||
const imageRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Find product by id or fallback to Apple HomePod
|
||||
const currentProduct: Product =
|
||||
products.find((p: Product) => p.id === productId) || products[1] || products[0];
|
||||
|
||||
// Gallery image list
|
||||
const colorGallery = [
|
||||
currentProduct.image,
|
||||
currentProduct.imageHover || currentProduct.image,
|
||||
'https://images.unsplash.com/photo-1543512214-318c7553f230?w=500&auto=format&fit=crop&q=60',
|
||||
'https://images.unsplash.com/photo-1588449668365-d15e397f6787?w=500&auto=format&fit=crop&q=60',
|
||||
'https://images.unsplash.com/photo-1508685096489-7aacd43bd3b1?w=500&auto=format&fit=crop&q=60',
|
||||
];
|
||||
|
||||
const [selectedImage, setSelectedImage] = useState(colorGallery[0]);
|
||||
|
||||
useEffect(() => {
|
||||
// Load initial wishlist and cart count from localStorage
|
||||
const savedWishlist = localStorage.getItem('wishlist');
|
||||
if (savedWishlist) setWishlist(JSON.parse(savedWishlist));
|
||||
|
||||
const savedCart = localStorage.getItem('cart');
|
||||
if (savedCart) {
|
||||
const parsed = JSON.parse(savedCart);
|
||||
setCartCount(Array.isArray(parsed) ? parsed.length : Number(parsed));
|
||||
}
|
||||
|
||||
// Scroll listener for sticky bottom bar
|
||||
const handleScroll = () => {
|
||||
if (window.scrollY > 500) {
|
||||
setShowStickyBar(true);
|
||||
} else {
|
||||
setShowStickyBar(false);
|
||||
}
|
||||
};
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
const isWishlisted = wishlist.includes(currentProduct.id);
|
||||
|
||||
const toggleWishlist = () => {
|
||||
let updated: string[];
|
||||
if (isWishlisted) {
|
||||
updated = wishlist.filter((id) => id !== currentProduct.id);
|
||||
} else {
|
||||
updated = [...wishlist, currentProduct.id];
|
||||
}
|
||||
setWishlist(updated);
|
||||
localStorage.setItem('wishlist', JSON.stringify(updated));
|
||||
};
|
||||
|
||||
const handleAddToCart = () => {
|
||||
const newCount = cartCount + quantity;
|
||||
setCartCount(newCount);
|
||||
localStorage.setItem('cart', JSON.stringify(newCount));
|
||||
alert(`Added ${quantity} x "${currentProduct.name}" to cart!`);
|
||||
};
|
||||
|
||||
// Image Magnifier Mouse Handler
|
||||
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!imageRef.current) return;
|
||||
const { left, top, width, height } = imageRef.current.getBoundingClientRect();
|
||||
const x = ((e.clientX - left) / width) * 100;
|
||||
const y = ((e.clientY - top) / height) * 100;
|
||||
setZoomPos({ x, y });
|
||||
};
|
||||
|
||||
const isRange = currentProduct.name.includes('Ceptics') || currentProduct.name.includes('Lenovo');
|
||||
const priceDisplay = isRange ? `$300 – $1,500` : `$${currentProduct.price.toLocaleString()}`;
|
||||
|
||||
// Comparison products
|
||||
const comparisonProducts = products.slice(1, 6);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white font-sans text-gray-800 antialiased pb-20">
|
||||
{/* Announcement & Main Header */}
|
||||
<AnnouncementBar />
|
||||
<Header
|
||||
onMenuToggle={() => setMobileDrawerOpen(true)}
|
||||
cartCount={cartCount}
|
||||
wishlistCount={wishlist.length}
|
||||
/>
|
||||
<Navbar />
|
||||
|
||||
<MobileDrawer
|
||||
isOpen={mobileDrawerOpen}
|
||||
onClose={() => setMobileDrawerOpen(false)}
|
||||
/>
|
||||
|
||||
{/* Breadcrumb Navigation */}
|
||||
<div className="bg-[#F8F9FA] border-b border-gray-150 py-3 text-xs text-gray-500">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 flex items-center gap-2">
|
||||
<Link href="/" className="hover:text-primary transition-colors">Home</Link>
|
||||
<ChevronRight size={12} />
|
||||
<Link href="/shop" className="hover:text-primary transition-colors">Shop</Link>
|
||||
<ChevronRight size={12} />
|
||||
<span className="text-gray-400 capitalize">{currentProduct.category}</span>
|
||||
<ChevronRight size={12} />
|
||||
<span className="text-gray-900 font-semibold truncate max-w-[250px]">{currentProduct.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Product Showcase Section */}
|
||||
<main className="container mx-auto px-4 sm:px-6 lg:px-8 py-8 md:py-12">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 lg:gap-12 items-start">
|
||||
|
||||
{/* Left Column: Image Gallery & Zoom Lens */}
|
||||
<div className="lg:col-span-6 flex flex-col md:flex-row gap-4">
|
||||
|
||||
{/* Vertical Thumbnail List */}
|
||||
<div className="flex md:flex-col items-center gap-2 order-2 md:order-1 flex-shrink-0 justify-center">
|
||||
<button className="text-gray-400 hover:text-primary hidden md:block">
|
||||
<ChevronUp size={16} />
|
||||
</button>
|
||||
{colorGallery.map((img, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => {
|
||||
setSelectedImage(img);
|
||||
setSelectedColorIndex(idx);
|
||||
}}
|
||||
className={`w-16 h-16 rounded-xl border p-1 transition-all overflow-hidden flex items-center justify-center bg-[#F8F9FB] ${
|
||||
selectedImage === img ? 'border-[#0073EC] ring-2 ring-[#0073EC]/20' : 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<img src={img} alt="" className="max-h-full max-w-full object-contain" />
|
||||
</button>
|
||||
))}
|
||||
<button className="text-gray-400 hover:text-primary hidden md:block">
|
||||
<ChevronDown size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Main Stage Image with Magnifier Lens */}
|
||||
<div className="flex-1 order-1 md:order-2">
|
||||
<div
|
||||
ref={imageRef}
|
||||
onMouseEnter={() => setIsZooming(true)}
|
||||
onMouseLeave={() => setIsZooming(false)}
|
||||
onMouseMove={handleMouseMove}
|
||||
className="relative bg-white border border-gray-150 rounded-2xl p-6 h-[420px] md:h-[480px] flex items-center justify-center overflow-hidden cursor-crosshair group select-none shadow-xs"
|
||||
>
|
||||
<img
|
||||
src={selectedImage}
|
||||
alt={currentProduct.name}
|
||||
className="max-h-full max-w-full object-contain transition-transform duration-200"
|
||||
/>
|
||||
|
||||
{/* Magnifier Lens Window overlay */}
|
||||
{isZooming && (
|
||||
<div
|
||||
className="absolute inset-0 pointer-events-none z-30 bg-white"
|
||||
style={{
|
||||
backgroundImage: `url(${selectedImage})`,
|
||||
backgroundPosition: `${zoomPos.x}% ${zoomPos.y}%`,
|
||||
backgroundSize: '250%',
|
||||
backgroundRepeat: 'no-repeat'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Trust Badges under Main Image */}
|
||||
<div className="grid grid-cols-3 gap-2 mt-4">
|
||||
<div className="bg-[#F6F8FE] border border-gray-150 rounded-xl p-3 flex items-center justify-center gap-2 text-xs font-semibold text-gray-700">
|
||||
<ShieldCheck size={16} className="text-[#0073EC]" />
|
||||
<span>101% Original</span>
|
||||
</div>
|
||||
<div className="bg-[#F6F8FE] border border-gray-150 rounded-xl p-3 flex items-center justify-center gap-2 text-xs font-semibold text-gray-700">
|
||||
<Coins size={16} className="text-[#0073EC]" />
|
||||
<span>Lowest Price</span>
|
||||
</div>
|
||||
<div className="bg-[#F6F8FE] border border-gray-150 rounded-xl p-3 flex items-center justify-center gap-2 text-xs font-semibold text-gray-700">
|
||||
<Truck size={16} className="text-[#0073EC]" />
|
||||
<span>Free Shipping</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Product Info & Purchase Form */}
|
||||
<div className="lg:col-span-6 text-left">
|
||||
{/* Brand Tag */}
|
||||
<div className="text-xs font-bold text-[#0073EC] uppercase tracking-wider mb-1">
|
||||
Brand: {currentProduct.brand || 'Gold'}
|
||||
</div>
|
||||
|
||||
{/* Product Title */}
|
||||
<h1 className="text-2xl md:text-3xl font-extrabold text-gray-900 tracking-tight leading-tight mb-3">
|
||||
{currentProduct.name}
|
||||
</h1>
|
||||
|
||||
{/* Price & Rating */}
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<span className="text-2xl font-black text-gray-900">{priceDisplay}</span>
|
||||
<div className="flex items-center gap-1.5 text-xs text-gray-500 border-l border-gray-200 pl-4">
|
||||
<RatingStars rating={currentProduct.rating} size={14} />
|
||||
<span className="font-semibold">(1 review)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Urgency Selling Fast Banner */}
|
||||
<div className="inline-flex items-center gap-2 bg-red-50 text-red-500 border border-red-100 rounded-lg px-3 py-1.5 text-xs font-bold mb-5">
|
||||
<Flame size={14} className="fill-red-500 animate-pulse" />
|
||||
<span>Selling fast! Over 18 people have in their cart</span>
|
||||
</div>
|
||||
|
||||
{/* Bullet Point Specifications */}
|
||||
<ul className="text-xs text-gray-600 space-y-1.5 mb-6 list-disc list-inside font-medium">
|
||||
<li>Gps+cellular</li>
|
||||
<li>Noise Cancellation</li>
|
||||
<li>Google Fast Pair</li>
|
||||
</ul>
|
||||
|
||||
{/* Color Swatch Options */}
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-xs font-bold text-gray-700">Color</span>
|
||||
{selectedColorIndex !== null && (
|
||||
<button
|
||||
onClick={() => setSelectedColorIndex(null)}
|
||||
className="text-xs text-gray-400 hover:text-red-500 transition-colors"
|
||||
>
|
||||
✕ Clear
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{colorGallery.slice(0, 5).map((img, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => {
|
||||
setSelectedImage(img);
|
||||
setSelectedColorIndex(idx);
|
||||
}}
|
||||
className={`w-10 h-10 rounded-lg border p-1 transition-all overflow-hidden flex items-center justify-center bg-[#F8F9FB] ${
|
||||
selectedColorIndex === idx ? 'border-[#0073EC] ring-2 ring-[#0073EC]/20' : 'border-gray-200'
|
||||
}`}
|
||||
>
|
||||
<img src={img} alt="" className="max-h-full max-w-full object-contain" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quantity Selector & Action Buttons */}
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 mb-4">
|
||||
{/* Quantity Counter */}
|
||||
<div className="flex items-center border border-gray-200 rounded-xl bg-[#F8F9FB] px-3 py-2.5 justify-between sm:w-32">
|
||||
<button
|
||||
onClick={() => setQuantity(Math.max(1, quantity - 1))}
|
||||
className="text-gray-500 hover:text-black font-extrabold text-sm px-2 select-none"
|
||||
>
|
||||
–
|
||||
</button>
|
||||
<span className="font-bold text-sm text-gray-900">{quantity}</span>
|
||||
<button
|
||||
onClick={() => setQuantity(quantity + 1)}
|
||||
className="text-gray-500 hover:text-black font-extrabold text-sm px-2 select-none"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Add to Cart Button */}
|
||||
<button
|
||||
onClick={handleAddToCart}
|
||||
className="flex-1 bg-[#0073EC] hover:bg-[#005FC8] text-white font-extrabold text-xs uppercase tracking-wider py-3.5 px-6 rounded-xl transition-all shadow-md active:scale-[0.99] flex items-center justify-center gap-2 cursor-pointer"
|
||||
>
|
||||
<ShoppingCart size={15} className="stroke-[2.5]" />
|
||||
Add To Cart
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Buy Now Button */}
|
||||
<button
|
||||
onClick={() => alert(`Proceeding to checkout for ${currentProduct.name}`)}
|
||||
className="w-full bg-[#0073EC] hover:bg-[#005FC8] text-white font-extrabold text-xs uppercase tracking-wider py-3.5 px-6 rounded-xl transition-all shadow-md active:scale-[0.99] mb-6 cursor-pointer"
|
||||
>
|
||||
Buy Now
|
||||
</button>
|
||||
|
||||
{/* Action Links Row */}
|
||||
<div className="flex items-center gap-6 border-b border-gray-150 pb-6 mb-6 text-xs font-semibold text-gray-600">
|
||||
<button className="flex items-center gap-1.5 hover:text-[#0073EC] transition-colors">
|
||||
<GitCompare size={14} />
|
||||
<span>Compare</span>
|
||||
</button>
|
||||
<button onClick={toggleWishlist} className={`flex items-center gap-1.5 transition-colors ${isWishlisted ? 'text-[#0073EC]' : 'hover:text-[#0073EC]'}`}>
|
||||
<Heart size={14} className={isWishlisted ? 'fill-[#0073EC]' : ''} />
|
||||
<span>Wishlist</span>
|
||||
</button>
|
||||
<button className="flex items-center gap-1.5 hover:text-[#0073EC] transition-colors">
|
||||
<HelpCircle size={14} />
|
||||
<span>Ask Us</span>
|
||||
</button>
|
||||
<button className="flex items-center gap-1.5 hover:text-[#0073EC] transition-colors">
|
||||
<Share2 size={14} />
|
||||
<span>Share</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Live Viewers Indicator */}
|
||||
<div className="flex items-center gap-2 text-xs font-bold text-gray-800 mb-4">
|
||||
<Eye size={15} className="text-gray-500" />
|
||||
<span>26 people are viewing this right now</span>
|
||||
</div>
|
||||
|
||||
{/* Shipping & Returns Details */}
|
||||
<div className="space-y-2 text-xs text-gray-600 font-medium mb-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle2 size={14} className="text-emerald-500" />
|
||||
<span><strong>Estimated Delivery :</strong> Up to 4 business days</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle2 size={14} className="text-emerald-500" />
|
||||
<span><strong>Free Shipping & Returns :</strong> On all orders over $200</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Safe Checkout Badges */}
|
||||
<div className="bg-[#F8F9FB] border border-gray-150 rounded-xl p-4 text-center">
|
||||
<p className="text-[11px] font-bold text-gray-500 uppercase tracking-wider mb-2">
|
||||
Guaranteed Safe And Secure Checkout
|
||||
</p>
|
||||
<div className="flex items-center justify-center gap-3 text-xs font-extrabold text-gray-700">
|
||||
<span className="px-2 py-1 bg-white rounded border border-gray-200">VISA</span>
|
||||
<span className="px-2 py-1 bg-white rounded border border-gray-200">MC</span>
|
||||
<span className="px-2 py-1 bg-white rounded border border-gray-200">AMEX</span>
|
||||
<span className="px-2 py-1 bg-white rounded border border-gray-200">DISCOVER</span>
|
||||
<span className="px-2 py-1 bg-white rounded border border-gray-200">PayPal</span>
|
||||
<span className="px-2 py-1 bg-white rounded border border-gray-200">Pay</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Tabs & Detailed Specifications Section */}
|
||||
<section className="container mx-auto px-4 sm:px-6 lg:px-8 my-12">
|
||||
<div className="border border-gray-150 rounded-2xl overflow-hidden bg-white shadow-xs">
|
||||
|
||||
{/* Tab Navigation Header */}
|
||||
<div className="flex items-center gap-2 border-b border-gray-150 px-6 pt-4 bg-[#F8F9FB] overflow-x-auto">
|
||||
<button
|
||||
onClick={() => setActiveTab('desc')}
|
||||
className={`pb-4 text-xs font-extrabold px-3 transition-colors border-b-2 whitespace-nowrap cursor-pointer ${
|
||||
activeTab === 'desc' ? 'border-[#0073EC] text-[#0073EC]' : 'border-transparent text-gray-500 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
Description
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('addInfo')}
|
||||
className={`pb-4 text-xs font-extrabold px-3 transition-colors border-b-2 whitespace-nowrap cursor-pointer ${
|
||||
activeTab === 'addInfo' ? 'border-[#0073EC] text-[#0073EC]' : 'border-transparent text-gray-500 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
Additional information
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('reviews')}
|
||||
className={`pb-4 text-xs font-extrabold px-3 transition-colors border-b-2 whitespace-nowrap cursor-pointer ${
|
||||
activeTab === 'reviews' ? 'border-[#0073EC] text-[#0073EC]' : 'border-transparent text-gray-500 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
Reviews(1)
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('sizeChart')}
|
||||
className={`pb-4 text-xs font-extrabold px-3 transition-colors border-b-2 whitespace-nowrap cursor-pointer ${
|
||||
activeTab === 'sizeChart' ? 'border-[#0073EC] text-[#0073EC]' : 'border-transparent text-gray-500 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
Size Chart
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('imageTab')}
|
||||
className={`pb-4 text-xs font-extrabold px-3 transition-colors border-b-2 whitespace-nowrap cursor-pointer ${
|
||||
activeTab === 'imageTab' ? 'border-[#0073EC] text-[#0073EC]' : 'border-transparent text-gray-500 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
Image Tab
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('shipping')}
|
||||
className={`pb-4 text-xs font-extrabold px-3 transition-colors border-b-2 whitespace-nowrap cursor-pointer ${
|
||||
activeTab === 'shipping' ? 'border-[#0073EC] text-[#0073EC]' : 'border-transparent text-gray-500 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
Shipping & Return
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tab Content Box */}
|
||||
<div className="p-6 md:p-8 text-left">
|
||||
{(activeTab === 'desc' || activeTab === 'addInfo') && (
|
||||
<div className="space-y-6 text-xs text-gray-700">
|
||||
|
||||
{/* GENERAL INFORMATION */}
|
||||
<div>
|
||||
<h4 className="text-xs font-extrabold text-[#0073EC] uppercase tracking-wider mb-3 bg-gray-50 p-2 border-l-4 border-[#0073EC]">
|
||||
GENERAL INFORMATION
|
||||
</h4>
|
||||
<table className="w-full border border-gray-150 divide-y divide-gray-150 text-left">
|
||||
<tbody>
|
||||
<tr className="bg-white">
|
||||
<td className="p-3 font-semibold text-gray-900 w-1/3 border-r border-gray-150">Model</td>
|
||||
<td className="p-3 text-gray-600">HomePod 2nd Generation</td>
|
||||
</tr>
|
||||
<tr className="bg-gray-50/50">
|
||||
<td className="p-3 font-semibold text-gray-900 w-1/3 border-r border-gray-150">Wireless Connectivity</td>
|
||||
<td className="p-3 text-gray-600">Bluetooth, Wi-Fi 802.11n</td>
|
||||
</tr>
|
||||
<tr className="bg-white">
|
||||
<td className="p-3 font-semibold text-gray-900 w-1/3 border-r border-gray-150">Brand</td>
|
||||
<td className="p-3 text-gray-600">{currentProduct.brand || 'Apple'}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* PHYSICAL DIMENSIONS */}
|
||||
<div>
|
||||
<h4 className="text-xs font-extrabold text-[#0073EC] uppercase tracking-wider mb-3 bg-gray-50 p-2 border-l-4 border-[#0073EC]">
|
||||
PHYSICAL DIMENSIONS
|
||||
</h4>
|
||||
<table className="w-full border border-gray-150 divide-y divide-gray-150 text-left">
|
||||
<tbody>
|
||||
<tr className="bg-white">
|
||||
<td className="p-3 font-semibold text-gray-900 w-1/3 border-r border-gray-150">Colour</td>
|
||||
<td className="p-3 text-gray-600">White / Midnight</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* ADDITIONAL FEATURES */}
|
||||
<div>
|
||||
<h4 className="text-xs font-extrabold text-[#0073EC] uppercase tracking-wider mb-3 bg-gray-50 p-2 border-l-4 border-[#0073EC]">
|
||||
ADDITIONAL FEATURES
|
||||
</h4>
|
||||
<ul className="list-disc list-inside space-y-1 text-gray-600 pl-2">
|
||||
<li>Touch Accommodations VoiceOver Home app and HomeKit Intercom Transcriptions</li>
|
||||
<li>Compatibility: iPhone 14 Pro Max, iPhone 14 Pro, iPhone 14 Plus, iPhone 14</li>
|
||||
<li>iPhone 13 Pro Max, iPhone 13 Pro, iPhone 13, iPhone 13 mini</li>
|
||||
<li>iPhone 12 Pro Max, iPhone 12 Pro</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'reviews' && (
|
||||
<div className="text-xs text-gray-600 space-y-4">
|
||||
<div className="flex items-center gap-3 p-4 bg-gray-50 rounded-xl border border-gray-150">
|
||||
<RatingStars rating={5} size={14} />
|
||||
<div>
|
||||
<p className="font-bold text-gray-900">John Doe — July 20, 2026</p>
|
||||
<p className="text-gray-600 mt-1">Excellent sound quality and fast delivery!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'shipping' && (
|
||||
<div className="text-xs text-gray-600 space-y-2">
|
||||
<p><strong>Standard Shipping:</strong> 2-4 business days.</p>
|
||||
<p><strong>Return Policy:</strong> 30-day money back guarantee for any defective products.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Quick Comparison Section */}
|
||||
<section className="container mx-auto px-4 sm:px-6 lg:px-8 my-12 text-left">
|
||||
<h3 className="text-xl font-extrabold text-gray-900 tracking-tight mb-6">
|
||||
Quick Comparison
|
||||
</h3>
|
||||
<div className="border border-gray-150 rounded-2xl overflow-x-auto bg-white shadow-xs">
|
||||
<table className="w-full text-xs text-left border-collapse min-w-[700px]">
|
||||
<thead>
|
||||
<tr className="bg-[#F8F9FB] border-b border-gray-150">
|
||||
<th className="p-4 font-bold text-gray-500 w-32 border-r border-gray-150">Products</th>
|
||||
{comparisonProducts.map((prod: Product) => (
|
||||
<th key={prod.id} className="p-4 font-bold text-gray-900 border-r border-gray-150 last:border-r-0 min-w-[160px]">
|
||||
<p className="line-clamp-2">{prod.name}</p>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-150">
|
||||
<tr>
|
||||
<td className="p-4 font-bold text-gray-500 bg-[#F8F9FB] border-r border-gray-150">Image</td>
|
||||
{comparisonProducts.map((prod: Product) => (
|
||||
<td key={prod.id} className="p-4 border-r border-gray-150 last:border-r-0 text-center">
|
||||
<div className="w-24 h-24 mx-auto p-2 bg-[#F8F9FB] rounded-lg flex items-center justify-center">
|
||||
<img src={prod.image} alt={prod.name} className="max-h-full max-w-full object-contain" />
|
||||
</div>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-4 font-bold text-gray-500 bg-[#F8F9FB] border-r border-gray-150">Rating</td>
|
||||
{comparisonProducts.map((prod: Product) => (
|
||||
<td key={prod.id} className="p-4 border-r border-gray-150 last:border-r-0">
|
||||
<RatingStars rating={prod.rating} size={12} />
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-4 font-bold text-gray-500 bg-[#F8F9FB] border-r border-gray-150">Price</td>
|
||||
{comparisonProducts.map((prod: Product) => (
|
||||
<td key={prod.id} className="p-4 font-extrabold text-gray-900 border-r border-gray-150 last:border-r-0">
|
||||
${prod.price}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-4 font-bold text-gray-500 bg-[#F8F9FB] border-r border-gray-150">Add to cart</td>
|
||||
{comparisonProducts.map((prod: Product) => (
|
||||
<td key={prod.id} className="p-4 border-r border-gray-150 last:border-r-0">
|
||||
<button
|
||||
onClick={() => alert(`Added ${prod.name} to cart!`)}
|
||||
className="w-full bg-[#0073EC] hover:bg-[#005FC8] text-white text-[11px] font-bold py-2.5 px-3 rounded-lg transition-colors"
|
||||
>
|
||||
Select Options
|
||||
</button>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Related Products Grid Section */}
|
||||
<section className="container mx-auto px-4 sm:px-6 lg:px-8 my-12 text-left">
|
||||
<h3 className="text-xl font-extrabold text-gray-900 tracking-tight mb-6">
|
||||
Related products
|
||||
</h3>
|
||||
<div className="bg-white border-t border-l border-gray-200 rounded-none shadow-sm overflow-hidden grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 lg:grid-cols-5">
|
||||
{products.slice(2, 7).map((prod: Product) => (
|
||||
<ProductCard key={prod.id} product={prod} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Sticky Bottom Purchase Bar */}
|
||||
{showStickyBar && (
|
||||
<div className="fixed bottom-0 left-0 right-0 z-[100] bg-white border-t border-gray-200 py-3 px-4 sm:px-8 shadow-2xl transition-all duration-300 flex items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<img
|
||||
src={selectedImage}
|
||||
alt=""
|
||||
className="w-10 h-10 object-contain p-1 bg-gray-50 border border-gray-200 rounded-lg flex-shrink-0"
|
||||
/>
|
||||
<p className="text-xs font-bold text-gray-900 truncate max-w-[280px] sm:max-w-[400px]">
|
||||
{currentProduct.name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm font-extrabold text-gray-900 hidden sm:inline-block">
|
||||
{priceDisplay}
|
||||
</span>
|
||||
<button
|
||||
onClick={handleAddToCart}
|
||||
className="bg-[#0073EC] hover:bg-[#005FC8] text-white font-extrabold text-xs uppercase tracking-wider py-2.5 px-5 rounded-xl transition-all shadow-sm cursor-pointer whitespace-nowrap"
|
||||
>
|
||||
Select Options
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Footer */}
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
900
NewFile/src/src/app/shop/page.tsx
Normal file
900
NewFile/src/src/app/shop/page.tsx
Normal file
|
|
@ -0,0 +1,900 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
Heart,
|
||||
Eye,
|
||||
ShoppingCart,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Grid,
|
||||
List,
|
||||
ChevronRight,
|
||||
Search,
|
||||
Star,
|
||||
X
|
||||
} from 'lucide-react';
|
||||
|
||||
import { AnnouncementBar } from '../../layout/AnnouncementBar';
|
||||
import { Header } from '../../layout/Header';
|
||||
import { Navbar } from '../../layout/Navbar';
|
||||
import { Footer } from '../../layout/Footer';
|
||||
import { MobileDrawer } from '../../components/MobileDrawer';
|
||||
import { FloatingButtons } from '../../components/FloatingButtons';
|
||||
import { LiveToast } from '../../components/LiveToast';
|
||||
import { QuickViewModal } from '../../components/QuickViewModal';
|
||||
import { WishlistModal } from '../../components/WishlistModal';
|
||||
|
||||
import { products as initialProducts } from '../../utils/mockData';
|
||||
import { Product } from '../../types';
|
||||
|
||||
export default function ShopPage() {
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [layout, setLayout] = useState<'grid' | 'list'>('grid');
|
||||
const [sortBy, setSortBy] = useState('default');
|
||||
const [wishlist, setWishlist] = useState<string[]>([]);
|
||||
const [quickViewProduct, setQuickViewProduct] = useState<Product | null>(null);
|
||||
const [wishlistModalOpen, setWishlistModalOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const saved = localStorage.getItem('wishlist');
|
||||
if (saved) {
|
||||
setWishlist(JSON.parse(saved));
|
||||
} else {
|
||||
const initial: string[] = [];
|
||||
setWishlist(initial);
|
||||
localStorage.setItem('wishlist', JSON.stringify(initial));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const toggleWishlist = (productId: string) => {
|
||||
setWishlist(prev => {
|
||||
const exists = prev.includes(productId);
|
||||
let updated;
|
||||
if (exists) {
|
||||
updated = prev.filter(id => id !== productId);
|
||||
} else {
|
||||
setWishlistModalOpen(true);
|
||||
updated = [...prev, productId];
|
||||
}
|
||||
localStorage.setItem('wishlist', JSON.stringify(updated));
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
// Filter States
|
||||
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
|
||||
const [selectedBrands, setSelectedBrands] = useState<string[]>([]);
|
||||
const [selectedHighlight, setSelectedHighlight] = useState<string>('All Products');
|
||||
const [selectedColor, setSelectedColor] = useState<string | null>(null);
|
||||
const [selectedPriceRange, setSelectedPriceRange] = useState<string>('All');
|
||||
const [selectedRating, setSelectedRating] = useState<number | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
|
||||
// Collapsible Sidebar States
|
||||
const [collapsedFilters, setCollapsedFilters] = useState({
|
||||
categories: false,
|
||||
brands: false,
|
||||
highlight: false,
|
||||
color: false,
|
||||
price: false,
|
||||
rating: false,
|
||||
});
|
||||
|
||||
const toggleCollapse = (filterKey: keyof typeof collapsedFilters) => {
|
||||
setCollapsedFilters(prev => ({
|
||||
...prev,
|
||||
[filterKey]: !prev[filterKey]
|
||||
}));
|
||||
};
|
||||
|
||||
// Sync category & search filters from URL if present
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const catParam = params.get('category');
|
||||
const layoutParam = params.get('layout');
|
||||
const searchParam = params.get('search');
|
||||
if (catParam) {
|
||||
setSelectedCategories([catParam]);
|
||||
}
|
||||
if (layoutParam === 'list' || layoutParam === 'grid') {
|
||||
setLayout(layoutParam);
|
||||
}
|
||||
if (searchParam) {
|
||||
setSearchQuery(searchParam);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Filter & Sort Logic
|
||||
const filteredProducts = initialProducts.filter(product => {
|
||||
// Search Filter
|
||||
if (searchQuery && !product.name.toLowerCase().includes(searchQuery.toLowerCase()) && !product.category.toLowerCase().includes(searchQuery.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Category Filter
|
||||
if (selectedCategories.length > 0 && !selectedCategories.includes(product.category)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Brand Filter
|
||||
if (selectedBrands.length > 0 && product.brand && !selectedBrands.includes(product.brand)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Highlight Filter
|
||||
if (selectedHighlight !== 'All Products') {
|
||||
if (selectedHighlight === 'Best Seller' && !product.isFeatured) return false;
|
||||
if (selectedHighlight === 'New Arrivals' && !product.isNewArrival) return false;
|
||||
if (selectedHighlight === 'Sale' && !product.discount) return false;
|
||||
if (selectedHighlight === 'Hot Items' && !product.isHot) return false;
|
||||
}
|
||||
|
||||
// Color Filter
|
||||
if (selectedColor && product.color !== selectedColor) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Price Filter
|
||||
if (selectedPriceRange !== 'All') {
|
||||
const price = product.price;
|
||||
if (selectedPriceRange === '$0-$400' && (price < 0 || price > 400)) return false;
|
||||
if (selectedPriceRange === '$400-$800' && (price < 400 || price > 800)) return false;
|
||||
if (selectedPriceRange === '$800-$1,200' && (price < 800 || price > 1200)) return false;
|
||||
if (selectedPriceRange === '$1,200-$1,650' && (price < 1200 || price > 1650)) return false; // Adjusted upper range to capture laptops
|
||||
if (selectedPriceRange === '$1,600+' && price < 1600) return false;
|
||||
}
|
||||
|
||||
// Rating Filter
|
||||
if (selectedRating && Math.floor(product.rating) !== selectedRating) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}).sort((a, b) => {
|
||||
if (sortBy === 'price-low') return a.price - b.price;
|
||||
if (sortBy === 'price-high') return b.price - a.price;
|
||||
if (sortBy === 'rating') return b.rating - a.rating;
|
||||
return 0; // default (no sorting)
|
||||
});
|
||||
|
||||
const handleCategoryChange = (category: string) => {
|
||||
setSelectedCategories(prev =>
|
||||
prev.includes(category)
|
||||
? prev.filter(c => c !== category)
|
||||
: [...prev, category]
|
||||
);
|
||||
};
|
||||
|
||||
const handleBrandChange = (brand: string) => {
|
||||
setSelectedBrands(prev =>
|
||||
prev.includes(brand)
|
||||
? prev.filter(b => b !== brand)
|
||||
: [...prev, brand]
|
||||
);
|
||||
};
|
||||
|
||||
const resetAllFilters = () => {
|
||||
setSelectedCategories([]);
|
||||
setSelectedBrands([]);
|
||||
setSelectedHighlight('All Products');
|
||||
setSelectedColor(null);
|
||||
setSelectedPriceRange('All');
|
||||
setSelectedRating(null);
|
||||
setSearchQuery('');
|
||||
};
|
||||
|
||||
const getRatingCount = (stars: number) => {
|
||||
return initialProducts.filter(p => Math.floor(p.rating) === stars).length;
|
||||
};
|
||||
|
||||
// Price label resolver (to match mockup price displays)
|
||||
const getProductPriceDisplay = (product: Product) => {
|
||||
if (product.name.includes('Airpods Pro')) return '$299 – $1,900';
|
||||
if (product.name.includes('Apple Watch')) return '$669 – $700';
|
||||
if (product.name.includes('Ceptics')) return '$300 – $1,500';
|
||||
if (product.name.includes('Bose Portable')) return '$139 – $190';
|
||||
if (product.name.includes('Lenovo IdeaPad')) return '$400 – $469';
|
||||
if (product.name.includes('Lenovo Tab')) return '$144 – $150';
|
||||
if (product.name.includes('LG 674 litres')) return '$85 – $299';
|
||||
if (product.name.includes('J.P.Gold')) return '$45 – $49';
|
||||
if (product.name.includes('Laview Home')) return '$79 – $99';
|
||||
if (product.name.includes('Jajot J2')) return `$${product.price.toLocaleString()}`;
|
||||
if (product.name.includes('HP LaserJet')) return `$${product.price.toLocaleString()}`;
|
||||
|
||||
return product.oldPrice
|
||||
? `$${product.price.toLocaleString()}`
|
||||
: `$${product.price.toLocaleString()}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8F9FB] text-[#1E293B] flex flex-col font-sans">
|
||||
<AnnouncementBar />
|
||||
<Header
|
||||
onMenuToggle={() => setDrawerOpen(true)}
|
||||
cartCount={0}
|
||||
wishlistCount={wishlist.length}
|
||||
onWishlistClick={() => setWishlistModalOpen(true)}
|
||||
/>
|
||||
<Navbar />
|
||||
<MobileDrawer isOpen={drawerOpen} onClose={() => setDrawerOpen(false)} />
|
||||
|
||||
{/* Main Container */}
|
||||
<main className="flex-grow pt-4 pb-12">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1400px]">
|
||||
|
||||
|
||||
|
||||
{/* Core Sidebar + Grid Layout Grid */}
|
||||
<div className="flex flex-col lg:flex-row gap-8 items-start">
|
||||
|
||||
{/* Sidebar Filters */}
|
||||
<aside className="w-full lg:w-[280px] flex-shrink-0 space-y-6">
|
||||
|
||||
{/* Category Filter */}
|
||||
<div className="bg-white rounded-2xl border border-gray-150 p-5 shadow-xs">
|
||||
<div
|
||||
onClick={() => toggleCollapse('categories')}
|
||||
className="flex items-center justify-between font-bold text-[14px] text-gray-800 cursor-pointer select-none pb-3 border-b border-gray-100"
|
||||
>
|
||||
<span>Shop By Categories</span>
|
||||
{collapsedFilters.categories ? <ChevronDown size={16} /> : <ChevronUp size={16} />}
|
||||
</div>
|
||||
{!collapsedFilters.categories && (
|
||||
<div className="pt-4 space-y-3">
|
||||
{[
|
||||
{ name: 'Our Store', count: 24 },
|
||||
{ name: 'Cameras', count: 5 },
|
||||
{ name: 'Chargers & Cables', count: 8 },
|
||||
{ name: 'Laptop & Computers', count: 6 },
|
||||
{ name: 'Phones', count: 6 },
|
||||
{ name: 'Smart Devices', count: 20 },
|
||||
{ name: 'TV & Speaker', count: 7 }
|
||||
].map(cat => (
|
||||
<label key={cat.name} className="flex items-center justify-between text-[13px] text-gray-600 cursor-pointer hover:text-primary transition-colors font-medium">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedCategories.includes(cat.name)}
|
||||
onChange={() => handleCategoryChange(cat.name)}
|
||||
className="w-4 h-4 rounded-md border-gray-305 text-primary focus:ring-primary cursor-pointer accent-primary"
|
||||
/>
|
||||
<span>{cat.name}</span>
|
||||
</div>
|
||||
<span className="text-gray-400">({cat.count})</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Brands Filter */}
|
||||
<div className="bg-white rounded-2xl border border-gray-150 p-5 shadow-xs">
|
||||
<div
|
||||
onClick={() => toggleCollapse('brands')}
|
||||
className="flex items-center justify-between font-bold text-[14px] text-gray-800 cursor-pointer select-none pb-3 border-b border-gray-100"
|
||||
>
|
||||
<span>Brands</span>
|
||||
{collapsedFilters.brands ? <ChevronDown size={16} /> : <ChevronUp size={16} />}
|
||||
</div>
|
||||
{!collapsedFilters.brands && (
|
||||
<div className="pt-4 space-y-3">
|
||||
{[
|
||||
'Creative',
|
||||
'Design',
|
||||
'Gallery',
|
||||
'Gold',
|
||||
'Highlight',
|
||||
'Modern',
|
||||
'Nature',
|
||||
'Sparker'
|
||||
].map(brand => (
|
||||
<label key={brand} className="flex items-center gap-2.5 text-[13px] text-gray-600 cursor-pointer hover:text-primary transition-colors font-medium font-outfit">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedBrands.includes(brand)}
|
||||
onChange={() => handleBrandChange(brand)}
|
||||
className="w-4 h-4 rounded-md border-gray-305 text-primary focus:ring-primary cursor-pointer accent-primary"
|
||||
/>
|
||||
<span>{brand}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Highlight Filter */}
|
||||
<div className="bg-white rounded-2xl border border-gray-150 p-5 shadow-xs">
|
||||
<div
|
||||
onClick={() => toggleCollapse('highlight')}
|
||||
className="flex items-center justify-between font-bold text-[14px] text-gray-800 cursor-pointer select-none pb-3 border-b border-gray-100"
|
||||
>
|
||||
<span>Highlight</span>
|
||||
{collapsedFilters.highlight ? <ChevronDown size={16} /> : <ChevronUp size={16} />}
|
||||
</div>
|
||||
{!collapsedFilters.highlight && (
|
||||
<div className="pt-4 space-y-3">
|
||||
{[
|
||||
'All Products',
|
||||
'Best Seller',
|
||||
'New Arrivals',
|
||||
'Sale',
|
||||
'Hot Items'
|
||||
].map(hl => (
|
||||
<button
|
||||
key={hl}
|
||||
onClick={() => setSelectedHighlight(hl)}
|
||||
className={`block text-left w-full text-[13px] py-1 transition-colors font-bold ${selectedHighlight === hl ? 'text-primary font-bold' : 'text-gray-600 hover:text-primary'
|
||||
}`}
|
||||
>
|
||||
{hl}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Filter By Color */}
|
||||
<div className="bg-white rounded-2xl border border-gray-150 p-5 shadow-xs">
|
||||
<div
|
||||
onClick={() => toggleCollapse('color')}
|
||||
className="flex items-center justify-between font-bold text-[14px] text-gray-800 cursor-pointer select-none pb-3 border-b border-gray-100"
|
||||
>
|
||||
<span>Filter by Color</span>
|
||||
{collapsedFilters.color ? <ChevronDown size={16} /> : <ChevronUp size={16} />}
|
||||
</div>
|
||||
{!collapsedFilters.color && (
|
||||
<div className="pt-4 flex flex-wrap gap-2.5">
|
||||
{[
|
||||
{ name: 'black', class: 'bg-black border border-black' },
|
||||
{ name: 'blue', class: 'bg-blue-600 border border-blue-600' },
|
||||
{ name: 'brown', class: 'bg-amber-800 border border-amber-800' },
|
||||
{ name: 'grey', class: 'bg-gray-500 border border-gray-500' },
|
||||
{ name: 'green', class: 'bg-emerald-600 border border-emerald-600' },
|
||||
{ name: 'orange', class: 'bg-orange-500 border border-orange-500' },
|
||||
{ name: 'red', class: 'bg-red-650 border border-red-650' },
|
||||
{ name: 'pink', class: 'bg-pink-400 border border-pink-400' },
|
||||
{ name: 'purple', class: 'bg-purple-600 border border-purple-600' },
|
||||
{ name: 'white', class: 'bg-white border border-gray-300' }
|
||||
].map(col => (
|
||||
<button
|
||||
key={col.name}
|
||||
onClick={() => setSelectedColor(selectedColor === col.name ? null : col.name)}
|
||||
className={`w-6 h-6 rounded-full cursor-pointer transition-all ${col.class} ${selectedColor === col.name ? 'ring-2 ring-primary ring-offset-2 scale-110' : 'hover:scale-105'
|
||||
}`}
|
||||
title={col.name}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Price Filter */}
|
||||
<div className="bg-white rounded-2xl border border-gray-150 p-5 shadow-xs">
|
||||
<div
|
||||
onClick={() => toggleCollapse('price')}
|
||||
className="flex items-center justify-between font-bold text-[14px] text-gray-800 cursor-pointer select-none pb-3 border-b border-gray-100"
|
||||
>
|
||||
<span>Price Filter</span>
|
||||
{collapsedFilters.price ? <ChevronDown size={16} /> : <ChevronUp size={16} />}
|
||||
</div>
|
||||
{!collapsedFilters.price && (
|
||||
<div className="pt-4 space-y-3">
|
||||
{[
|
||||
{ name: 'All', label: 'All' },
|
||||
{ name: '$0-$400', label: '$0 - $400' },
|
||||
{ name: '$400-$800', label: '$400 - $800' },
|
||||
{ name: '$800-$1,200', label: '$800 - $1,200' },
|
||||
{ name: '$1,200-$1,650', label: '$1,200 - $1,600' },
|
||||
{ name: '$1,600+', label: '$1,600+' }
|
||||
].map(pr => (
|
||||
<button
|
||||
key={pr.name}
|
||||
onClick={() => setSelectedPriceRange(pr.name)}
|
||||
className={`block text-left w-full text-[13px] py-1 transition-colors font-bold ${selectedPriceRange === pr.name ? 'text-primary font-bold' : 'text-gray-600 hover:text-primary'
|
||||
}`}
|
||||
>
|
||||
{pr.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Average Rating Filter */}
|
||||
<div className="bg-white rounded-2xl border border-gray-150 p-5 shadow-xs">
|
||||
<div
|
||||
onClick={() => toggleCollapse('rating')}
|
||||
className="flex items-center justify-between font-bold text-[14px] text-gray-800 cursor-pointer select-none pb-3 border-b border-gray-100"
|
||||
>
|
||||
<span>Average rating</span>
|
||||
{collapsedFilters.rating ? <ChevronDown size={16} /> : <ChevronUp size={16} />}
|
||||
</div>
|
||||
{!collapsedFilters.rating && (
|
||||
<div className="pt-4 space-y-3">
|
||||
{[5, 4, 3, 2, 1].map(stars => (
|
||||
<button
|
||||
key={stars}
|
||||
onClick={() => setSelectedRating(selectedRating === stars ? null : stars)}
|
||||
className="flex items-center justify-between w-full text-[13px] text-gray-600 hover:text-primary transition-colors font-medium font-outfit"
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<Star
|
||||
key={i}
|
||||
size={12}
|
||||
className={i < stars ? "text-amber-400 fill-amber-400" : "text-gray-200"}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-gray-400">({getRatingCount(stars)})</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
{/* Products Main Grid */}
|
||||
<div className="flex-grow min-w-0 w-full">
|
||||
|
||||
{/* Toolbar */}
|
||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 mb-6 font-sans">
|
||||
|
||||
{/* Result count */}
|
||||
<div className="text-[14px] font-semibold text-[#1E293B]">
|
||||
Showing all {filteredProducts.length} results
|
||||
</div>
|
||||
|
||||
{/* Right controls */}
|
||||
<div className="flex items-center gap-5 w-full sm:w-auto justify-end">
|
||||
{/* Sorting */}
|
||||
<div className="relative">
|
||||
<select
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value)}
|
||||
className="appearance-none bg-transparent border-b border-gray-250 pr-6 pb-1 text-[13px] font-bold text-gray-700 focus:outline-none cursor-pointer font-outfit"
|
||||
>
|
||||
<option value="default">Default sorting</option>
|
||||
<option value="price-low">Sort by price: low to high</option>
|
||||
<option value="price-high">Sort by price: high to low</option>
|
||||
<option value="rating">Sort by average rating</option>
|
||||
</select>
|
||||
<ChevronDown size={12} className="text-gray-400 absolute right-0.5 top-1/2 transform -translate-y-1/2 pointer-events-none" />
|
||||
</div>
|
||||
|
||||
{/* Grid/List toggles */}
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => setLayout('grid')}
|
||||
className={`p-2.5 cursor-pointer rounded-sm transition-colors text-[13px] font-bold ${layout === 'grid' ? 'bg-[#0073EC] text-white' : 'bg-gray-100 text-gray-500 hover:bg-gray-200'}`}
|
||||
title="Grid Layout"
|
||||
>
|
||||
<Grid size={15} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setLayout('list')}
|
||||
className={`p-2.5 cursor-pointer rounded-sm transition-colors text-[13px] font-bold ${layout === 'list' ? 'bg-[#0073EC] text-white' : 'bg-gray-100 text-gray-500 hover:bg-gray-200'}`}
|
||||
title="List Layout"
|
||||
>
|
||||
<List size={15} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Active Filter Chips */}
|
||||
{(selectedCategories.length > 0 || selectedBrands.length > 0 || selectedHighlight !== 'All Products' || selectedColor || selectedPriceRange !== 'All' || selectedRating || searchQuery) && (
|
||||
<div className="flex flex-wrap items-center gap-2.5 mb-6 font-sans">
|
||||
|
||||
{searchQuery && (
|
||||
<button
|
||||
onClick={() => setSearchQuery('')}
|
||||
className="bg-[#EFEFEF] hover:bg-black text-[#1E1E1E] hover:text-white transition-all cursor-pointer rounded-[4px] px-3.5 py-1.5 text-[12px] font-extrabold flex items-center gap-1.5 border-0 focus:outline-none"
|
||||
title="Remove Search Term"
|
||||
>
|
||||
<span className="text-[14px] leading-none">×</span>
|
||||
<span>Search: “{searchQuery}”</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{selectedCategories.map(cat => (
|
||||
<button
|
||||
key={cat}
|
||||
onClick={() => handleCategoryChange(cat)}
|
||||
className="bg-[#EFEFEF] hover:bg-black text-[#1E1E1E] hover:text-white transition-all cursor-pointer rounded-[4px] px-3.5 py-1.5 text-[12px] font-extrabold flex items-center gap-1.5 border-0 focus:outline-none"
|
||||
title="Remove This Category"
|
||||
>
|
||||
<span className="text-[14px] leading-none">×</span>
|
||||
<span>{cat}</span>
|
||||
</button>
|
||||
))}
|
||||
|
||||
{selectedBrands.map(br => (
|
||||
<button
|
||||
key={br}
|
||||
onClick={() => handleBrandChange(br)}
|
||||
className="bg-[#EFEFEF] hover:bg-black text-[#1E1E1E] hover:text-white transition-all cursor-pointer rounded-[4px] px-3.5 py-1.5 text-[12px] font-extrabold flex items-center gap-1.5 border-0 focus:outline-none"
|
||||
title="Remove This Product Brand"
|
||||
>
|
||||
<span className="text-[14px] leading-none">×</span>
|
||||
<span>{br}</span>
|
||||
</button>
|
||||
))}
|
||||
|
||||
{selectedHighlight !== 'All Products' && (
|
||||
<button
|
||||
onClick={() => setSelectedHighlight('All Products')}
|
||||
className="bg-[#EFEFEF] hover:bg-black text-[#1E1E1E] hover:text-white transition-all cursor-pointer rounded-[4px] px-3.5 py-1.5 text-[12px] font-extrabold flex items-center gap-1.5 border-0 focus:outline-none"
|
||||
title="Remove This Highlight"
|
||||
>
|
||||
<span className="text-[14px] leading-none">×</span>
|
||||
<span>{selectedHighlight}</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{selectedColor && (
|
||||
<button
|
||||
onClick={() => setSelectedColor(null)}
|
||||
className="bg-[#EFEFEF] hover:bg-black text-[#1E1E1E] hover:text-white transition-all cursor-pointer rounded-[4px] px-3.5 py-1.5 text-[12px] font-extrabold flex items-center gap-1.5 border-0 focus:outline-none"
|
||||
title="Remove This Color"
|
||||
>
|
||||
<span className="text-[14px] leading-none">×</span>
|
||||
<span>Color: {selectedColor}</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{selectedPriceRange !== 'All' && (
|
||||
<button
|
||||
onClick={() => setSelectedPriceRange('All')}
|
||||
className="bg-[#EFEFEF] hover:bg-black text-[#1E1E1E] hover:text-white transition-all cursor-pointer rounded-[4px] px-3.5 py-1.5 text-[12px] font-extrabold flex items-center gap-1.5 border-0 focus:outline-none"
|
||||
title="Remove This Price Filter"
|
||||
>
|
||||
<span className="text-[14px] leading-none">×</span>
|
||||
<span>Price: {selectedPriceRange}</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{selectedRating && (
|
||||
<button
|
||||
onClick={() => setSelectedRating(null)}
|
||||
className="bg-[#EFEFEF] hover:bg-black text-[#1E1E1E] hover:text-white transition-all cursor-pointer rounded-[4px] px-3.5 py-1.5 text-[12px] font-extrabold flex items-center gap-1.5 border-0 focus:outline-none"
|
||||
title="Remove This Rating Filter"
|
||||
>
|
||||
<span className="text-[14px] leading-none">×</span>
|
||||
<span>{selectedRating} Stars</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={resetAllFilters}
|
||||
className="text-[13px] font-bold text-[#0073EC] hover:text-black transition-colors underline decoration-solid ml-2 cursor-pointer focus:outline-none"
|
||||
>
|
||||
Clear All
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* No Products Found */}
|
||||
{filteredProducts.length === 0 && (
|
||||
<div className="bg-white rounded-2xl border border-gray-150 p-12 text-center shadow-xs">
|
||||
<div className="max-w-md mx-auto">
|
||||
<Search className="mx-auto text-gray-300 mb-4 stroke-[1.5]" size={48} />
|
||||
<h3 className="text-lg font-bold text-gray-800 mb-1">No products found</h3>
|
||||
<p className="text-gray-500 text-sm mb-6">We couldn't find any items matching your current filters. Try removing some filters to see more products.</p>
|
||||
<button
|
||||
onClick={resetAllFilters}
|
||||
className="bg-primary hover:bg-primary-hover text-white text-xs font-semibold px-6 py-3 rounded-lg shadow-sm transition-all"
|
||||
>
|
||||
Clear All Filters
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Products List/Grid Rendering */}
|
||||
{layout === 'grid' ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 bg-white border-t border-l border-gray-200 shadow-xs">
|
||||
{filteredProducts.map((product) => (
|
||||
<GridProductCard
|
||||
key={product.id}
|
||||
product={product}
|
||||
priceDisplay={getProductPriceDisplay(product)}
|
||||
isWishlisted={wishlist.includes(product.id)}
|
||||
onToggleWishlist={toggleWishlist}
|
||||
onQuickView={setQuickViewProduct}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white border border-gray-200 divide-y divide-gray-200 shadow-xs">
|
||||
{filteredProducts.map((product) => (
|
||||
<ListProductCard
|
||||
key={product.id}
|
||||
product={product}
|
||||
priceDisplay={getProductPriceDisplay(product)}
|
||||
isWishlisted={wishlist.includes(product.id)}
|
||||
onToggleWishlist={toggleWishlist}
|
||||
onQuickView={setQuickViewProduct}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
<FloatingButtons />
|
||||
<LiveToast />
|
||||
|
||||
{/* Quick View Modal Overlay */}
|
||||
{quickViewProduct && (
|
||||
<QuickViewModal
|
||||
product={quickViewProduct}
|
||||
onClose={() => setQuickViewProduct(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Wishlist Modal Overlay */}
|
||||
{wishlistModalOpen && (
|
||||
<WishlistModal onClose={() => setWishlistModalOpen(false)} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* Custom Grid Product Card to match the exact mockup designs */
|
||||
function GridProductCard({
|
||||
product,
|
||||
priceDisplay,
|
||||
isWishlisted,
|
||||
onToggleWishlist,
|
||||
onQuickView
|
||||
}: {
|
||||
product: Product;
|
||||
priceDisplay: string;
|
||||
isWishlisted: boolean;
|
||||
onToggleWishlist: (id: string) => void;
|
||||
onQuickView: (product: Product) => void;
|
||||
}) {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
className="bg-white p-5 flex flex-col justify-between relative overflow-visible transition-all duration-300 min-h-[430px] group/card border-r border-b border-gray-200 rounded-none font-sans"
|
||||
>
|
||||
{/* Top badges & actions */}
|
||||
<div className="flex justify-between items-start z-10 w-full mb-1">
|
||||
{/* Discount Badge */}
|
||||
{product.discount ? (
|
||||
<span className="bg-primary text-white text-[10px] font-extrabold px-2 py-0.5 rounded-sm">
|
||||
-{product.discount}%
|
||||
</span>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
|
||||
{/* Hover action flyouts on the side */}
|
||||
<div className={`flex flex-col gap-1.5 transition-all duration-300 absolute right-3 top-3 z-20 ${isHovered ? 'opacity-100 translate-x-0' : 'opacity-0 translate-x-2 pointer-events-none'}`}>
|
||||
<button
|
||||
onClick={(e) => { e.preventDefault(); e.stopPropagation(); onToggleWishlist(product.id); }}
|
||||
className={`w-8 h-8 rounded-full border flex items-center justify-center shadow-sm transition-all cursor-pointer ${isWishlisted ? 'bg-primary text-white border-primary' : 'bg-white hover:bg-primary hover:text-white text-gray-500 border-gray-150'
|
||||
}`}
|
||||
title="Add to Wishlist"
|
||||
>
|
||||
<Heart size={13} className={isWishlisted ? 'fill-white' : ''} />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.preventDefault(); e.stopPropagation(); onQuickView(product); }}
|
||||
className="w-8 h-8 rounded-full bg-white hover:bg-primary hover:text-white text-gray-500 border border-gray-150 flex items-center justify-center shadow-sm transition-all cursor-pointer"
|
||||
title="Quick View"
|
||||
>
|
||||
<Eye size={13} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Product Image & Timer Container */}
|
||||
<Link href={`/product/${product.id}`} className="w-full flex-1 flex flex-col items-center justify-center relative min-h-[170px] mb-3 select-none block">
|
||||
<img
|
||||
src={product.image}
|
||||
alt={product.name}
|
||||
className={`max-h-[150px] max-w-full object-contain drop-shadow-md rounded-lg transition-all duration-300 ${product.imageHover && isHovered ? 'opacity-0 scale-95' : 'opacity-100 scale-100'
|
||||
}`}
|
||||
/>
|
||||
{product.imageHover && (
|
||||
<img
|
||||
src={product.imageHover}
|
||||
alt={product.name}
|
||||
className={`max-h-[150px] max-w-full object-contain drop-shadow-md rounded-lg transition-all duration-300 absolute ${isHovered ? 'opacity-100 scale-100' : 'opacity-0 scale-95 pointer-events-none'
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Countdown Timer Block (styled to match mockup) */}
|
||||
{product.hasTimer && product.timeRemaining && (
|
||||
<div className="absolute -bottom-2 left-1/2 transform -translate-x-1/2 bg-red-50 text-red-650 border border-red-100 px-3 py-1.5 rounded-lg shadow-sm z-10 w-[95%] text-center">
|
||||
<div className="flex items-center justify-center gap-1.5 text-[9px] font-extrabold uppercase tracking-wide">
|
||||
<div><span className="text-[12px] block leading-none font-black text-red-650">{product.timeRemaining.days}</span><span>DAYS</span></div>
|
||||
<div className="text-gray-400 font-normal">:</div>
|
||||
<div><span className="text-[12px] block leading-none font-black text-red-650">04</span><span>HRS</span></div>
|
||||
<div className="text-gray-400 font-normal">:</div>
|
||||
<div><span className="text-[12px] block leading-none font-black text-red-650">50</span><span>MIN</span></div>
|
||||
<div className="text-gray-400 font-normal">:</div>
|
||||
<div><span className="text-[12px] block leading-none font-black text-red-650">18</span><span>SEC</span></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
{/* Info Block */}
|
||||
<div className="text-left w-full mt-3">
|
||||
{/* Title */}
|
||||
<Link href={`/product/${product.id}`}>
|
||||
<h3 className="text-[12.5px] font-bold text-gray-800 line-clamp-2 leading-snug hover:text-primary transition-colors cursor-pointer min-h-[38px] mb-2 font-outfit" title={product.name}>
|
||||
{product.name}
|
||||
</h3>
|
||||
</Link>
|
||||
|
||||
{/* Rating Stars */}
|
||||
<div className="flex items-center gap-0.5 mb-1.5">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<Star
|
||||
key={i}
|
||||
size={12}
|
||||
className={i < Math.floor(product.rating) ? "text-amber-400 fill-amber-400" : "text-gray-200"}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Price Display */}
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="text-[14px] font-extrabold text-gray-900">
|
||||
{priceDisplay}
|
||||
</span>
|
||||
{product.oldPrice && !priceDisplay.includes('–') && (
|
||||
<span className="text-xs text-gray-400 line-through font-medium">
|
||||
${product.oldPrice.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Select Options Button */}
|
||||
<div
|
||||
className={`w-full relative overflow-hidden transition-all duration-300 ${
|
||||
isHovered ? 'h-[44px] mt-2' : 'h-0'
|
||||
}`}
|
||||
>
|
||||
<Link
|
||||
href={`/product/${product.id}`}
|
||||
className="w-full bg-primary hover:bg-black text-white text-[11px] font-bold uppercase py-3 px-4 rounded-lg flex items-center justify-center gap-2 shadow-sm transition-all duration-300 cursor-pointer"
|
||||
>
|
||||
<ShoppingCart size={13} className="stroke-[2.5]" />
|
||||
Select Options
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* Custom List Product Card to support List layout view */
|
||||
function ListProductCard({
|
||||
product,
|
||||
priceDisplay,
|
||||
isWishlisted,
|
||||
onToggleWishlist,
|
||||
onQuickView
|
||||
}: {
|
||||
product: Product;
|
||||
priceDisplay: string;
|
||||
isWishlisted: boolean;
|
||||
onToggleWishlist: (id: string) => void;
|
||||
onQuickView: (product: Product) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="bg-white p-6 flex flex-col md:flex-row items-center gap-6 group font-sans rounded-none">
|
||||
|
||||
{/* Product Image */}
|
||||
<Link href={`/product/${product.id}`} className="w-full md:w-48 h-48 flex-shrink-0 flex items-center justify-center relative select-none block">
|
||||
<img
|
||||
src={product.image}
|
||||
alt={product.name}
|
||||
className="max-h-[160px] max-w-full object-contain drop-shadow-md rounded-lg group-hover:scale-105 transition-transform duration-300"
|
||||
/>
|
||||
{product.discount && (
|
||||
<span className="absolute top-2 left-2 bg-primary text-white text-[10px] font-extrabold px-2 py-0.5 rounded-md">
|
||||
-{product.discount}%
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
{/* Product Details */}
|
||||
<div className="flex-grow text-left">
|
||||
{/* Title */}
|
||||
<Link href={`/product/${product.id}`}>
|
||||
<h3 className="text-base font-bold text-gray-800 hover:text-primary transition-colors cursor-pointer mb-2 font-outfit" title={product.name}>
|
||||
{product.name}
|
||||
</h3>
|
||||
</Link>
|
||||
|
||||
{/* Rating Stars */}
|
||||
<div className="flex items-center gap-0.5 mb-2">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<Star
|
||||
key={i}
|
||||
size={12}
|
||||
className={i < Math.floor(product.rating) ? "text-amber-400 fill-amber-400" : "text-gray-200"}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Price Display */}
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="text-[17px] font-extrabold text-gray-900">
|
||||
{priceDisplay}
|
||||
</span>
|
||||
{product.oldPrice && !priceDisplay.includes('–') && (
|
||||
<span className="text-xs text-gray-400 line-through font-medium">
|
||||
${product.oldPrice.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Specs List (styled to match Image 2) */}
|
||||
<ul className="text-gray-500 text-[12.5px] font-semibold space-y-1 mb-4 list-disc list-inside">
|
||||
<li>Functional Crown</li>
|
||||
<li>Fhd Footage</li>
|
||||
<li>1.78" Amoled</li>
|
||||
</ul>
|
||||
|
||||
{/* Action Row */}
|
||||
<div className="flex items-center gap-3 pt-3.5 border-t border-gray-100">
|
||||
<Link
|
||||
href={`/product/${product.id}`}
|
||||
className="bg-[#0073EC] hover:bg-black text-white text-xs font-semibold px-6 py-2.5 rounded-lg flex items-center gap-2 shadow-sm transition-all cursor-pointer"
|
||||
>
|
||||
<ShoppingCart size={13} className="stroke-[2.5]" />
|
||||
View Products
|
||||
</Link>
|
||||
<button
|
||||
onClick={(e) => { e.preventDefault(); e.stopPropagation(); onToggleWishlist(product.id); }}
|
||||
className={`p-2.5 rounded-lg border transition-colors cursor-pointer ${isWishlisted ? 'bg-primary text-white border-primary' : 'text-gray-400 hover:text-primary hover:bg-blue-50 border-gray-150'
|
||||
}`}
|
||||
title="Add to Wishlist"
|
||||
>
|
||||
<Heart size={14} className={isWishlisted ? 'fill-white' : ''} />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.preventDefault(); e.stopPropagation(); onQuickView(product); }}
|
||||
className="p-2.5 text-gray-400 hover:text-primary hover:bg-blue-50 rounded-lg border border-gray-150 transition-colors cursor-pointer"
|
||||
title="Quick View"
|
||||
>
|
||||
<Eye size={14} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
83
NewFile/src/src/components/CategorySidebar.tsx
Normal file
83
NewFile/src/src/components/CategorySidebar.tsx
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import React from 'react';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface CategoryItem {
|
||||
name: string;
|
||||
hasSubmenu?: boolean;
|
||||
}
|
||||
|
||||
interface CategorySidebarProps {
|
||||
isDropdown?: boolean;
|
||||
}
|
||||
|
||||
export const CategorySidebar: React.FC<CategorySidebarProps> = ({ isDropdown = false }) => {
|
||||
const categories: CategoryItem[] = [
|
||||
{ name: 'All Products' },
|
||||
{ name: 'Our Store', hasSubmenu: true },
|
||||
{ name: 'TV & Speaker' },
|
||||
{ name: 'Smart Devices', hasSubmenu: true },
|
||||
{ name: 'Chargers & Cables' },
|
||||
{ name: 'Laptop & Computers' },
|
||||
{ name: 'Phones', hasSubmenu: true },
|
||||
{ name: 'Cameras' },
|
||||
{ name: 'Smart Watches' },
|
||||
{ name: 'Home Theatre Systems' },
|
||||
{ name: 'Televisions' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={`w-[280px] bg-white rounded-b-theme border-l border-r border-b border-primary shadow-[0_0_12px_rgba(25,118,243,0.2)] flex-shrink-0 hidden xl:block relative z-45 mt-0 ${
|
||||
isDropdown ? 'border-t' : ''
|
||||
}`}>
|
||||
<ul className="flex flex-col">
|
||||
{categories.map((cat, idx) => (
|
||||
<li key={cat.name} className="relative group/item">
|
||||
<Link
|
||||
href={cat.name === 'All Products' ? '/shop' : `/shop?category=${encodeURIComponent(cat.name)}`}
|
||||
className={`flex items-center justify-between px-5 py-[12.5px] text-[13px] font-semibold text-gray-700 hover:text-primary hover:bg-blue-50/20 transition-all ${
|
||||
idx < categories.length - 1 ? 'border-b border-gray-100' : ''
|
||||
}`}
|
||||
>
|
||||
<span>{cat.name}</span>
|
||||
{cat.hasSubmenu && (
|
||||
<ChevronDown size={13} className="text-gray-400 group-hover/item:text-primary transition-all" />
|
||||
)}
|
||||
</Link>
|
||||
|
||||
{/* Premium Flyout Mega Menu */}
|
||||
{cat.hasSubmenu && (
|
||||
<div className="absolute left-[279px] top-0 w-[500px] bg-white rounded-r-theme border border-gray-200 shadow-soft p-6 opacity-0 invisible translate-x-2 group-hover/item:opacity-100 group-hover/item:visible group-hover/item:translate-x-0 transition-all duration-200 z-50 grid grid-cols-2 gap-6 min-h-[380px]">
|
||||
<div>
|
||||
<h4 className="text-[11px] font-bold text-gray-800 uppercase tracking-wider mb-3">Popular Brands</h4>
|
||||
<ul className="space-y-2 text-[13px] text-gray-500 font-medium">
|
||||
<li><a href="#" className="hover:text-primary transition-colors">Apple Store</a></li>
|
||||
<li><a href="#" className="hover:text-primary transition-colors">Samsung Galaxy</a></li>
|
||||
<li><a href="#" className="hover:text-primary transition-colors">Google Pixel</a></li>
|
||||
<li><a href="#" className="hover:text-primary transition-colors">Sony Electronics</a></li>
|
||||
<li><a href="#" className="hover:text-primary transition-colors">OnePlus Official</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-[11px] font-bold text-gray-800 uppercase tracking-wider mb-3">Trending Products</h4>
|
||||
<ul className="space-y-2 text-[13px] text-gray-500 font-medium text-left">
|
||||
<li><a href="#" className="hover:text-primary transition-colors">Top Sellers</a></li>
|
||||
<li><a href="#" className="hover:text-primary transition-colors">Super Discounts</a></li>
|
||||
<li><a href="#" className="hover:text-primary transition-colors">Vouchers & Coupons</a></li>
|
||||
<li><a href="#" className="hover:text-primary transition-colors">New Releases</a></li>
|
||||
<li><a href="#" className="hover:text-primary transition-colors">Exclusive Packs</a></li>
|
||||
</ul>
|
||||
<div className="mt-6 p-4 rounded-xl bg-gradient-to-r from-blue-500 to-indigo-600 text-white text-left">
|
||||
<p className="text-[10px] font-bold uppercase tracking-widest text-blue-100">Hot Offer</p>
|
||||
<h5 className="font-bold text-[13px] leading-tight mt-1">Get 20% OFF on Audio</h5>
|
||||
<a href="#" className="inline-block mt-3 text-[11px] font-extrabold uppercase border-b-2 border-white leading-none pb-0.5">Shop Now</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
94
NewFile/src/src/components/CountdownTimer.tsx
Normal file
94
NewFile/src/src/components/CountdownTimer.tsx
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
interface CountdownTimerProps {
|
||||
initialDays?: number;
|
||||
initialHours?: number;
|
||||
initialMinutes?: number;
|
||||
initialSeconds?: number;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const CountdownTimer: React.FC<CountdownTimerProps> = ({
|
||||
initialDays = 0,
|
||||
initialHours = 0,
|
||||
initialMinutes = 0,
|
||||
initialSeconds = 0,
|
||||
size = 'md',
|
||||
className = '',
|
||||
}) => {
|
||||
const [totalSeconds, setTotalSeconds] = useState(
|
||||
initialDays * 86400 + initialHours * 3600 + initialMinutes * 60 + initialSeconds
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (totalSeconds <= 0) return;
|
||||
const timer = setInterval(() => {
|
||||
setTotalSeconds((prev) => prev - 1);
|
||||
}, 1000);
|
||||
return () => clearInterval(timer);
|
||||
}, [totalSeconds]);
|
||||
|
||||
const days = Math.floor(totalSeconds / 86400);
|
||||
const hours = Math.floor((totalSeconds % 86400) / 3600);
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
|
||||
const formatNumber = (num: number) => String(num).padStart(2, '0');
|
||||
|
||||
const containerSizes = {
|
||||
sm: 'text-[9px] gap-1',
|
||||
md: 'text-xs gap-1.5',
|
||||
lg: 'text-sm gap-2',
|
||||
};
|
||||
|
||||
const blockSizes = {
|
||||
sm: 'w-7 h-7 text-[11px] rounded-md',
|
||||
md: 'w-9 h-9 text-[13px] rounded-lg',
|
||||
lg: 'w-12 h-12 text-[16px] rounded-xl',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`flex items-center ${containerSizes[size]} ${className}`}>
|
||||
{/* Days */}
|
||||
<div className="flex flex-col items-center">
|
||||
<div className={`flex items-center justify-center font-bold text-red-500 bg-red-50 border border-red-100 shadow-sm ${blockSizes[size]}`}>
|
||||
{formatNumber(days)}
|
||||
</div>
|
||||
<span className="text-[9px] uppercase tracking-wider text-gray-400 mt-1 font-semibold">Days</span>
|
||||
</div>
|
||||
|
||||
<span className="font-bold text-red-500 relative -top-2.5">:</span>
|
||||
|
||||
{/* Hours */}
|
||||
<div className="flex flex-col items-center">
|
||||
<div className={`flex items-center justify-center font-bold text-red-500 bg-red-50 border border-red-100 shadow-sm ${blockSizes[size]}`}>
|
||||
{formatNumber(hours)}
|
||||
</div>
|
||||
<span className="text-[9px] uppercase tracking-wider text-gray-400 mt-1 font-semibold">Hrs</span>
|
||||
</div>
|
||||
|
||||
<span className="font-bold text-red-500 relative -top-2.5">:</span>
|
||||
|
||||
{/* Minutes */}
|
||||
<div className="flex flex-col items-center">
|
||||
<div className={`flex items-center justify-center font-bold text-red-500 bg-red-50 border border-red-100 shadow-sm ${blockSizes[size]}`}>
|
||||
{formatNumber(minutes)}
|
||||
</div>
|
||||
<span className="text-[9px] uppercase tracking-wider text-gray-400 mt-1 font-semibold">Min</span>
|
||||
</div>
|
||||
|
||||
<span className="font-bold text-red-500 relative -top-2.5">:</span>
|
||||
|
||||
{/* Seconds */}
|
||||
<div className="flex flex-col items-center">
|
||||
<div className={`flex items-center justify-center font-bold text-red-500 bg-red-50 border border-red-100 shadow-sm ${blockSizes[size]}`}>
|
||||
{formatNumber(seconds)}
|
||||
</div>
|
||||
<span className="text-[9px] uppercase tracking-wider text-gray-400 mt-1 font-semibold">Sec</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
62
NewFile/src/src/components/FloatingButtons.tsx
Normal file
62
NewFile/src/src/components/FloatingButtons.tsx
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ChevronUp, MessageSquare } from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
export const FloatingButtons: React.FC = () => {
|
||||
const [showScrollTop, setShowScrollTop] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setShowScrollTop(window.scrollY > 400);
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
const scrollToTop = () => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-6 right-6 z-50 flex flex-col gap-3">
|
||||
{/* Scroll to Top */}
|
||||
<AnimatePresence>
|
||||
{showScrollTop && (
|
||||
<motion.button
|
||||
initial={{ opacity: 0, scale: 0.8, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.8, y: 20 }}
|
||||
whileHover={{ y: -3, scale: 1.05 }}
|
||||
onClick={scrollToTop}
|
||||
className="w-12 h-12 bg-white hover:bg-gray-50 text-gray-700 rounded-full shadow-soft hover:shadow-soft-hover border border-gray-100 flex items-center justify-center transition-colors"
|
||||
title="Scroll to top"
|
||||
>
|
||||
<ChevronUp size={22} className="stroke-[2.5]" />
|
||||
</motion.button>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Live Chat Bubble */}
|
||||
<motion.button
|
||||
whileHover={{ y: -3, scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="w-12 h-12 bg-primary hover:bg-primary-hover text-white rounded-full shadow-soft hover:shadow-soft-hover flex items-center justify-center relative group"
|
||||
title="Live Chat Support"
|
||||
onClick={() => alert('Support Chat simulation started! Hello, how can we help you today?')}
|
||||
>
|
||||
<MessageSquare size={20} className="text-white" />
|
||||
<span className="absolute right-14 bg-gray-900 text-white text-[10px] font-bold px-2.5 py-1.5 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity duration-200 shadow-md whitespace-nowrap">
|
||||
Chat with us!
|
||||
</span>
|
||||
{/* Pulse effect */}
|
||||
<span className="absolute inset-0 rounded-full border-2 border-primary animate-ping opacity-25" />
|
||||
</motion.button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
116
NewFile/src/src/components/LiveToast.tsx
Normal file
116
NewFile/src/src/components/LiveToast.tsx
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { CheckCircle2, X } from 'lucide-react';
|
||||
|
||||
interface Purchase {
|
||||
name: string;
|
||||
location: string;
|
||||
product: string;
|
||||
time: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
const mockPurchases: Purchase[] = [
|
||||
{
|
||||
name: 'Ben',
|
||||
location: 'New York',
|
||||
product: 'HP (23.8 Inch) with IPS Panel Technology',
|
||||
time: '3 hours ago',
|
||||
image: 'https://images.unsplash.com/photo-1527443224154-c4a3942d3acf?w=100&auto=format&fit=crop&q=60'
|
||||
},
|
||||
{
|
||||
name: 'Swaraa',
|
||||
location: 'California',
|
||||
product: 'HP LaserJet P1108 Mono Single Function Laser Printer',
|
||||
time: '46 minutes ago',
|
||||
image: 'https://images.unsplash.com/photo-1612815154858-60aa4c59eaa6?w=100&auto=format&fit=crop&q=60'
|
||||
},
|
||||
{
|
||||
name: 'Alex',
|
||||
location: 'London',
|
||||
product: 'SwapME Braided Nylon Woven Smart Watch',
|
||||
time: '12 minutes ago',
|
||||
image: 'https://images.unsplash.com/photo-1579586337278-3befd40fd17a?w=100&auto=format&fit=crop&q=60'
|
||||
},
|
||||
{
|
||||
name: 'Hiroshi',
|
||||
location: 'Tokyo',
|
||||
product: 'Ceptics India to Europe Travel Adapter Plug',
|
||||
time: '5 minutes ago',
|
||||
image: 'https://images.unsplash.com/photo-1628157582853-a796fa650a6a?w=100&auto=format&fit=crop&q=60'
|
||||
}
|
||||
];
|
||||
|
||||
export const LiveToast: React.FC = () => {
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const initialTimeout = setTimeout(() => {
|
||||
setIsVisible(true);
|
||||
}, 4000);
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setIsVisible(false);
|
||||
setTimeout(() => {
|
||||
setCurrentIndex((prev) => (prev + 1) % mockPurchases.length);
|
||||
setIsVisible(true);
|
||||
}, 1000);
|
||||
}, 15000);
|
||||
|
||||
return () => {
|
||||
clearTimeout(initialTimeout);
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!mockPurchases.length) return null;
|
||||
const currentPurchase = mockPurchases[currentIndex];
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isVisible && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 50, scale: 0.9 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: 20, scale: 0.9 }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 25 }}
|
||||
className="fixed bottom-6 left-6 z-50 max-w-[340px] bg-white rounded-theme p-4 shadow-soft hover:shadow-soft-hover border border-gray-100 flex items-start gap-3"
|
||||
>
|
||||
<img
|
||||
src={currentPurchase.image}
|
||||
alt={currentPurchase.product}
|
||||
className="w-12 h-12 object-cover rounded-lg border border-gray-100 flex-shrink-0"
|
||||
/>
|
||||
<div className="flex-1 min-w-0 pr-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-[11px] text-gray-500 font-medium">
|
||||
<span className="font-bold text-gray-800">{currentPurchase.name}</span> from{' '}
|
||||
<span className="font-semibold text-primary">{currentPurchase.location}</span>
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setIsVisible(false)}
|
||||
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
<X size={12} />
|
||||
</button>
|
||||
</div>
|
||||
<h4 className="text-[12px] font-bold text-gray-800 mt-0.5 leading-snug truncate">
|
||||
{currentPurchase.product}
|
||||
</h4>
|
||||
<div className="flex items-center gap-1.5 mt-1.5">
|
||||
<span className="text-[10px] text-gray-400 font-semibold">{currentPurchase.time}</span>
|
||||
<span className="w-1 h-1 bg-gray-300 rounded-full" />
|
||||
<div className="flex items-center gap-0.5 text-green-600">
|
||||
<CheckCircle2 size={10} fill="currentColor" className="text-white" />
|
||||
<span className="text-[9px] font-bold tracking-wider uppercase">Verified</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
112
NewFile/src/src/components/MobileDrawer.tsx
Normal file
112
NewFile/src/src/components/MobileDrawer.tsx
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { X, Tablet, Tv, Speaker, Cable, Laptop, Smartphone, Camera, Watch, LayoutGrid } from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
interface MobileDrawerProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const MobileDrawer: React.FC<MobileDrawerProps> = ({ isOpen, onClose }) => {
|
||||
const categories = [
|
||||
{ name: 'Our Store', icon: <LayoutGrid size={15} /> },
|
||||
{ name: 'TV & Speaker', icon: <Speaker size={15} /> },
|
||||
{ name: 'Smart Devices', icon: <Tablet size={15} /> },
|
||||
{ name: 'Chargers & Cables', icon: <Cable size={15} /> },
|
||||
{ name: 'Laptop & Computers', icon: <Laptop size={15} /> },
|
||||
{ name: 'Phones', icon: <Smartphone size={15} /> },
|
||||
{ name: 'Cameras', icon: <Camera size={15} /> },
|
||||
{ name: 'Smart Watches', icon: <Watch size={15} /> },
|
||||
];
|
||||
|
||||
const links = [
|
||||
{ name: 'Home', isNew: false, badge: '', badgeBg: '' },
|
||||
{ name: 'Shop', isNew: false, badge: '', badgeBg: '' },
|
||||
{ name: 'Categories', isNew: true, badge: 'Sale', badgeBg: 'bg-[#E2F9F3] text-[#00B894]' },
|
||||
{ name: 'Products', isNew: true, badge: 'Hot', badgeBg: 'bg-red-50 text-red-500' },
|
||||
{ name: 'Top Deals', isNew: false, badge: '', badgeBg: '' },
|
||||
{ name: 'Elements', isNew: false, badge: '', badgeBg: '' },
|
||||
];
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 0.5 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={onClose}
|
||||
className="fixed inset-0 bg-black z-50 xl:hidden"
|
||||
/>
|
||||
|
||||
{/* Drawer Panel */}
|
||||
<motion.div
|
||||
initial={{ x: '-100%' }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: '-100%' }}
|
||||
transition={{ type: 'tween', duration: 0.3 }}
|
||||
className="fixed top-0 left-0 bottom-0 w-[280px] bg-white z-50 xl:hidden flex flex-col justify-between shadow-xl overflow-y-auto"
|
||||
>
|
||||
<div>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-100">
|
||||
<span className="text-xl font-extrabold tracking-tight text-gray-900">
|
||||
Tech<span className="text-primary">Shop</span>
|
||||
</span>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1.5 text-gray-400 hover:text-gray-650 hover:bg-gray-50 rounded-lg transition-colors"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Navigation Links */}
|
||||
<div className="p-4 border-b border-gray-100 text-left">
|
||||
<h4 className="text-[11px] font-bold text-gray-400 uppercase tracking-widest mb-3">Navigation</h4>
|
||||
<ul className="space-y-3.5">
|
||||
{links.map((link) => (
|
||||
<li key={link.name}>
|
||||
<a href="#" className="flex items-center justify-between text-sm font-bold text-gray-700 hover:text-primary transition-colors">
|
||||
<span>{link.name}</span>
|
||||
{link.isNew && (
|
||||
<span className={`text-[8px] font-extrabold px-1.5 py-0.5 rounded tracking-wide uppercase ${link.badgeBg}`}>
|
||||
{link.badge}
|
||||
</span>
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Categories */}
|
||||
<div className="p-4 text-left">
|
||||
<h4 className="text-[11px] font-bold text-gray-400 uppercase tracking-widest mb-3">Categories</h4>
|
||||
<ul className="space-y-3">
|
||||
{categories.map((cat) => (
|
||||
<li key={cat.name}>
|
||||
<a href="#" className="flex items-center gap-3 text-sm font-semibold text-gray-650 hover:text-primary transition-colors">
|
||||
<span className="text-gray-400">{cat.icon}</span>
|
||||
<span>{cat.name}</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-4 border-t border-gray-100 text-xs text-gray-400 text-center font-medium bg-gray-50">
|
||||
© {new Date().getFullYear()} TechShop. All Rights Reserved.
|
||||
</div>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
209
NewFile/src/src/components/ProductCard.tsx
Normal file
209
NewFile/src/src/components/ProductCard.tsx
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Heart, Eye, ShoppingCart } from 'lucide-react';
|
||||
import { Product } from '../types';
|
||||
import { RatingStars } from './RatingStars';
|
||||
import { CountdownTimer } from './CountdownTimer';
|
||||
|
||||
interface ProductCardProps {
|
||||
product: Product;
|
||||
onAddToCart?: (product: Product) => void;
|
||||
onAddToWishlist?: (product: Product) => void;
|
||||
}
|
||||
|
||||
export const ProductCard: React.FC<ProductCardProps> = ({
|
||||
product,
|
||||
onAddToCart,
|
||||
onAddToWishlist,
|
||||
}) => {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [isWishlisted, setIsWishlisted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkWishlist = () => {
|
||||
const saved = localStorage.getItem('wishlist');
|
||||
if (saved) {
|
||||
try {
|
||||
const list: string[] = JSON.parse(saved);
|
||||
const active = list.includes(product.id) || list.includes(product.id.replace('prod-', ''));
|
||||
setIsWishlisted(active);
|
||||
} catch (e) {
|
||||
setIsWishlisted(false);
|
||||
}
|
||||
} else {
|
||||
setIsWishlisted(false);
|
||||
}
|
||||
};
|
||||
checkWishlist();
|
||||
window.addEventListener('storage', checkWishlist);
|
||||
window.addEventListener('wishlistUpdated', checkWishlist);
|
||||
return () => {
|
||||
window.removeEventListener('storage', checkWishlist);
|
||||
window.removeEventListener('wishlistUpdated', checkWishlist);
|
||||
};
|
||||
}, [product.id]);
|
||||
|
||||
const handleCartClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (onAddToCart) {
|
||||
onAddToCart(product);
|
||||
} else {
|
||||
alert(`Added ${product.name} to cart!`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleWishlistClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
let saved = localStorage.getItem('wishlist');
|
||||
let list: string[] = [];
|
||||
if (saved) {
|
||||
try {
|
||||
list = JSON.parse(saved);
|
||||
} catch (err) {
|
||||
list = [];
|
||||
}
|
||||
}
|
||||
const cleanNumId = product.id.replace('prod-', '');
|
||||
const isPresent = list.includes(product.id) || list.includes(cleanNumId);
|
||||
if (isPresent) {
|
||||
list = list.filter((id) => id !== product.id && id !== cleanNumId);
|
||||
} else {
|
||||
list.push(product.id);
|
||||
}
|
||||
localStorage.setItem('wishlist', JSON.stringify(list));
|
||||
setIsWishlisted(!isPresent);
|
||||
window.dispatchEvent(new Event('wishlistUpdated'));
|
||||
window.dispatchEvent(new Event('openWishlistModal'));
|
||||
if (onAddToWishlist) onAddToWishlist(product);
|
||||
};
|
||||
|
||||
// Determine price text representation
|
||||
const isRange = product.name.includes('Ceptics') || product.name.includes('Lenovo');
|
||||
const priceDisplay = isRange
|
||||
? `$300 – $1,500`
|
||||
: `$${product.price.toLocaleString()}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
className="bg-white p-5 flex flex-col justify-between relative overflow-visible transition-all duration-300 min-h-[390px] font-sans hover:shadow-soft-hover group/card border-r border-b border-gray-200 rounded-none"
|
||||
>
|
||||
{/* Top badges & actions */}
|
||||
<div className="flex justify-between items-start z-10 w-full mb-1">
|
||||
{/* Discount Tag */}
|
||||
{product.discount ? (
|
||||
<span className="bg-primary text-white text-[10px] font-extrabold px-2 py-0.5 rounded-md">
|
||||
-{product.discount}%
|
||||
</span>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
|
||||
{/* Hover action flyouts */}
|
||||
<div className={`flex flex-col gap-1.5 transition-all duration-300 absolute right-3 top-3 z-20 ${isHovered ? 'opacity-100 translate-x-0' : 'opacity-0 translate-x-2 pointer-events-none'}`}>
|
||||
<button
|
||||
onClick={handleWishlistClick}
|
||||
className={`w-8 h-8 rounded-full border flex items-center justify-center shadow-sm transition-all cursor-pointer ${
|
||||
isWishlisted ? 'bg-primary text-white border-primary' : 'bg-white hover:bg-primary hover:text-white text-gray-500 border-gray-150'
|
||||
}`}
|
||||
title={isWishlisted ? "Remove from Wishlist" : "Add to Wishlist"}
|
||||
>
|
||||
<Heart size={13} className={isWishlisted ? 'fill-white' : ''} />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
window.dispatchEvent(new CustomEvent('openQuickView', { detail: product }));
|
||||
}}
|
||||
className="w-8 h-8 rounded-full bg-white hover:bg-primary hover:text-white text-gray-500 border border-gray-150 flex items-center justify-center shadow-sm transition-all cursor-pointer"
|
||||
title="Quick View"
|
||||
>
|
||||
<Eye size={13} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Product Image & Timer Container */}
|
||||
<Link href={`/product/${product.id}`} className="w-full flex-1 flex flex-col items-center justify-center relative min-h-[160px] mb-3 select-none block">
|
||||
{/* Default Image */}
|
||||
<img
|
||||
src={product.image}
|
||||
alt={product.name}
|
||||
className={`max-h-[140px] max-w-full object-contain drop-shadow-md rounded-lg transition-opacity duration-300 absolute ${
|
||||
product.imageHover && isHovered ? 'opacity-0' : 'opacity-100'
|
||||
}`}
|
||||
/>
|
||||
|
||||
{/* Hover Swap Image */}
|
||||
{product.imageHover && (
|
||||
<img
|
||||
src={product.imageHover}
|
||||
alt={product.name}
|
||||
className={`max-h-[140px] max-w-full object-contain drop-shadow-md rounded-lg transition-opacity duration-300 ${
|
||||
isHovered ? 'opacity-100' : 'opacity-0 pointer-events-none'
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Countdown Timer (Bottom of Image) */}
|
||||
{product.hasTimer && product.timeRemaining && (
|
||||
<div className="absolute bottom-0 left-1/2 transform -translate-x-1/2 bg-white/95 backdrop-blur-xs px-2.5 py-1 rounded-xl border border-red-100 shadow-md z-10">
|
||||
<CountdownTimer
|
||||
initialDays={product.timeRemaining.days}
|
||||
initialHours={product.timeRemaining.hours}
|
||||
initialMinutes={product.timeRemaining.minutes}
|
||||
initialSeconds={product.timeRemaining.seconds}
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
{/* Name, Rating & Price */}
|
||||
<div className="text-left w-full mt-2">
|
||||
{/* Title */}
|
||||
<Link href={`/product/${product.id}`}>
|
||||
<h3 className="text-[12.5px] font-bold text-gray-800 line-clamp-2 leading-snug hover:text-primary transition-colors cursor-pointer min-h-[34px] mb-1.5" title={product.name}>
|
||||
{product.name}
|
||||
</h3>
|
||||
</Link>
|
||||
|
||||
{/* Rating */}
|
||||
<RatingStars rating={product.rating} size={12} className="mb-1" />
|
||||
|
||||
{/* Price & Old Price */}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-[13.5px] font-extrabold text-gray-900">
|
||||
{priceDisplay}
|
||||
</span>
|
||||
{product.oldPrice && !isRange && (
|
||||
<span className="text-xs text-gray-400 line-through font-medium">
|
||||
${product.oldPrice.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add To Cart slide-up action */}
|
||||
<div
|
||||
className={`w-full relative overflow-hidden transition-all duration-300 ${
|
||||
isHovered ? 'h-[38px] mt-2' : 'h-0'
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
onClick={handleCartClick}
|
||||
className="w-full bg-primary hover:bg-black text-white text-[11px] font-bold py-2.5 px-4 rounded-lg flex items-center justify-center gap-2 shadow-sm transition-all duration-300"
|
||||
>
|
||||
<ShoppingCart size={13} className="stroke-[2.5]" />
|
||||
Add To Cart
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
189
NewFile/src/src/components/QuickViewModal.tsx
Normal file
189
NewFile/src/src/components/QuickViewModal.tsx
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Star, X } from 'lucide-react';
|
||||
import { Product } from '../types';
|
||||
|
||||
interface QuickViewModalProps {
|
||||
product: Product;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const QuickViewModal: React.FC<QuickViewModalProps> = ({ product, onClose }) => {
|
||||
const [activeImgIndex, setActiveImgIndex] = useState(0);
|
||||
const images = product.imageHover ? [product.image, product.imageHover] : [product.image];
|
||||
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = 'hidden';
|
||||
return () => {
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handlePrev = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setActiveImgIndex((prev) => (prev === 0 ? images.length - 1 : prev - 1));
|
||||
};
|
||||
|
||||
const handleNext = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setActiveImgIndex((prev) => (prev === images.length - 1 ? 0 : prev + 1));
|
||||
};
|
||||
|
||||
const ratingsCount = 1;
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onClose}
|
||||
className="fixed inset-0 bg-black/75 backdrop-blur-xs flex items-center justify-center z-[200] p-4 font-sans"
|
||||
>
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="bg-white w-full max-w-4xl max-h-[90vh] overflow-y-auto rounded-lg shadow-2xl relative flex flex-col md:flex-row text-left animate-in fade-in zoom-in duration-200"
|
||||
>
|
||||
{/* Close Button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-4 right-4 text-gray-400 hover:text-black transition-colors cursor-pointer z-30"
|
||||
title="Close Modal"
|
||||
>
|
||||
<X size={20} className="stroke-[2.5]" />
|
||||
</button>
|
||||
|
||||
{/* Left Side: Product Image Slider */}
|
||||
<div className="w-full md:w-1/2 p-6 flex flex-col items-center justify-center border-r border-gray-100 min-h-[350px] relative select-none bg-white">
|
||||
<div className="w-full h-72 flex items-center justify-center relative">
|
||||
<img
|
||||
src={images[activeImgIndex]}
|
||||
alt={product.name}
|
||||
className="max-h-full max-w-full object-contain transition-all duration-300"
|
||||
/>
|
||||
|
||||
{images.length > 1 && (
|
||||
<>
|
||||
<button
|
||||
onClick={handlePrev}
|
||||
className="absolute left-2 top-1/2 transform -translate-y-1/2 bg-[#0073EC] hover:bg-[#005bb5] text-white w-9 h-9 rounded-full flex items-center justify-center cursor-pointer transition-all shadow-md font-bold text-lg select-none z-10"
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
<button
|
||||
onClick={handleNext}
|
||||
className="absolute right-2 top-1/2 transform -translate-y-1/2 bg-[#0073EC] hover:bg-[#005bb5] text-white w-9 h-9 rounded-full flex items-center justify-center cursor-pointer transition-all shadow-md font-bold text-lg select-none z-10"
|
||||
>
|
||||
›
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Dots Indicator */}
|
||||
{images.length > 1 && (
|
||||
<div className="flex gap-1.5 mt-4">
|
||||
{images.map((_, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={(e) => { e.stopPropagation(); setActiveImgIndex(idx); }}
|
||||
className={`w-2 h-2 rounded-full cursor-pointer transition-all ${
|
||||
activeImgIndex === idx ? 'bg-black w-4' : 'bg-gray-300'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right Side: Details & Options */}
|
||||
<div className="w-full md:w-1/2 p-8 flex flex-col justify-between">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-gray-900 pr-6 mb-2 font-outfit">
|
||||
{product.name}
|
||||
</h2>
|
||||
|
||||
{/* Ratings */}
|
||||
<div className="flex items-center gap-1.5 mb-4">
|
||||
<div className="flex items-center gap-0.5">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<Star
|
||||
key={i}
|
||||
size={13}
|
||||
className={i < Math.floor(product.rating) ? "text-amber-400 fill-amber-400" : "text-gray-200"}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-xs text-[#0073EC] font-semibold cursor-pointer hover:underline">
|
||||
({ratingsCount} review)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Price */}
|
||||
<p className="text-lg font-black text-gray-950 mb-5">
|
||||
{product.name.includes('Airpods Pro')
|
||||
? '$299 – $1,900'
|
||||
: product.name.includes('Apple Watch')
|
||||
? '$669 – $700'
|
||||
: `$${product.price.toLocaleString()}`}
|
||||
</p>
|
||||
|
||||
{/* Specs Bullet Points */}
|
||||
<ul className="text-gray-500 text-[13px] font-semibold space-y-2 mb-6 list-disc list-inside">
|
||||
<li>Functional Crown</li>
|
||||
<li>Fhd Footage</li>
|
||||
<li>1.78" Amoled</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Cross-Sell Bundles list */}
|
||||
<div className="mt-4 pt-4 border-t border-gray-100">
|
||||
<div className="border border-gray-150 rounded-lg p-3 bg-white space-y-3">
|
||||
{/* Bundle item 1 */}
|
||||
<div className="flex items-center justify-between gap-3 text-left">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1579586337278-3befd40fd17a?w=100&auto=format&fit=crop&q=60"
|
||||
alt="Raptag Screen Guard"
|
||||
className="w-10 h-10 object-contain rounded-md border border-gray-100"
|
||||
/>
|
||||
<div>
|
||||
<h4 className="text-xs font-bold text-gray-800 line-clamp-1">Raptag Screen Guard</h4>
|
||||
<p className="text-[11px] text-gray-500 font-bold">
|
||||
<span className="text-gray-400 line-through mr-1">$2,000</span>
|
||||
<span className="text-gray-800">$1,900</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
defaultValue={0}
|
||||
min={0}
|
||||
className="w-11 text-center border border-gray-250 py-1 rounded text-xs font-bold focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Bundle item 2 */}
|
||||
<div className="flex items-center justify-between gap-3 text-left pt-3 border-t border-gray-100">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1543512214-318c7553f230?w=100&auto=format&fit=crop&q=60"
|
||||
alt="Google Home Smart Speaker"
|
||||
className="w-10 h-10 object-contain rounded-md border border-gray-100"
|
||||
/>
|
||||
<div>
|
||||
<h4 className="text-xs font-bold text-gray-800 line-clamp-1">Google Home Smart...</h4>
|
||||
<p className="text-[11px] text-gray-800 font-bold">$300</p>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
defaultValue={0}
|
||||
min={0}
|
||||
className="w-11 text-center border border-gray-250 py-1 rounded text-xs font-bold focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
43
NewFile/src/src/components/RatingStars.tsx
Normal file
43
NewFile/src/src/components/RatingStars.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import React from 'react';
|
||||
import { Star } from 'lucide-react';
|
||||
|
||||
interface RatingStarsProps {
|
||||
rating: number;
|
||||
max?: number;
|
||||
size?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const RatingStars: React.FC<RatingStarsProps> = ({
|
||||
rating,
|
||||
max = 5,
|
||||
size = 16,
|
||||
className = '',
|
||||
}) => {
|
||||
const stars = [];
|
||||
const roundedRating = Math.round(rating * 2) / 2;
|
||||
|
||||
for (let i = 1; i <= max; i++) {
|
||||
const isFull = i <= roundedRating;
|
||||
const isHalf = !isFull && i - 0.5 <= roundedRating;
|
||||
|
||||
stars.push(
|
||||
<span key={i} className="text-[#FFB21E] inline-flex">
|
||||
{isFull ? (
|
||||
<Star size={size} fill="currentColor" stroke="currentColor" />
|
||||
) : isHalf ? (
|
||||
<span className="relative inline-flex" style={{ width: size, height: size }}>
|
||||
<Star size={size} className="absolute top-0 left-0 text-gray-300" />
|
||||
<span className="absolute top-0 left-0 overflow-hidden" style={{ width: '50%' }}>
|
||||
<Star size={size} fill="currentColor" stroke="currentColor" />
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
<Star size={size} className="text-gray-300" />
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className={`flex items-center gap-0.5 ${className}`}>{stars}</div>;
|
||||
};
|
||||
70
NewFile/src/src/components/SearchBar.tsx
Normal file
70
NewFile/src/src/components/SearchBar.tsx
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Search, ChevronDown } from 'lucide-react';
|
||||
|
||||
export const SearchBar: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const [category, setCategory] = useState('All Categories');
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const categories = [
|
||||
'All Categories',
|
||||
'TV & Speaker',
|
||||
'Smart Devices',
|
||||
'Chargers & Cables',
|
||||
'Laptop & Computers',
|
||||
'Phones',
|
||||
'Cameras',
|
||||
'Smart Watches',
|
||||
];
|
||||
|
||||
const handleSearchSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const catQuery = category === 'All Categories' ? '' : `category=${encodeURIComponent(category)}`;
|
||||
const searchQueryStr = searchQuery.trim() ? `search=${encodeURIComponent(searchQuery.trim())}` : '';
|
||||
const queryParts = [catQuery, searchQueryStr].filter(Boolean).join('&');
|
||||
const path = queryParts ? `/shop?${queryParts}` : '/shop';
|
||||
router.push(path);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSearchSubmit} className="flex flex-1 max-w-[720px] items-center bg-white rounded-lg border border-gray-200 overflow-visible shadow-sm h-[46px] relative">
|
||||
{/* Category Dropdown */}
|
||||
<div className="relative h-full hidden lg:block">
|
||||
<select
|
||||
value={category}
|
||||
onChange={(e) => setCategory(e.target.value)}
|
||||
className="appearance-none bg-gray-50 border-r border-gray-150 h-full px-5 pr-8 text-[13px] font-semibold text-gray-700 hover:bg-gray-100 transition-colors focus:outline-none cursor-pointer rounded-l-lg font-sans"
|
||||
>
|
||||
{categories.map((cat) => (
|
||||
<option key={cat} value={cat}>
|
||||
{cat}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<ChevronDown size={14} className="text-gray-400 absolute right-3 top-1/2 transform -translate-y-1/2 pointer-events-none" />
|
||||
</div>
|
||||
|
||||
{/* Input Field */}
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search products..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="flex-1 px-4 text-[13px] text-gray-800 placeholder-gray-400 focus:outline-none h-full font-medium"
|
||||
/>
|
||||
|
||||
{/* Search Button */}
|
||||
<button
|
||||
type="submit"
|
||||
className="h-full bg-primary hover:bg-primary-hover text-white px-7 flex items-center justify-center font-bold text-sm transition-colors rounded-r-lg"
|
||||
title="Search"
|
||||
>
|
||||
Search
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
171
NewFile/src/src/components/WishlistModal.tsx
Normal file
171
NewFile/src/src/components/WishlistModal.tsx
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Heart, X } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { products as initialProducts } from '../utils/mockData';
|
||||
|
||||
interface WishlistModalProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const WishlistModal: React.FC<WishlistModalProps> = ({ onClose }) => {
|
||||
const [wishlistIds, setWishlistIds] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = 'hidden';
|
||||
const loadWishlist = () => {
|
||||
const saved = localStorage.getItem('wishlist');
|
||||
if (saved) {
|
||||
try {
|
||||
const list = JSON.parse(saved);
|
||||
setWishlistIds(Array.isArray(list) ? list : []);
|
||||
} catch (e) {
|
||||
setWishlistIds([]);
|
||||
}
|
||||
} else {
|
||||
setWishlistIds([]);
|
||||
}
|
||||
};
|
||||
loadWishlist();
|
||||
window.addEventListener('storage', loadWishlist);
|
||||
window.addEventListener('wishlistUpdated', loadWishlist);
|
||||
return () => {
|
||||
document.body.style.overflow = '';
|
||||
window.removeEventListener('storage', loadWishlist);
|
||||
window.removeEventListener('wishlistUpdated', loadWishlist);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleRemove = (id: string) => {
|
||||
const updated = wishlistIds.filter(
|
||||
(item) => item !== id && item !== id.replace('prod-', '') && `prod-${item}` !== id
|
||||
);
|
||||
setWishlistIds(updated);
|
||||
localStorage.setItem('wishlist', JSON.stringify(updated));
|
||||
window.dispatchEvent(new Event('wishlistUpdated'));
|
||||
};
|
||||
|
||||
const wishlistProducts = initialProducts.filter(
|
||||
(p) => wishlistIds.includes(p.id) || wishlistIds.includes(p.id.replace('prod-', ''))
|
||||
);
|
||||
|
||||
const getAddedDate = (id: string) => {
|
||||
const dates: Record<string, string> = {
|
||||
'prod-1': 'July 25, 2026',
|
||||
'prod-2': 'July 25, 2026',
|
||||
'prod-3': 'June 8, 2026',
|
||||
'prod-4': 'March 22, 2026',
|
||||
'prod-5': 'October 20, 2025',
|
||||
'prod-6': 'October 8, 2025',
|
||||
'prod-7': 'April 28, 2025',
|
||||
};
|
||||
return dates[id] || 'July 25, 2026';
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onClose}
|
||||
className="fixed inset-0 bg-black/75 backdrop-blur-xs flex items-center justify-center z-[200] p-4 font-sans animate-in fade-in duration-200"
|
||||
>
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="bg-white w-full max-w-[740px] rounded-2xl shadow-2xl overflow-hidden relative flex flex-col max-h-[85vh] text-left"
|
||||
>
|
||||
{/* Header (Dark style matching mockup) */}
|
||||
<div className="bg-[#1E1E1E] text-white px-7 py-4 flex items-center justify-between">
|
||||
<h2 className="text-[16px] font-bold tracking-wide">
|
||||
Wishlist ({wishlistProducts.length})
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-9 h-9 rounded-full flex items-center justify-center text-gray-400 hover:text-white transition-colors cursor-pointer -mr-2"
|
||||
title="Close"
|
||||
>
|
||||
<X size={19} className="stroke-[2.5]" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Wishlist Items List */}
|
||||
{wishlistProducts.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-gray-500 flex-grow bg-[#F8F9FB]">
|
||||
<Heart size={40} className="text-gray-300 mb-3 stroke-[1.5]" />
|
||||
<p className="text-[13px] font-semibold">Your wishlist is empty!</p>
|
||||
<p className="text-[11px] text-gray-400 mt-1">Add items from the store to see them here.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="overflow-y-auto divide-y divide-gray-150 max-h-[495px] bg-white px-7 border-b border-gray-100"
|
||||
>
|
||||
{wishlistProducts.map((product) => {
|
||||
const hasOldPrice =
|
||||
product.oldPrice &&
|
||||
!product.name.includes('Airpods') &&
|
||||
!product.name.includes('Apple Watch');
|
||||
return (
|
||||
<div key={product.id} className="py-5.5 flex items-center gap-5 justify-between">
|
||||
<div className="flex items-center gap-4 flex-grow min-w-0">
|
||||
<img
|
||||
src={product.image}
|
||||
alt={product.name}
|
||||
className="w-[72px] h-[72px] object-contain rounded-xl border border-gray-150 bg-white p-2 flex-shrink-0 shadow-xs"
|
||||
/>
|
||||
<div className="min-w-0 pr-2">
|
||||
{/* Title Link (Blue) */}
|
||||
<Link
|
||||
href={`/product/${product.id}`}
|
||||
onClick={onClose}
|
||||
className="text-[15px] font-bold text-[#0073EC] hover:text-black line-clamp-1 block transition-colors leading-snug mb-1"
|
||||
>
|
||||
{product.name}
|
||||
</Link>
|
||||
|
||||
{/* Price display */}
|
||||
<p className="text-[14.5px] font-extrabold text-gray-950 mb-1">
|
||||
{hasOldPrice && (
|
||||
<span className="text-gray-400 line-through mr-2 font-medium text-xs">
|
||||
${product.oldPrice?.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
<span>
|
||||
{product.name.includes('Airpods')
|
||||
? '$299 – $1,900'
|
||||
: product.name.includes('Apple Watch')
|
||||
? '$669 – $700'
|
||||
: `$${product.price.toLocaleString()}`}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
{/* Date */}
|
||||
<p className="text-xs text-gray-400 font-medium">
|
||||
{getAddedDate(product.id)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Right Buttons */}
|
||||
<div className="flex items-center gap-3.5 flex-shrink-0">
|
||||
<button
|
||||
onClick={() => alert(`Added ${product.name} to cart!`)}
|
||||
className="bg-[#0073EC] hover:bg-black text-white text-[12.5px] font-bold px-5 py-3 rounded-xl transition-colors cursor-pointer select-none shadow-xs whitespace-nowrap"
|
||||
>
|
||||
Move to Cart
|
||||
</button>
|
||||
{/* Inline trash close icon aligned with header X */}
|
||||
<button
|
||||
onClick={() => handleRemove(product.id)}
|
||||
className="w-9 h-9 rounded-full flex items-center justify-center text-gray-400 hover:text-red-500 transition-colors cursor-pointer -mr-2"
|
||||
title="Remove Item"
|
||||
>
|
||||
<X size={18} className="stroke-[2.5]" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
36
NewFile/src/src/layout/AnnouncementBar.tsx
Normal file
36
NewFile/src/src/layout/AnnouncementBar.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import React from 'react';
|
||||
import { Truck, Package } from 'lucide-react';
|
||||
|
||||
export const AnnouncementBar: React.FC = () => {
|
||||
return (
|
||||
<div className="bg-white border-b border-gray-200 py-2.5 text-[12px] text-gray-500 font-medium z-50">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 flex flex-col md:flex-row justify-between items-center gap-2">
|
||||
{/* Left Side Links */}
|
||||
<div className="flex flex-wrap items-center gap-x-5 gap-y-1 justify-center md:justify-start">
|
||||
<a href="#" className="hover:text-primary transition-colors flex items-center gap-1.5">
|
||||
<Package size={13} className="text-gray-400" />
|
||||
Track Order
|
||||
</a>
|
||||
<a href="#" className="hover:text-primary transition-colors">
|
||||
About Us
|
||||
</a>
|
||||
<a href="#" className="hover:text-primary transition-colors">
|
||||
Blog
|
||||
</a>
|
||||
<a href="#" className="hover:text-primary transition-colors">
|
||||
Contact Us
|
||||
</a>
|
||||
<a href="#" className="hover:text-primary transition-colors">
|
||||
FAQs
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Right Side Info */}
|
||||
<div className="flex items-center gap-1.5 text-gray-600 font-semibold text-center text-[12px]">
|
||||
<Truck size={14} className="text-primary" />
|
||||
<span>Free Shipping on orders over <span className="text-primary font-bold">$100!</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
207
NewFile/src/src/layout/Footer.tsx
Normal file
207
NewFile/src/src/layout/Footer.tsx
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Mail } from 'lucide-react';
|
||||
|
||||
export const Footer: React.FC = () => {
|
||||
const [email, setEmail] = useState('');
|
||||
|
||||
const handleSubscribe = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (email.trim()) {
|
||||
alert(`Thank you for subscribing with: ${email}`);
|
||||
setEmail('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<footer className="bg-white border-t border-gray-200 font-sans">
|
||||
|
||||
{/* 1. Bright Blue Newsletter Banner */}
|
||||
<div className="bg-[#0073EC] py-10 text-white">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1400px] flex flex-col lg:flex-row items-center justify-between gap-6">
|
||||
{/* Left Text Block with Mail Envelope Icon */}
|
||||
<div className="flex items-center gap-4 text-left">
|
||||
<div className="w-12 h-12 flex-shrink-0 flex items-center justify-center border-2 border-white/80 rounded-lg">
|
||||
<Mail className="w-6 h-6 stroke-[1.5]" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-[17px] font-extrabold tracking-tight">
|
||||
Join our newsletter and get $20 discount!
|
||||
</h3>
|
||||
<p className="text-[12px] text-blue-100 font-medium">
|
||||
Get E-mail updates about our latest shop and special offers.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Input and Subscribe Form */}
|
||||
<form onSubmit={handleSubscribe} className="w-full lg:w-[550px] max-w-[600px] flex items-center bg-white rounded-lg p-1 shadow-sm">
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
placeholder="Your email address"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="flex-grow px-4 py-1.5 bg-transparent text-gray-800 placeholder-gray-400 focus:outline-none text-[13px] font-medium"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-[#1E1E1E] hover:bg-primary text-white font-extrabold text-[11px] uppercase tracking-wider px-6 py-2.5 rounded-md transition-colors cursor-pointer"
|
||||
>
|
||||
Subscribe
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 2. Main Footer Links & Contact Info */}
|
||||
<div className="py-16 bg-[#F8F9FB] border-b border-gray-150">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1400px]">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-8">
|
||||
|
||||
{/* Column 1: Contact Us */}
|
||||
<div className="flex flex-col gap-4 text-left">
|
||||
<h4 className="text-[12.5px] font-bold text-[#1E293B] tracking-[0.05em] uppercase font-sans">Contact Us</h4>
|
||||
|
||||
<div className="text-[17px] font-bold text-gray-900 font-sans tracking-tight leading-tight">
|
||||
(+00) 123-456-789
|
||||
</div>
|
||||
|
||||
<p className="text-[13px] text-gray-500 font-semibold font-sans leading-relaxed max-w-[220px]">
|
||||
99 New Theme St. XY, USA 12345, Beside the Sun point land.
|
||||
</p>
|
||||
|
||||
<a
|
||||
href="mailto:demo@example.com"
|
||||
className="text-[13px] text-gray-500 font-semibold underline hover:text-[#0073EC] transition-colors font-sans"
|
||||
>
|
||||
demo@example.com
|
||||
</a>
|
||||
|
||||
{/* Social Media Links */}
|
||||
<div className="flex items-center gap-4 mt-4">
|
||||
<a href="#" className="text-gray-600 hover:text-[#0073EC] transition-colors" title="Facebook">
|
||||
<svg className="w-5 h-5 fill-current" viewBox="0 0 24 24">
|
||||
<path d="M9 8h-3v4h3v12h5v-12h3.642l.358-4h-4v-1.667c0-.955.192-1.333 1.115-1.333h2.885v-5h-3.808c-3.596 0-5.192 1.583-5.192 4.615v3.385z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#" className="text-gray-600 hover:text-[#0073EC] transition-colors" title="Twitter/X">
|
||||
<svg className="w-4.5 h-4.5 fill-current" viewBox="0 0 24 24">
|
||||
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#" className="text-gray-600 hover:text-[#0073EC] transition-colors" title="Instagram">
|
||||
<svg className="w-5 h-5 fill-current" viewBox="0 0 24 24">
|
||||
<path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#" className="text-gray-600 hover:text-[#0073EC] transition-colors" title="TikTok">
|
||||
<svg className="w-4.5 h-4.5 fill-current" viewBox="0 0 24 24">
|
||||
<path d="M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.17-2.86-.74-3.99-1.72-.08-.07-.17-.17-.24-.25v5.13c-.02 3.42-1.92 6.78-5.26 7.82-3.34 1.04-7.25-.33-8.81-3.41-1.57-3.07-.48-7.3 2.5-8.88 1.4-.74 3.01-.93 4.55-.58v4.01c-.88-.23-1.84-.13-2.63.34-.8.47-1.34 1.34-1.37 2.27-.02 1.32.96 2.55 2.27 2.76 1.3.2 2.6-.62 2.97-1.87.12-.41.15-.85.15-1.28V.02z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#" className="text-gray-600 hover:text-[#0073EC] transition-colors" title="Pinterest">
|
||||
<svg className="w-4.5 h-4.5 fill-current" viewBox="0 0 24 24">
|
||||
<path d="M12 0c-6.627 0-12 5.372-12 12 0 5.084 3.163 9.426 7.627 11.174-.105-.949-.2-2.405.042-3.441.218-.937 1.407-5.965 1.407-5.965s-.359-.719-.359-1.782c0-1.668.967-2.914 2.171-2.914 1.023 0 1.518.769 1.518 1.69 0 1.029-.655 2.568-.993 3.994-.283 1.194.599 2.169 1.777 2.169 2.133 0 3.772-2.249 3.772-5.495 0-2.873-2.064-4.882-5.012-4.882-3.414 0-5.418 2.561-5.418 5.207 0 1.031.397 2.138.893 2.738.098.119.112.224.083.345-.091.377-.294 1.197-.333 1.36-.053.218-.173.264-.399.158-1.492-.695-2.424-2.876-2.424-4.629 0-3.769 2.739-7.229 7.892-7.229 4.144 0 7.365 2.953 7.365 6.899 0 4.117-2.595 7.431-6.199 7.431-1.21 0-2.348-.628-2.738-1.373 0 0-.599 2.282-.744 2.84-.282 1.084-1.047 2.443-1.558 3.272 1.124.348 2.316.537 3.551.537 6.628 0 12-5.373 12-12 0-6.628-5.372-12-12-12z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Column 2: Get to know Us */}
|
||||
<div className="text-left">
|
||||
<h4 className="text-[12.5px] font-bold text-[#1E293B] tracking-[0.05em] uppercase mb-5 font-sans">Get to know Us</h4>
|
||||
<ul className="space-y-3.5 text-[13px] font-semibold text-gray-500 font-sans">
|
||||
<li><a href="#" className="hover:text-[#0073EC] transition-colors">About Us</a></li>
|
||||
<li><a href="#" className="hover:text-[#0073EC] transition-colors">Term & Policy</a></li>
|
||||
<li><a href="#" className="hover:text-[#0073EC] transition-colors">Careers</a></li>
|
||||
<li><a href="#" className="hover:text-[#0073EC] transition-colors">News & Blog</a></li>
|
||||
<li><a href="#" className="hover:text-[#0073EC] transition-colors">Contact Us</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Column 3: Information */}
|
||||
<div className="text-left">
|
||||
<h4 className="text-[12.5px] font-bold text-[#1E293B] tracking-[0.05em] uppercase mb-5 font-sans">Information</h4>
|
||||
<ul className="space-y-3.5 text-[13px] font-semibold text-gray-500 font-sans">
|
||||
<li><a href="#" className="hover:text-[#0073EC] transition-colors">Help Center</a></li>
|
||||
<li><a href="#" className="hover:text-[#0073EC] transition-colors">Feedback</a></li>
|
||||
<li><a href="#" className="hover:text-[#0073EC] transition-colors">FAQs</a></li>
|
||||
<li><a href="#" className="hover:text-[#0073EC] transition-colors">Size Guide</a></li>
|
||||
<li><a href="#" className="hover:text-[#0073EC] transition-colors">Payments</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Column 4: Orders & Returns */}
|
||||
<div className="text-left">
|
||||
<h4 className="text-[12.5px] font-bold text-[#1E293B] tracking-[0.05em] uppercase mb-5 font-sans">Orders & Returns</h4>
|
||||
<ul className="space-y-3.5 text-[13px] font-semibold text-gray-500 font-sans">
|
||||
<li><a href="#" className="hover:text-[#0073EC] transition-colors">Track Order</a></li>
|
||||
<li><a href="#" className="hover:text-[#0073EC] transition-colors">Delivery</a></li>
|
||||
<li><a href="#" className="hover:text-[#0073EC] transition-colors">Services</a></li>
|
||||
<li><a href="#" className="hover:text-[#0073EC] transition-colors">Returns</a></li>
|
||||
<li><a href="#" className="hover:text-[#0073EC] transition-colors">Exchange</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Column 5: Our Store */}
|
||||
<div className="text-left">
|
||||
<h4 className="text-[12.5px] font-bold text-[#1E293B] tracking-[0.05em] uppercase mb-5 font-sans">Our Store</h4>
|
||||
<ul className="space-y-3.5 text-[13px] font-semibold text-gray-500 font-sans">
|
||||
<li><a href="#" className="hover:text-[#0073EC] transition-colors">Affiliate</a></li>
|
||||
<li><a href="#" className="hover:text-[#0073EC] transition-colors">Best Seller</a></li>
|
||||
<li><a href="#" className="hover:text-[#0073EC] transition-colors">New Products</a></li>
|
||||
<li><a href="#" className="hover:text-[#0073EC] transition-colors">On Sale</a></li>
|
||||
<li><a href="#" className="hover:text-[#0073EC] transition-colors">Featured</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 3. Bottom Bar with copyright and payment methods */}
|
||||
<div className="py-8 bg-[#F0F2F5]">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1400px] flex flex-col md:flex-row justify-between items-center gap-4">
|
||||
<p className="text-[12.5px] text-gray-500 font-medium text-center md:text-left">
|
||||
© 2026 ifixkart
|
||||
</p>
|
||||
|
||||
{/* Payment Card Logos */}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Visa */}
|
||||
<div className="bg-white border border-gray-200 rounded px-2.5 py-1 text-[#1A1F71] font-bold italic text-[11px] shadow-2xs select-none" title="Visa">
|
||||
Visa
|
||||
</div>
|
||||
{/* CB */}
|
||||
<div className="bg-white border border-gray-200 rounded px-2.5 py-1 text-emerald-600 font-black text-[11px] shadow-2xs select-none" title="CB">
|
||||
CB
|
||||
</div>
|
||||
{/* Amex */}
|
||||
<div className="bg-white border border-gray-200 rounded px-2.5 py-1 text-[#0070CD] font-bold text-[10px] shadow-2xs select-none" title="American Express">
|
||||
Amex
|
||||
</div>
|
||||
{/* Maestro */}
|
||||
<div className="bg-white border border-gray-200 rounded px-2 py-0.5 shadow-2xs select-none flex items-center gap-1" title="Maestro">
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-[#EB001B] opacity-80" />
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-[#00A2E8] -ml-2 opacity-80" />
|
||||
<span className="text-[10px] font-bold text-gray-500">maestro</span>
|
||||
</div>
|
||||
{/* Mastercard */}
|
||||
<div className="bg-white border border-gray-200 rounded px-2 py-0.5 shadow-2xs select-none flex items-center gap-1" title="Mastercard">
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-[#EB001B] opacity-90" />
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-[#F79E1B] -ml-2 opacity-90" />
|
||||
<span className="text-[10px] font-bold text-gray-500">mastercard</span>
|
||||
</div>
|
||||
{/* PayPal */}
|
||||
<div className="bg-white border border-gray-200 rounded px-2.5 py-1 text-[#003087] font-black italic text-[11px] shadow-2xs select-none" title="PayPal">
|
||||
PayPal
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
124
NewFile/src/src/layout/Header.tsx
Normal file
124
NewFile/src/src/layout/Header.tsx
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Heart, User, ShoppingCart, Menu, X, Search } from 'lucide-react';
|
||||
import { SearchBar } from '../components/SearchBar';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
interface HeaderProps {
|
||||
onMenuToggle: () => void;
|
||||
cartCount: number;
|
||||
wishlistCount: number;
|
||||
onWishlistClick?: () => void;
|
||||
}
|
||||
|
||||
export const Header: React.FC<HeaderProps> = ({ onMenuToggle, cartCount = 0, wishlistCount = 0, onWishlistClick }) => {
|
||||
const [mobileSearchOpen, setMobileSearchOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<header className="bg-white pt-6 pb-4 relative z-[110]">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 flex items-center justify-between gap-5">
|
||||
{/* Mobile Hamburger Menu */}
|
||||
<button
|
||||
onClick={onMenuToggle}
|
||||
className="lg:hidden p-2 text-gray-600 hover:text-primary transition-colors hover:bg-gray-50 rounded-lg"
|
||||
aria-label="Open Menu"
|
||||
>
|
||||
<Menu size={24} className="stroke-[2.5]" />
|
||||
</button>
|
||||
|
||||
{/* Brand Logo */}
|
||||
<Link href="/" className="flex items-center select-none tracking-tighter hover:opacity-95 transition-opacity">
|
||||
<span className="text-[#0073EC] text-[38px] font-black leading-none">iFix</span>
|
||||
<span className="text-gray-900 text-[38px] font-black leading-none">Kart</span>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Search Bar */}
|
||||
<div className="hidden lg:block flex-grow max-w-[660px] mx-2 xl:mx-6">
|
||||
<SearchBar />
|
||||
</div>
|
||||
|
||||
{/* Actions (Wishlist, Account, Cart) */}
|
||||
<div className="flex items-center gap-2 md:gap-5 flex-shrink-0">
|
||||
{/* Mobile Search Toggle */}
|
||||
<button
|
||||
onClick={() => setMobileSearchOpen(!mobileSearchOpen)}
|
||||
className="lg:hidden p-2 text-gray-600 hover:text-primary transition-colors hover:bg-gray-50 rounded-lg"
|
||||
>
|
||||
<Search size={22} className="stroke-[2.5]" />
|
||||
</button>
|
||||
|
||||
{/* Wishlist */}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (onWishlistClick) {
|
||||
onWishlistClick();
|
||||
} else {
|
||||
window.dispatchEvent(new Event('openWishlistModal'));
|
||||
}
|
||||
}}
|
||||
className="relative p-2 text-gray-600 hover:text-primary transition-colors hover:bg-gray-50 rounded-lg group cursor-pointer focus:outline-none"
|
||||
>
|
||||
<Heart size={24} className="group-hover:scale-105 transition-transform" />
|
||||
<span className="absolute top-0 right-0 w-5 h-5 bg-[#0073EC] text-white text-[10px] font-bold rounded-full flex items-center justify-center border-2 border-white shadow-xs">
|
||||
{wishlistCount}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Account */}
|
||||
<a
|
||||
href="#"
|
||||
className="flex items-center gap-2.5 p-2 text-gray-600 hover:text-primary transition-colors hover:bg-gray-50 rounded-lg group"
|
||||
>
|
||||
<User size={24} className="group-hover:scale-105 transition-transform text-gray-600 group-hover:text-primary transition-colors" />
|
||||
<div className="text-left hidden md:block text-[11.5px] leading-tight font-medium">
|
||||
<span className="text-gray-400 block font-normal">My Account</span>
|
||||
<span className="text-gray-800 font-bold group-hover:text-primary transition-colors text-[13.5px]">Login</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
{/* Cart */}
|
||||
<a
|
||||
href="#"
|
||||
className="flex items-center gap-2.5 p-2 text-gray-600 hover:text-primary transition-colors hover:bg-gray-50 rounded-lg group relative"
|
||||
>
|
||||
<div className="relative">
|
||||
<ShoppingCart size={24} className="group-hover:scale-105 transition-transform" />
|
||||
<span className="absolute -top-1.5 -right-1.5 w-5 h-5 bg-[#0073EC] text-white text-[10px] font-bold rounded-full flex items-center justify-center border-2 border-white shadow-xs">
|
||||
{cartCount}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-left hidden md:block text-[11.5px] leading-tight font-medium">
|
||||
<span className="text-gray-400 block font-normal">My Cart</span>
|
||||
<span className="text-gray-800 font-bold group-hover:text-primary transition-colors text-[13.5px]">$0.00</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Search Overlay */}
|
||||
<AnimatePresence>
|
||||
{mobileSearchOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
className="lg:hidden border-t border-gray-100 px-4 pt-3 pb-2 bg-white"
|
||||
>
|
||||
<div className="flex gap-2">
|
||||
<SearchBar />
|
||||
<button
|
||||
onClick={() => setMobileSearchOpen(false)}
|
||||
className="p-2 text-gray-400 hover:text-gray-650"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
101
NewFile/src/src/layout/Navbar.tsx
Normal file
101
NewFile/src/src/layout/Navbar.tsx
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { ChevronDown, Percent } from 'lucide-react';
|
||||
import { CategorySidebar } from '../components/CategorySidebar';
|
||||
|
||||
export const Navbar: React.FC = () => {
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const [isSticky, setIsSticky] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
if (window.scrollY > 120) {
|
||||
setIsSticky(true);
|
||||
} else {
|
||||
setIsSticky(false);
|
||||
}
|
||||
// Always close dropdown on scroll
|
||||
setDropdownOpen(false);
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<nav className={`bg-white hidden xl:block transition-all duration-200 z-[100] ${
|
||||
isSticky
|
||||
? 'fixed top-0 left-0 right-0 shadow-md py-0 border-b border-gray-150'
|
||||
: 'relative pt-3.5'
|
||||
}`}>
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
{/* All Categories Header Anchor Box with Dropdown */}
|
||||
<div className="relative">
|
||||
<div
|
||||
onClick={() => setDropdownOpen(!dropdownOpen)}
|
||||
className="w-[280px] bg-primary text-white py-4 px-6 font-bold text-[13px] flex items-center gap-3 rounded-t-lg select-none cursor-pointer"
|
||||
>
|
||||
<span className="flex flex-col gap-1 w-4">
|
||||
<span className="h-[2px] bg-white w-full rounded" />
|
||||
<span className="h-[2px] bg-white w-4/5 rounded" />
|
||||
<span className="h-[2px] bg-white w-full rounded" />
|
||||
</span>
|
||||
All Categories
|
||||
</div>
|
||||
|
||||
{isSticky && dropdownOpen && (
|
||||
<div className="absolute top-full left-0 z-50 mt-0">
|
||||
<CategorySidebar isDropdown={true} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Links */}
|
||||
<div className="flex items-center gap-10 ml-10">
|
||||
<Link href="/" className="text-primary font-bold text-[14px] hover:text-primary transition-colors py-[14.5px]">
|
||||
Home
|
||||
</Link>
|
||||
|
||||
{/* Shop Dropdown */}
|
||||
<div className="relative group py-[14.5px]">
|
||||
<Link href="/shop" className="text-gray-700 font-bold text-[14px] hover:text-primary transition-colors flex items-center gap-1">
|
||||
Shop
|
||||
<ChevronDown size={14} className="text-gray-400 group-hover:text-primary transition-colors" />
|
||||
</Link>
|
||||
<div className="absolute top-[48px] left-0 bg-white border border-gray-100 shadow-lg py-2 rounded-lg w-48 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50">
|
||||
<Link href="/shop?layout=grid" className="block px-4 py-2 text-xs font-semibold text-gray-600 hover:bg-gray-50 hover:text-primary transition-colors">Grid Layout</Link>
|
||||
<Link href="/shop?layout=list" className="block px-4 py-2 text-xs font-semibold text-gray-600 hover:bg-gray-50 hover:text-primary transition-colors">List Layout</Link>
|
||||
<Link href="#" className="block px-4 py-2 text-xs font-semibold text-gray-600 hover:bg-gray-50 hover:text-primary transition-colors">Product Detail Page</Link>
|
||||
<Link href="#" className="block px-4 py-2 text-xs font-semibold text-gray-600 hover:bg-gray-50 hover:text-primary transition-colors">Cart Page</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Service */}
|
||||
<a href="#" className="text-gray-700 font-bold text-[14px] hover:text-primary transition-colors py-[14.5px]">
|
||||
Service
|
||||
</a>
|
||||
|
||||
{/* Top Deals */}
|
||||
<a href="#" className="text-gray-700 font-bold text-[14px] hover:text-primary transition-colors py-[14.5px]">
|
||||
Top Deals
|
||||
</a>
|
||||
|
||||
{/* Featured Products */}
|
||||
<a href="#" className="text-gray-700 font-bold text-[14px] hover:text-primary transition-colors py-[14.5px]">
|
||||
Featured Products
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Super Discount banner */}
|
||||
<a href="#" className="flex items-center gap-2 text-red-500 hover:text-red-650 font-bold text-[13px] transition-colors py-[14.5px]">
|
||||
<Percent size={14} className="stroke-[2.5] text-red-500 animate-pulse" />
|
||||
Super Discount
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
10
NewFile/src/src/main.tsx
Normal file
10
NewFile/src/src/main.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import './styles/index.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
79
NewFile/src/src/sections/ArticlesSection.tsx
Normal file
79
NewFile/src/src/sections/ArticlesSection.tsx
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { blogs } from '../utils/mockData';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export const ArticlesSection: React.FC = () => {
|
||||
return (
|
||||
<section className="py-8 bg-bgLight font-sans">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
|
||||
{/* Section Header */}
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-xl md:text-2xl font-extrabold text-gray-900 tracking-tight font-outfit">
|
||||
From Our Articles
|
||||
</h2>
|
||||
<a
|
||||
href="#"
|
||||
className="text-xs font-bold text-gray-900 hover:text-primary transition-colors flex items-center gap-1 group"
|
||||
>
|
||||
View All
|
||||
<ArrowRight size={14} className="group-hover:translate-x-1 transition-transform stroke-[2.5]" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Blog Cards Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{blogs.map((blog) => (
|
||||
<motion.article
|
||||
key={blog.id}
|
||||
whileHover={{ y: -5 }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 25 }}
|
||||
className="bg-white rounded-theme border border-gray-150 overflow-hidden shadow-sm hover:shadow-soft-hover transition-all duration-300 flex flex-col justify-between"
|
||||
>
|
||||
{/* Image Frame */}
|
||||
<div className="w-full aspect-[16/10] overflow-hidden bg-gray-50 relative select-none">
|
||||
<img
|
||||
src={blog.image}
|
||||
alt={blog.title}
|
||||
className="w-full h-full object-cover transition-transform duration-500 hover:scale-105"
|
||||
/>
|
||||
<span className="absolute top-3 left-3 bg-white/90 backdrop-blur-xs text-[10px] font-extrabold px-2.5 py-1 rounded-md text-gray-700 shadow-sm">
|
||||
{blog.readTime}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Text Body */}
|
||||
<div className="p-6 text-left flex-1 flex flex-col justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-[11px] text-gray-400 font-semibold mb-2">
|
||||
<span>{blog.author}</span>
|
||||
<span className="w-1 h-1 bg-gray-300 rounded-full" />
|
||||
<span>{blog.date}</span>
|
||||
</div>
|
||||
<h4 className="text-[13.5px] font-bold text-gray-900 hover:text-primary transition-colors leading-snug mb-2 cursor-pointer">
|
||||
{blog.title}
|
||||
</h4>
|
||||
<p className="text-xs text-gray-500 font-medium leading-relaxed line-clamp-2 mb-4">
|
||||
{blog.excerpt}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
className="inline-flex items-center gap-1 text-xs font-bold text-primary hover:text-primary-dark transition-colors group/link mt-auto pt-2"
|
||||
>
|
||||
Read More
|
||||
<ArrowRight size={13} className="group-hover/link:translate-x-0.5 transition-transform" />
|
||||
</a>
|
||||
</div>
|
||||
</motion.article>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
56
NewFile/src/src/sections/BrandsSection.tsx
Normal file
56
NewFile/src/src/sections/BrandsSection.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
interface Brand {
|
||||
name: string;
|
||||
hoverColor: string;
|
||||
fontStyle: string;
|
||||
}
|
||||
|
||||
export const BrandsSection: React.FC = () => {
|
||||
const brandList: Brand[] = [
|
||||
{ name: 'amazon', hoverColor: 'text-[#FF9900]', fontStyle: 'font-extrabold italic tracking-tighter' },
|
||||
{ name: 'AMD', hoverColor: 'text-[#ED1C24]', fontStyle: 'font-sans font-black tracking-normal' },
|
||||
{ name: 'facebook', hoverColor: 'text-[#1877F2]', fontStyle: 'font-serif font-bold lowercase tracking-tight' },
|
||||
{ name: 'logitech', hoverColor: 'text-[#00B0FF]', fontStyle: 'font-sans font-bold lowercase' },
|
||||
{ name: 'apper', hoverColor: 'text-[#8E44AD]', fontStyle: 'font-mono font-bold tracking-widest' },
|
||||
{ name: 'hooli', hoverColor: 'text-[#2ECC71]', fontStyle: 'font-sans font-extrabold tracking-tight lowercase' },
|
||||
{ name: 'FedEx', hoverColor: 'text-[#4F2D7F]', fontStyle: 'font-sans font-extrabold tracking-tighter' },
|
||||
{ name: 'PayPal', hoverColor: 'text-[#003087]', fontStyle: 'font-serif font-black italic tracking-tight' },
|
||||
{ name: 'NETFLIX', hoverColor: 'text-[#E50914]', fontStyle: 'font-sans font-bold tracking-widest uppercase' },
|
||||
{ name: 'Spotify', hoverColor: 'text-[#1DB954]', fontStyle: 'font-sans font-extrabold tracking-tight' },
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="py-8 bg-bgLight font-sans">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
|
||||
{/* Section Header */}
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-xl md:text-2xl font-extrabold text-gray-900 tracking-tight font-outfit">
|
||||
Popular Brands
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* Brand Grid */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
|
||||
{brandList.map((brand) => (
|
||||
<motion.div
|
||||
key={brand.name}
|
||||
whileHover={{ y: -4, scale: 1.02 }}
|
||||
transition={{ type: 'spring', stiffness: 350, damping: 25 }}
|
||||
className="bg-white border border-gray-150 rounded-theme h-16 flex items-center justify-center p-4 shadow-sm hover:shadow-soft-hover transition-all duration-300 group cursor-pointer"
|
||||
>
|
||||
<span className={`text-[17px] font-bold text-gray-300 group-hover:${brand.hoverColor} transition-colors select-none ${brand.fontStyle}`}>
|
||||
{brand.name}
|
||||
</span>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
59
NewFile/src/src/sections/CategoriesSection.tsx
Normal file
59
NewFile/src/src/sections/CategoriesSection.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { categories } from '../utils/mockData';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export const CategoriesSection: React.FC = () => {
|
||||
return (
|
||||
<section className="py-8 bg-white font-sans">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{/* Section Header */}
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-xl md:text-2xl font-extrabold text-gray-900 tracking-tight font-outfit">
|
||||
Shop By Categories
|
||||
</h2>
|
||||
<a
|
||||
href="#"
|
||||
className="text-xs font-bold text-gray-900 hover:text-primary transition-colors flex items-center gap-1 group"
|
||||
>
|
||||
View All
|
||||
<ArrowRight size={14} className="group-hover:translate-x-1 transition-transform stroke-[2.5]" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Categories List */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-6">
|
||||
{categories.map((cat) => (
|
||||
<motion.a
|
||||
key={cat.id}
|
||||
href="#"
|
||||
whileHover={{ y: -6 }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 25 }}
|
||||
className="flex flex-col items-center group text-center"
|
||||
>
|
||||
{/* Image Box */}
|
||||
<div className="w-full aspect-square bg-[#F8F9FB] rounded-theme flex items-center justify-center p-6 border border-transparent group-hover:border-blue-100 group-hover:bg-blue-50/10 transition-all duration-300 relative overflow-hidden">
|
||||
<motion.img
|
||||
whileHover={{ scale: 1.06 }}
|
||||
src={cat.image}
|
||||
alt={cat.name}
|
||||
className="max-h-full max-w-full object-contain rounded-lg drop-shadow-md"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Title & Count */}
|
||||
<h3 className="text-[13px] font-bold text-gray-800 mt-3.5 group-hover:text-primary transition-colors">
|
||||
{cat.name}
|
||||
</h3>
|
||||
<span className="text-[10px] font-bold text-gray-400 mt-0.5 tracking-wide">
|
||||
{cat.productCount} Products
|
||||
</span>
|
||||
</motion.a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
52
NewFile/src/src/sections/FeaturesSection.tsx
Normal file
52
NewFile/src/src/sections/FeaturesSection.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import React from 'react';
|
||||
import { Truck, RotateCcw, Headphones, ShieldCheck } from 'lucide-react';
|
||||
|
||||
export const FeaturesSection: React.FC = () => {
|
||||
const items = [
|
||||
{
|
||||
icon: <Truck size={24} className="text-primary" />,
|
||||
title: 'Free Shipping',
|
||||
desc: 'Free shipping on all orders over $100',
|
||||
},
|
||||
{
|
||||
icon: <RotateCcw size={24} className="text-primary" />,
|
||||
title: 'Easy Returns',
|
||||
desc: '30 days money back guarantee policy',
|
||||
},
|
||||
{
|
||||
icon: <Headphones size={24} className="text-primary" />,
|
||||
title: '24/7 Support',
|
||||
desc: 'Friendly 24/7 support line whenever needed',
|
||||
},
|
||||
{
|
||||
icon: <ShieldCheck size={24} className="text-primary" />,
|
||||
title: 'Secure Payment',
|
||||
desc: '100% secure payment gateways and protocols',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="py-12 bg-white font-sans border-t border-gray-100">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{items.map((item, idx) => (
|
||||
<div
|
||||
key={item.title}
|
||||
className={`flex items-center gap-4 text-left ${
|
||||
idx < items.length - 1 ? 'lg:border-r lg:border-gray-100 pr-6' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="p-3 bg-blue-50/50 rounded-xl flex-shrink-0">
|
||||
{item.icon}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-gray-800 mb-0.5">{item.title}</h4>
|
||||
<p className="text-[11px] text-gray-400 font-semibold leading-relaxed">{item.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
94
NewFile/src/src/sections/HeroSection.tsx
Normal file
94
NewFile/src/src/sections/HeroSection.tsx
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||
import { Autoplay, Pagination } from 'swiper/modules';
|
||||
import { CategorySidebar } from '../components/CategorySidebar';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
// Import Swiper styles
|
||||
import 'swiper/css';
|
||||
import 'swiper/css/pagination';
|
||||
|
||||
export const HeroSection: React.FC = () => {
|
||||
const slides = [
|
||||
{
|
||||
id: 1,
|
||||
badge: 'Limited Time Offer',
|
||||
title: 'Ultimate Virtual Reality\nExperience Box',
|
||||
subtitle: 'Experience the Future of Entertainment Today',
|
||||
image: 'https://images.unsplash.com/photo-1593508512255-86ab42a8e620?w=600&auto=format&fit=crop&q=80',
|
||||
gradient: 'from-blue-600 via-indigo-600 to-purple-650',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
badge: 'Exclusive Launch',
|
||||
title: 'Next-Gen Performance\nPro Gaming Gear',
|
||||
subtitle: 'Unleash Your Full Creative Power Everywhere',
|
||||
image: 'https://images.unsplash.com/photo-1606144042614-b2417e99c4e3?w=600&auto=format&fit=crop&q=80',
|
||||
gradient: 'from-slate-900 via-blue-900 to-indigo-950',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="pt-0 pb-6 bg-white font-sans">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex gap-6 items-start">
|
||||
{/* Left Vertical Categories Sidebar */}
|
||||
<CategorySidebar />
|
||||
|
||||
{/* Right Swiper Hero Slider */}
|
||||
<div className="flex-1 min-w-0 rounded-theme overflow-hidden shadow-sm h-[455px]">
|
||||
<Swiper
|
||||
modules={[Autoplay, Pagination]}
|
||||
pagination={{ clickable: true }}
|
||||
autoplay={{ delay: 6000, disableOnInteraction: false }}
|
||||
loop={true}
|
||||
className="h-full"
|
||||
>
|
||||
{slides.map((slide) => (
|
||||
<SwiperSlide key={slide.id}>
|
||||
<div className={`h-full w-full bg-gradient-to-r ${slide.gradient} p-8 md:p-12 lg:p-16 flex flex-col md:flex-row items-center justify-between gap-8 text-white relative`}>
|
||||
|
||||
{/* Content */}
|
||||
<div className="text-left flex-1 z-10">
|
||||
<span className="inline-block bg-red-650 text-[11px] font-extrabold uppercase tracking-wider px-3.5 py-1 rounded-full mb-4">
|
||||
{slide.badge}
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl lg:text-5xl font-extrabold tracking-tight leading-[1.1] whitespace-pre-line mb-4 font-outfit">
|
||||
{slide.title}
|
||||
</h2>
|
||||
<p className="text-sm md:text-base text-blue-100 font-medium mb-8">
|
||||
{slide.subtitle}
|
||||
</p>
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="bg-primary text-white hover:bg-primary-hover text-xs md:text-sm font-semibold px-8 py-3.5 rounded-lg flex items-center shadow-md transition-colors"
|
||||
>
|
||||
Shop Now
|
||||
</motion.button>
|
||||
</div>
|
||||
|
||||
{/* Image */}
|
||||
<div className="flex-1 w-full md:w-auto h-[220px] md:h-full relative flex items-center justify-center">
|
||||
<motion.img
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
src={slide.image}
|
||||
alt={slide.title}
|
||||
className="max-h-full max-w-full object-contain rounded-xl drop-shadow-2xl mix-blend-screen"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
221
NewFile/src/src/sections/HotDealsSection.tsx
Normal file
221
NewFile/src/src/sections/HotDealsSection.tsx
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { products } from '../utils/mockData';
|
||||
import { CountdownTimer } from '../components/CountdownTimer';
|
||||
import { RatingStars } from '../components/RatingStars';
|
||||
import { ChevronLeft, ChevronRight, ShoppingCart, Heart, Eye } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export const HotDealsSection: React.FC = () => {
|
||||
// Hot Deal product (Jajot J2)
|
||||
const hotDealProduct = products.find((p) => p.isHot) || products[0];
|
||||
|
||||
// Monthly Featured items (6 items displayed in mini horizontal cards)
|
||||
const featuredProducts = products.filter((p) => p.isFeatured).slice(0, 6);
|
||||
|
||||
const soldCount = hotDealProduct.soldCount || 85;
|
||||
const totalStock = hotDealProduct.totalStock || 120;
|
||||
const percentSold = Math.round((soldCount / totalStock) * 100);
|
||||
|
||||
return (
|
||||
<section className="py-8 bg-bgLight font-sans">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
|
||||
{/* Left Column: Hot Deals */}
|
||||
<div className="lg:col-span-1">
|
||||
<div className="bg-white rounded-none border-2 border-primary/20 overflow-hidden shadow-sm flex flex-col justify-between p-6 h-full relative">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center border-b border-gray-100 pb-4 mb-6">
|
||||
<h3 className="text-lg font-extrabold text-gray-900 tracking-tight font-outfit">
|
||||
Hot Deals
|
||||
</h3>
|
||||
<div className="flex gap-1.5">
|
||||
<button className="w-7 h-7 rounded-full border border-gray-200 text-gray-400 hover:text-primary hover:border-primary flex items-center justify-center transition-colors">
|
||||
<ChevronLeft size={14} className="stroke-[2.5]" />
|
||||
</button>
|
||||
<button className="w-7 h-7 rounded-full border border-gray-200 text-gray-400 hover:text-primary hover:border-primary flex items-center justify-center transition-colors">
|
||||
<ChevronRight size={14} className="stroke-[2.5]" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Badge */}
|
||||
<div className="absolute top-20 right-6">
|
||||
<span className="bg-primary text-white text-[10px] font-extrabold px-2.5 py-1 rounded-md">
|
||||
-{hotDealProduct.discount}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Product Image */}
|
||||
<div className="w-full flex-1 flex items-center justify-center min-h-[200px] mb-6">
|
||||
<img
|
||||
src={hotDealProduct.image}
|
||||
alt={hotDealProduct.name}
|
||||
className="max-h-[180px] max-w-full object-contain rounded-lg drop-shadow-md"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Product Info */}
|
||||
<div className="text-left mt-2">
|
||||
{/* Rating */}
|
||||
<RatingStars rating={hotDealProduct.rating} size={13} className="mb-2" />
|
||||
|
||||
{/* Title */}
|
||||
<h4 className="text-sm font-extrabold text-gray-800 leading-snug hover:text-primary cursor-pointer transition-colors mb-2">
|
||||
{hotDealProduct.name}
|
||||
</h4>
|
||||
|
||||
{/* Pricing */}
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<span className="text-lg font-extrabold text-gray-900">${hotDealProduct.price}</span>
|
||||
{hotDealProduct.oldPrice && (
|
||||
<span className="text-xs text-gray-400 line-through font-semibold">${hotDealProduct.oldPrice}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Progress bar */}
|
||||
<div className="mb-6">
|
||||
<div className="flex justify-between text-[11px] text-gray-500 font-bold mb-1.5">
|
||||
<span>Sold: {soldCount} Items</span>
|
||||
<span>Available: {totalStock - soldCount} Items</span>
|
||||
</div>
|
||||
<div className="w-full h-2 bg-gray-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-primary rounded-full transition-all duration-500"
|
||||
style={{ width: `${percentSold}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Countdown Timer */}
|
||||
<div className="flex flex-col items-center p-4 bg-red-50/50 border border-red-100/55 rounded-2xl mb-2">
|
||||
<span className="text-[10px] text-red-500 font-extrabold uppercase tracking-wider mb-2">
|
||||
Offer Expires In:
|
||||
</span>
|
||||
{hotDealProduct.timeRemaining && (
|
||||
<CountdownTimer
|
||||
initialDays={hotDealProduct.timeRemaining.days}
|
||||
initialHours={hotDealProduct.timeRemaining.hours}
|
||||
initialMinutes={hotDealProduct.timeRemaining.minutes}
|
||||
initialSeconds={hotDealProduct.timeRemaining.seconds}
|
||||
size="md"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Monthly Featured Items */}
|
||||
<div className="lg:col-span-2">
|
||||
<div className="bg-white rounded-none border border-gray-150 p-6 h-full flex flex-col justify-between">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center border-b border-gray-100 pb-4 mb-6">
|
||||
<h3 className="text-lg font-extrabold text-gray-900 tracking-tight font-outfit">
|
||||
Monthly Featured Item
|
||||
</h3>
|
||||
<div className="flex gap-1.5">
|
||||
<button className="w-7 h-7 rounded-full border border-gray-200 text-gray-400 hover:text-primary hover:border-primary flex items-center justify-center transition-colors">
|
||||
<ChevronLeft size={14} className="stroke-[2.5]" />
|
||||
</button>
|
||||
<button className="w-7 h-7 rounded-full border border-gray-200 text-gray-400 hover:text-primary hover:border-primary flex items-center justify-center transition-colors">
|
||||
<ChevronRight size={14} className="stroke-[2.5]" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Grid of Mini Horizontal Cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5 flex-1 mt-4">
|
||||
{featuredProducts.map((product) => (
|
||||
<div
|
||||
key={product.id}
|
||||
className="flex gap-3 p-4 bg-white border border-gray-150 rounded-xl hover:border-[#0073EC]/30 hover:shadow-xs transition-all duration-300 group text-left relative"
|
||||
>
|
||||
{/* Thumbnail */}
|
||||
<div className="w-20 h-20 bg-[#F8F9FB] rounded-lg p-2 flex-shrink-0 flex items-center justify-center relative overflow-hidden">
|
||||
<img
|
||||
src={product.image}
|
||||
alt={product.name}
|
||||
className="max-h-full max-w-full object-contain rounded drop-shadow group-hover:scale-105 transition-transform duration-300"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Meta info */}
|
||||
<div className="flex-1 min-w-0 flex flex-col justify-start">
|
||||
<div>
|
||||
<h5 className="text-[12px] font-bold text-gray-800 line-clamp-1 leading-tight mb-0.5" title={product.name}>
|
||||
{product.name}
|
||||
</h5>
|
||||
<RatingStars rating={product.rating} size={11} className="mb-0.5" />
|
||||
|
||||
{/* Price */}
|
||||
<div className="flex items-center gap-1.5 my-1">
|
||||
<span className="text-[13px] font-extrabold text-red-500">
|
||||
${product.price}
|
||||
</span>
|
||||
{product.oldPrice && (
|
||||
<span className="text-[10px] text-gray-400 line-through font-medium">
|
||||
${product.oldPrice}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Features List */}
|
||||
<p className="text-[10px] text-gray-400 font-semibold line-clamp-1 mb-1.5">
|
||||
{product.features ? product.features.slice(0, 2).join(' • ') : "Functional Crown • 1.78\" Amoled"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Action Triggers */}
|
||||
<div className="flex items-center justify-end gap-2 mt-2 pt-2 border-t border-gray-50">
|
||||
<button
|
||||
onClick={() => alert(`Added ${product.name} to cart!`)}
|
||||
className="text-gray-400 hover:text-[#0073EC] transition-colors p-1"
|
||||
title="Add to Cart"
|
||||
>
|
||||
<ShoppingCart size={13} className="stroke-[2.5]" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
let saved = localStorage.getItem('wishlist');
|
||||
let list: string[] = saved ? JSON.parse(saved) : [];
|
||||
if (!list.includes(product.id)) {
|
||||
list.push(product.id);
|
||||
localStorage.setItem('wishlist', JSON.stringify(list));
|
||||
}
|
||||
window.dispatchEvent(new Event('wishlistUpdated'));
|
||||
window.dispatchEvent(new Event('openWishlistModal'));
|
||||
}}
|
||||
className="text-gray-400 hover:text-[#0073EC] transition-colors p-1 cursor-pointer"
|
||||
title="Add to Wishlist"
|
||||
>
|
||||
<Heart size={13} />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
window.dispatchEvent(new CustomEvent('openQuickView', { detail: product }));
|
||||
}}
|
||||
className="text-gray-400 hover:text-[#0073EC] transition-colors p-1 cursor-pointer"
|
||||
title="Quick View"
|
||||
>
|
||||
<Eye size={13} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
35
NewFile/src/src/sections/NewArrivalsSection.tsx
Normal file
35
NewFile/src/src/sections/NewArrivalsSection.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import React from 'react';
|
||||
import { products } from '../utils/mockData';
|
||||
import { ProductCard } from '../components/ProductCard';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
|
||||
export const NewArrivalsSection: React.FC = () => {
|
||||
const newArrivals = products.filter((p) => p.isNewArrival);
|
||||
|
||||
return (
|
||||
<section className="py-8 bg-bgLight font-sans">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{/* Section Header */}
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-xl md:text-2xl font-extrabold text-gray-900 tracking-tight font-outfit">
|
||||
New Arrivals Products
|
||||
</h2>
|
||||
<a
|
||||
href="#"
|
||||
className="text-xs font-bold text-gray-900 hover:text-primary transition-colors flex items-center gap-1 group"
|
||||
>
|
||||
View All
|
||||
<ArrowRight size={14} className="group-hover:translate-x-1 transition-transform stroke-[2.5]" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Product Grid */}
|
||||
<div className="bg-white border-t border-l border-gray-200 rounded-none shadow-sm overflow-hidden grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
||||
{newArrivals.map((product) => (
|
||||
<ProductCard key={product.id} product={product} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
59
NewFile/src/src/sections/NewsletterSection.tsx
Normal file
59
NewFile/src/src/sections/NewsletterSection.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Send } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export const NewsletterSection: React.FC = () => {
|
||||
const [email, setEmail] = useState('');
|
||||
|
||||
const handleSubscribe = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (email.trim()) {
|
||||
alert(`Thank you for subscribing with: ${email}`);
|
||||
setEmail('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="py-16 bg-primary font-sans text-white relative overflow-hidden">
|
||||
{/* Decorative background shapes */}
|
||||
<div className="absolute top-0 left-0 w-64 h-64 bg-blue-500 rounded-full mix-blend-multiply filter blur-3xl opacity-30 -translate-x-1/2 -translate-y-1/2 pointer-events-none" />
|
||||
<div className="absolute bottom-0 right-0 w-80 h-80 bg-indigo-500 rounded-full mix-blend-multiply filter blur-3xl opacity-30 translate-x-1/3 translate-y-1/3 pointer-events-none" />
|
||||
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<div className="max-w-2xl mx-auto text-center flex flex-col items-center">
|
||||
<span className="text-[10px] font-extrabold uppercase tracking-widest text-blue-100 bg-blue-800/40 px-3 py-1 rounded-full mb-4">
|
||||
Newsletter Signup
|
||||
</span>
|
||||
<h2 className="text-2xl md:text-3xl font-extrabold tracking-tight mb-2 font-outfit">
|
||||
Subscribe to our Newsletter
|
||||
</h2>
|
||||
<p className="text-xs md:text-sm text-blue-100 font-medium max-w-md mb-8 leading-relaxed">
|
||||
Get the latest tech arrivals, exclusive brand discounts, and weekly hot deal alerts directly in your inbox.
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleSubscribe} className="w-full max-w-md flex flex-col sm:flex-row gap-3">
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
placeholder="Your email address..."
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="flex-grow px-4 py-3.5 bg-white/10 border border-white/20 hover:border-white/40 focus:border-white rounded-lg text-white placeholder-white/60 focus:outline-none text-xs font-semibold backdrop-blur-sm transition-all"
|
||||
/>
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.03 }}
|
||||
whileTap={{ scale: 0.97 }}
|
||||
type="submit"
|
||||
className="bg-white hover:bg-gray-50 text-primary hover:text-primary-dark font-extrabold text-xs px-8 py-3.5 rounded-lg flex items-center justify-center gap-2 shadow-lg transition-colors"
|
||||
>
|
||||
Subscribe
|
||||
<Send size={13} className="stroke-[2.5]" />
|
||||
</motion.button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
65
NewFile/src/src/sections/PromoBannersSection.tsx
Normal file
65
NewFile/src/src/sections/PromoBannersSection.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export const PromoBannersSection: React.FC = () => {
|
||||
return (
|
||||
<section className="py-8 bg-bgLight font-sans">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Banner 1 */}
|
||||
<motion.a
|
||||
href="#"
|
||||
whileHover={{ y: -4 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="rounded-theme bg-[#E5E9E6] p-8 md:p-10 flex items-center justify-between overflow-hidden shadow-sm relative min-h-[220px] cursor-pointer block"
|
||||
>
|
||||
<div className="text-left z-10 max-w-[55%]">
|
||||
<h3 className="text-xl md:text-2xl font-extrabold text-gray-900 leading-tight mb-2 font-outfit">
|
||||
Precision Sound with Amazing Quality
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 font-semibold">
|
||||
USB 3 Rechargeable
|
||||
</p>
|
||||
</div>
|
||||
{/* Image */}
|
||||
<div className="absolute right-4 bottom-0 w-[200px] h-[90%] flex items-end justify-center select-none">
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1598327105666-5b89351aff97?w=350&auto=format&fit=crop&q=60"
|
||||
alt="Precision Sound"
|
||||
className="max-h-full max-w-full object-contain rounded-t-lg drop-shadow-xl"
|
||||
/>
|
||||
</div>
|
||||
</motion.a>
|
||||
|
||||
{/* Banner 2 */}
|
||||
<motion.a
|
||||
href="#"
|
||||
whileHover={{ y: -4 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="rounded-theme bg-[#E5ECEF] p-8 md:p-10 flex items-center justify-between overflow-hidden shadow-sm relative min-h-[220px] cursor-pointer block"
|
||||
>
|
||||
<div className="text-left z-10 max-w-[55%]">
|
||||
<h3 className="text-xl md:text-2xl font-extrabold text-gray-900 leading-tight mb-2 font-outfit">
|
||||
Nonstop Sound with Amazing Quality
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 font-semibold">
|
||||
Portable Power in Style
|
||||
</p>
|
||||
</div>
|
||||
{/* Image */}
|
||||
<div className="absolute right-4 bottom-0 w-[200px] h-[95%] flex items-end justify-center select-none">
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1590658268037-6bf12165a8df?w=350&auto=format&fit=crop&q=60"
|
||||
alt="Nonstop Sound"
|
||||
className="max-h-full max-w-full object-contain rounded-t-lg drop-shadow-xl"
|
||||
/>
|
||||
</div>
|
||||
</motion.a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
53
NewFile/src/src/sections/PromoCardsSection.tsx
Normal file
53
NewFile/src/src/sections/PromoCardsSection.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { promoCards } from '../utils/mockData';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export const PromoCardsSection: React.FC = () => {
|
||||
return (
|
||||
<section className="py-8 bg-bgLight font-sans">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{promoCards.map((card) => (
|
||||
<motion.a
|
||||
key={card.id}
|
||||
href="#"
|
||||
whileHover={{ y: -6 }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 25 }}
|
||||
className={`rounded-theme p-6 md:p-8 flex items-center justify-between ${card.bgColor} relative overflow-hidden shadow-sm hover:shadow-soft-hover transition-shadow duration-300 min-h-[190px] cursor-pointer block`}
|
||||
>
|
||||
{/* Text side */}
|
||||
<div className="flex flex-col items-start text-left z-10 max-w-[60%]">
|
||||
<span className={`text-[9px] font-extrabold uppercase px-2.5 py-0.5 rounded-full mb-3 tracking-wider ${card.badgeBg}`}>
|
||||
{card.badge}
|
||||
</span>
|
||||
<h3 className="text-lg md:text-xl font-extrabold text-gray-900 leading-tight mb-1 font-outfit">
|
||||
{card.title}
|
||||
</h3>
|
||||
<p className="text-[11px] text-gray-500 font-semibold mb-3">
|
||||
{card.subtitle}
|
||||
</p>
|
||||
<div className="text-[13px] font-extrabold text-gray-800">
|
||||
{card.priceText}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Image side */}
|
||||
<div className="w-[110px] h-[110px] relative flex items-center justify-center flex-shrink-0">
|
||||
<motion.img
|
||||
whileHover={{ scale: 1.08 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
src={card.image}
|
||||
alt={card.title}
|
||||
className="max-h-full max-w-full object-contain drop-shadow-xl rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
</motion.a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
111
NewFile/src/src/sections/RecentlyLaunchedSection.tsx
Normal file
111
NewFile/src/src/sections/RecentlyLaunchedSection.tsx
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||
import { Navigation } from 'swiper/modules';
|
||||
import { products } from '../utils/mockData';
|
||||
import { ProductCard } from '../components/ProductCard';
|
||||
import { ArrowRight, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
// Import Swiper styles
|
||||
import 'swiper/css';
|
||||
import 'swiper/css/navigation';
|
||||
|
||||
export const RecentlyLaunchedSection: React.FC = () => {
|
||||
const recentlyLaunched = products.filter((p) => p.isRecentlyLaunched);
|
||||
|
||||
return (
|
||||
<section className="py-8 bg-white font-sans">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
|
||||
{/* Section Header */}
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-xl md:text-2xl font-extrabold text-gray-900 tracking-tight font-outfit">
|
||||
Recently Launched
|
||||
</h2>
|
||||
<div className="flex items-center gap-4">
|
||||
<a
|
||||
href="#"
|
||||
className="text-xs font-bold text-gray-900 hover:text-primary transition-colors flex items-center gap-1 group"
|
||||
>
|
||||
View All
|
||||
<ArrowRight size={14} className="group-hover:translate-x-1 transition-transform stroke-[2.5]" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Layout: Left Banner, Right Carousel */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-stretch">
|
||||
|
||||
{/* Left Large Feature Banner */}
|
||||
<div className="lg:col-span-4 h-full">
|
||||
<motion.a
|
||||
href="#"
|
||||
whileHover={{ y: -4 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="rounded-theme bg-[#EFEFEF] p-8 md:p-10 flex flex-col justify-between overflow-hidden shadow-sm relative h-full min-h-[380px] cursor-pointer block"
|
||||
>
|
||||
<div className="text-left z-10">
|
||||
<h3 className="text-2xl font-extrabold text-gray-900 leading-tight mb-2 font-outfit">
|
||||
iPhone Prodigy
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 font-semibold">
|
||||
Performance in Motion
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Image */}
|
||||
<div className="absolute right-0 bottom-0 w-[80%] h-[60%] flex items-end justify-end select-none">
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1511707171634-5f897ff02aa9?w=450&auto=format&fit=crop&q=60"
|
||||
alt="iPhone Prodigy"
|
||||
className="max-h-full max-w-full object-contain rounded-tl-xl drop-shadow-2xl"
|
||||
/>
|
||||
</div>
|
||||
</motion.a>
|
||||
</div>
|
||||
|
||||
{/* Right Product Carousel */}
|
||||
<div className="lg:col-span-8 relative">
|
||||
{/* Custom Carousel Arrows */}
|
||||
<div className="absolute -top-12 right-2 flex gap-1.5 z-20">
|
||||
<button className="rl-prev w-7 h-7 rounded-full border border-gray-250 text-gray-450 hover:text-primary hover:border-primary flex items-center justify-center transition-colors bg-white">
|
||||
<ChevronLeft size={14} className="stroke-[2.5]" />
|
||||
</button>
|
||||
<button className="rl-next w-7 h-7 rounded-full border border-gray-250 text-gray-450 hover:text-primary hover:border-primary flex items-center justify-center transition-colors bg-white">
|
||||
<ChevronRight size={14} className="stroke-[2.5]" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-gray-150 rounded-none shadow-sm overflow-hidden">
|
||||
<Swiper
|
||||
modules={[Navigation]}
|
||||
navigation={{
|
||||
prevEl: '.rl-prev',
|
||||
nextEl: '.rl-next',
|
||||
}}
|
||||
spaceBetween={0}
|
||||
slidesPerView={1}
|
||||
breakpoints={{
|
||||
480: { slidesPerView: 1 },
|
||||
640: { slidesPerView: 2 },
|
||||
1024: { slidesPerView: 3 },
|
||||
}}
|
||||
className="h-full"
|
||||
>
|
||||
{recentlyLaunched.map((product) => (
|
||||
<SwiperSlide key={product.id} className="h-full border-r border-gray-100 last:border-r-0">
|
||||
<ProductCard product={product} />
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
76
NewFile/src/src/sections/UltimateTechSection.tsx
Normal file
76
NewFile/src/src/sections/UltimateTechSection.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { products } from '../utils/mockData';
|
||||
import { ProductCard } from '../components/ProductCard';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export const UltimateTechSection: React.FC = () => {
|
||||
// Query 4 tech gadget products
|
||||
const techGadgets = products.filter((p) => p.isTechGadget).slice(0, 4);
|
||||
|
||||
return (
|
||||
<section className="py-8 bg-white font-sans">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
|
||||
{/* Section Header */}
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-xl md:text-2xl font-extrabold text-gray-900 tracking-tight font-outfit">
|
||||
Ultimate Tech Gadgets
|
||||
</h2>
|
||||
<a
|
||||
href="#"
|
||||
className="text-xs font-bold text-gray-900 hover:text-primary transition-colors flex items-center gap-1 group"
|
||||
>
|
||||
View All
|
||||
<ArrowRight size={14} className="group-hover:translate-x-1 transition-transform stroke-[2.5]" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Layout: Left Grid, Right Banner */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-stretch">
|
||||
|
||||
{/* Left Product Grid */}
|
||||
<div className="lg:col-span-9">
|
||||
<div className="bg-white border-t border-l border-gray-200 rounded-none shadow-sm overflow-hidden grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4">
|
||||
{techGadgets.map((product) => (
|
||||
<ProductCard key={product.id} product={product} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Large Feature Banner */}
|
||||
<div className="lg:col-span-3 h-full">
|
||||
<motion.a
|
||||
href="#"
|
||||
whileHover={{ y: -4 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="rounded-theme bg-[#FBF2EE] p-8 flex flex-col justify-between overflow-hidden shadow-sm relative h-full min-h-[380px] cursor-pointer block"
|
||||
>
|
||||
<div className="text-left z-10">
|
||||
<h3 className="text-2xl font-extrabold text-gray-900 leading-tight mb-2 font-outfit">
|
||||
Active Laptop
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 font-semibold">
|
||||
Smooth in Performance
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Image */}
|
||||
<div className="absolute right-0 bottom-0 w-[95%] h-[60%] flex items-end justify-end select-none">
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1544244015-0df4b3ffc6b0?w=350&auto=format&fit=crop&q=60"
|
||||
alt="Active Laptop"
|
||||
className="max-h-full max-w-full object-contain rounded-tl-xl drop-shadow-2xl"
|
||||
/>
|
||||
</div>
|
||||
</motion.a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
61
NewFile/src/src/styles/index.css
Normal file
61
NewFile/src/src/styles/index.css
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
body {
|
||||
@apply font-sans bg-bgLight text-textMain;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom Swiper Styles */
|
||||
.swiper-pagination-bullet-active {
|
||||
background: #1976F3 !important;
|
||||
width: 24px !important;
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
|
||||
.swiper-button-next,
|
||||
.swiper-button-prev {
|
||||
color: #1976F3 !important;
|
||||
background: white;
|
||||
width: 40px !important;
|
||||
height: 40px !important;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.swiper-button-next::after,
|
||||
.swiper-button-prev::after {
|
||||
font-size: 16px !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Scrollbar behaviors */
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
/* Direct font overrides */
|
||||
body, p, span, a, input, select, button {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6, .font-outfit {
|
||||
font-family: 'Outfit', sans-serif;
|
||||
}
|
||||
|
||||
/* Force container max-width to 1400px on large displays */
|
||||
@media (min-width: 1400px) {
|
||||
.container {
|
||||
max-w-screen-2xl: 1400px;
|
||||
max-width: 1400px !important;
|
||||
}
|
||||
}
|
||||
61
NewFile/src/src/types/index.ts
Normal file
61
NewFile/src/src/types/index.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
export interface Product {
|
||||
id: string;
|
||||
name: string;
|
||||
price: number;
|
||||
oldPrice?: number;
|
||||
rating: number;
|
||||
reviewsCount?: number;
|
||||
image: string;
|
||||
imageHover?: string;
|
||||
discount?: number;
|
||||
category: string;
|
||||
hasTimer?: boolean;
|
||||
timeRemaining?: {
|
||||
days: number;
|
||||
hours: number;
|
||||
minutes: number;
|
||||
seconds: number;
|
||||
};
|
||||
features?: string[];
|
||||
description?: string;
|
||||
isHot?: boolean;
|
||||
isFeatured?: boolean;
|
||||
isNewArrival?: boolean;
|
||||
isRecentlyLaunched?: boolean;
|
||||
isTechGadget?: boolean;
|
||||
soldCount?: number;
|
||||
totalStock?: number;
|
||||
color?: string;
|
||||
brand?: string;
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
iconName: string;
|
||||
productCount: number;
|
||||
image: string;
|
||||
}
|
||||
|
||||
export interface BlogArticle {
|
||||
id: string;
|
||||
title: string;
|
||||
excerpt: string;
|
||||
date: string;
|
||||
author: string;
|
||||
image: string;
|
||||
readTime: string;
|
||||
}
|
||||
|
||||
export interface PromoCardData {
|
||||
id: string;
|
||||
badge: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
priceText: string;
|
||||
image: string;
|
||||
bgColor: string;
|
||||
textColor: string;
|
||||
badgeBg: string;
|
||||
}
|
||||
384
NewFile/src/src/utils/mockData.ts
Normal file
384
NewFile/src/src/utils/mockData.ts
Normal file
|
|
@ -0,0 +1,384 @@
|
|||
import { Product, Category, BlogArticle, PromoCardData } from '../types';
|
||||
|
||||
export const categories: Category[] = [
|
||||
{
|
||||
id: 'cat-1',
|
||||
name: 'TV & Speaker',
|
||||
slug: 'tv-speaker',
|
||||
iconName: 'Speaker',
|
||||
productCount: 7,
|
||||
image: 'https://images.unsplash.com/photo-1545454675-3531b543be5d?w=200&auto=format&fit=crop&q=60',
|
||||
},
|
||||
{
|
||||
id: 'cat-2',
|
||||
name: 'Party Speakers',
|
||||
slug: 'party-speakers',
|
||||
iconName: 'Volume2',
|
||||
productCount: 3,
|
||||
image: 'https://images.unsplash.com/photo-1508700115892-45ecd05ae2ad?w=200&auto=format&fit=crop&q=60',
|
||||
},
|
||||
{
|
||||
id: 'cat-3',
|
||||
name: 'Cameras',
|
||||
slug: 'cameras',
|
||||
iconName: 'Camera',
|
||||
productCount: 5,
|
||||
image: 'https://images.unsplash.com/photo-1516035069371-29a1b244cc32?w=200&auto=format&fit=crop&q=60',
|
||||
},
|
||||
{
|
||||
id: 'cat-4',
|
||||
name: 'Phones',
|
||||
slug: 'phones',
|
||||
iconName: 'Smartphone',
|
||||
productCount: 6,
|
||||
image: 'https://images.unsplash.com/photo-1511707171634-5f897ff02aa9?w=200&auto=format&fit=crop&q=60',
|
||||
},
|
||||
{
|
||||
id: 'cat-5',
|
||||
name: 'Chargers & Cables',
|
||||
slug: 'chargers-cables',
|
||||
iconName: 'Cable',
|
||||
productCount: 6,
|
||||
image: 'https://images.unsplash.com/photo-1628157582853-a796fa650a6a?w=200&auto=format&fit=crop&q=60',
|
||||
},
|
||||
{
|
||||
id: 'cat-6',
|
||||
name: 'Smart Watches',
|
||||
slug: 'smart-watches',
|
||||
iconName: 'Watch',
|
||||
productCount: 4,
|
||||
image: 'https://images.unsplash.com/photo-1508685096489-7aacd43bd3b1?w=200&auto=format&fit=crop&q=60',
|
||||
},
|
||||
];
|
||||
|
||||
export const products: Product[] = [
|
||||
{
|
||||
id: 'prod-1',
|
||||
name: 'Apple Airpods Pro (2nd Gen) with MagSafe Charging Case',
|
||||
price: 299,
|
||||
oldPrice: 1900,
|
||||
rating: 5.0,
|
||||
reviewsCount: 24,
|
||||
image: 'https://images.unsplash.com/photo-1588449668365-d15e397f6787?w=300&auto=format&fit=crop&q=60',
|
||||
imageHover: 'https://images.unsplash.com/photo-1600294037681-c80b4cb5b434?w=300&auto=format&fit=crop&q=60',
|
||||
discount: 5,
|
||||
category: 'Smart Devices',
|
||||
brand: 'Creative',
|
||||
color: 'white',
|
||||
isNewArrival: true,
|
||||
isFeatured: true,
|
||||
isHot: true,
|
||||
isTechGadget: true,
|
||||
},
|
||||
{
|
||||
id: 'prod-2',
|
||||
name: 'Apple HomePod Assistant and Voice Recognition',
|
||||
price: 397,
|
||||
rating: 4.0,
|
||||
reviewsCount: 15,
|
||||
image: 'https://images.unsplash.com/photo-1543512214-318c7553f230?w=300&auto=format&fit=crop&q=60',
|
||||
imageHover: 'https://images.unsplash.com/photo-1546435770-a3e426bf472b?w=300&auto=format&fit=crop&q=60',
|
||||
category: 'TV & Speaker',
|
||||
brand: 'Design',
|
||||
color: 'grey',
|
||||
isNewArrival: true,
|
||||
isFeatured: true,
|
||||
isTechGadget: true,
|
||||
},
|
||||
{
|
||||
id: 'prod-3',
|
||||
name: 'Apple Watch Cellular 40 mm Starlight Aluminium Case',
|
||||
price: 669,
|
||||
oldPrice: 700,
|
||||
rating: 5.0,
|
||||
reviewsCount: 32,
|
||||
image: 'https://images.unsplash.com/photo-1508685096489-7aacd43bd3b1?w=300&auto=format&fit=crop&q=60',
|
||||
imageHover: 'https://images.unsplash.com/photo-1579586337278-3befd40fd17a?w=300&auto=format&fit=crop&q=60',
|
||||
discount: 5,
|
||||
category: 'Smart Watches',
|
||||
brand: 'Gallery',
|
||||
color: 'pink',
|
||||
isNewArrival: true,
|
||||
isFeatured: true,
|
||||
isHot: true,
|
||||
isRecentlyLaunched: true,
|
||||
isTechGadget: true,
|
||||
},
|
||||
{
|
||||
id: 'prod-4',
|
||||
name: 'Bose Portable Speaker, Upto 12 hrs of playtime',
|
||||
price: 139,
|
||||
oldPrice: 190,
|
||||
rating: 4.8,
|
||||
reviewsCount: 45,
|
||||
image: 'https://images.unsplash.com/photo-1608043152269-423dbba4e7e1?w=300&auto=format&fit=crop&q=60',
|
||||
imageHover: 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=300&auto=format&fit=crop&q=60',
|
||||
discount: 5,
|
||||
category: 'TV & Speaker',
|
||||
brand: 'Gold',
|
||||
color: 'grey',
|
||||
isFeatured: true,
|
||||
},
|
||||
{
|
||||
id: 'prod-5',
|
||||
name: 'Ceptics India to Europe & More Travel Adapter Plug',
|
||||
price: 300,
|
||||
oldPrice: 1500,
|
||||
rating: 5.0,
|
||||
reviewsCount: 12,
|
||||
image: 'https://images.unsplash.com/photo-1628157582853-a796fa650a6a?w=300&auto=format&fit=crop&q=60',
|
||||
imageHover: 'https://images.unsplash.com/photo-1583863788434-e58a36330cf0?w=300&auto=format&fit=crop&q=60',
|
||||
discount: 25,
|
||||
category: 'Chargers & Cables',
|
||||
brand: 'Highlight',
|
||||
color: 'white',
|
||||
isNewArrival: true,
|
||||
isRecentlyLaunched: true,
|
||||
},
|
||||
{
|
||||
id: 'prod-6',
|
||||
name: "D'Wild USB Type C 100W Pd Power Delivery 20V Cable",
|
||||
price: 9,
|
||||
rating: 4.0,
|
||||
reviewsCount: 19,
|
||||
image: 'https://images.unsplash.com/photo-1589492477829-5e65395b66cc?w=300&auto=format&fit=crop&q=60',
|
||||
imageHover: 'https://images.unsplash.com/photo-1611186871348-b1ce696e52c9?w=300&auto=format&fit=crop&q=60',
|
||||
category: 'Chargers & Cables',
|
||||
brand: 'Modern',
|
||||
color: 'orange',
|
||||
isNewArrival: true,
|
||||
isRecentlyLaunched: true,
|
||||
},
|
||||
{
|
||||
id: 'prod-7',
|
||||
name: 'Google Home Smart Voice Activated Speaker',
|
||||
price: 300,
|
||||
rating: 5.0,
|
||||
reviewsCount: 41,
|
||||
image: 'https://images.unsplash.com/photo-1543512214-318c7553f230?w=300&auto=format&fit=crop&q=60',
|
||||
imageHover: 'https://images.unsplash.com/photo-1546435770-a3e426bf472b?w=300&auto=format&fit=crop&q=60',
|
||||
category: 'TV & Speaker',
|
||||
brand: 'Nature',
|
||||
color: 'white',
|
||||
isFeatured: true,
|
||||
},
|
||||
{
|
||||
id: 'prod-8',
|
||||
name: 'HP (23.8 Inch) with IPS Panel Technology',
|
||||
price: 1899,
|
||||
oldPrice: 1989,
|
||||
rating: 5.0,
|
||||
reviewsCount: 72,
|
||||
image: 'https://images.unsplash.com/photo-1527443224154-c4a3942d3acf?w=300&auto=format&fit=crop&q=60',
|
||||
imageHover: 'https://images.unsplash.com/photo-1585776245991-cf89dd7fc73a?w=300&auto=format&fit=crop&q=60',
|
||||
discount: 5,
|
||||
category: 'Laptop & Computers',
|
||||
brand: 'Sparker',
|
||||
color: 'grey',
|
||||
isFeatured: true,
|
||||
},
|
||||
{
|
||||
id: 'prod-9',
|
||||
name: 'HP LaserJet P1108 Mono Single Function Laser Printer',
|
||||
price: 85,
|
||||
oldPrice: 89,
|
||||
rating: 5.0,
|
||||
reviewsCount: 8,
|
||||
image: 'https://images.unsplash.com/photo-1612815154858-60aa4c59eaa6?w=300&auto=format&fit=crop&q=60',
|
||||
imageHover: 'https://images.unsplash.com/photo-1563206767-5b18f218e8de?w=300&auto=format&fit=crop&q=60',
|
||||
discount: 4,
|
||||
category: 'Laptop & Computers',
|
||||
brand: 'Creative',
|
||||
color: 'white',
|
||||
hasTimer: true,
|
||||
timeRemaining: { days: 41, hours: 4, minutes: 50, seconds: 18 },
|
||||
isFeatured: true,
|
||||
},
|
||||
{
|
||||
id: 'prod-10',
|
||||
name: 'J.P.Gold Wireless Stereo Earphones Headphonea',
|
||||
price: 45,
|
||||
oldPrice: 49,
|
||||
rating: 4.8,
|
||||
reviewsCount: 16,
|
||||
image: 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=300&auto=format&fit=crop&q=60',
|
||||
imageHover: 'https://images.unsplash.com/photo-1546868871-7041f2a55e12?w=300&auto=format&fit=crop&q=60',
|
||||
discount: 8,
|
||||
category: 'Smart Devices',
|
||||
brand: 'Design',
|
||||
color: 'white',
|
||||
isNewArrival: true,
|
||||
},
|
||||
{
|
||||
id: 'prod-11',
|
||||
name: 'Jajot J2 - Feature Phone, Single Sim Keypad Phone',
|
||||
price: 1500,
|
||||
oldPrice: 2000,
|
||||
rating: 4.0,
|
||||
reviewsCount: 229,
|
||||
image: 'https://images.unsplash.com/photo-1598327105666-5b89351aff97?w=300&auto=format&fit=crop&q=60',
|
||||
imageHover: 'https://images.unsplash.com/photo-1580910051074-3eb694886505?w=300&auto=format&fit=crop&q=60',
|
||||
discount: 25,
|
||||
category: 'Phones',
|
||||
brand: 'Gallery',
|
||||
color: 'grey',
|
||||
hasTimer: true,
|
||||
timeRemaining: { days: 229, hours: 4, minutes: 50, seconds: 18 },
|
||||
isNewArrival: true,
|
||||
isHot: true,
|
||||
isRecentlyLaunched: true,
|
||||
},
|
||||
{
|
||||
id: 'prod-12',
|
||||
name: 'Laview Home Security Camera HD 1080p',
|
||||
price: 79,
|
||||
oldPrice: 99,
|
||||
rating: 5.0,
|
||||
reviewsCount: 15,
|
||||
image: 'https://images.unsplash.com/photo-1558002038-1055907df827?w=300&auto=format&fit=crop&q=60',
|
||||
imageHover: 'https://images.unsplash.com/photo-1525786210598-d527194d852a?w=300&auto=format&fit=crop&q=60',
|
||||
discount: 11,
|
||||
category: 'Smart Devices',
|
||||
brand: 'Gold',
|
||||
color: 'white',
|
||||
isNewArrival: true,
|
||||
},
|
||||
{
|
||||
id: 'prod-13',
|
||||
name: 'Lenovo IdeaPad Slim 3 Laptop (12th Gen Intel i3)',
|
||||
price: 400,
|
||||
oldPrice: 469,
|
||||
rating: 4.0,
|
||||
reviewsCount: 35,
|
||||
image: 'https://images.unsplash.com/photo-1588872657578-7efd1f1555ed?w=300&auto=format&fit=crop&q=60',
|
||||
imageHover: 'https://images.unsplash.com/photo-1603302576837-37561b2e2302?w=300&auto=format&fit=crop&q=60',
|
||||
discount: 15,
|
||||
category: 'Laptop & Computers',
|
||||
brand: 'Highlight',
|
||||
color: 'grey',
|
||||
isNewArrival: true,
|
||||
},
|
||||
{
|
||||
id: 'prod-14',
|
||||
name: 'Lenovo Tab M9 Tablet 4 GB RAM 64 GB ROM',
|
||||
price: 144,
|
||||
oldPrice: 150,
|
||||
rating: 5.0,
|
||||
reviewsCount: 22,
|
||||
image: 'https://images.unsplash.com/photo-1544244015-0df4b3ffc6b0?w=300&auto=format&fit=crop&q=60',
|
||||
imageHover: 'https://images.unsplash.com/photo-1585776245991-cf89dd7fc73a?w=300&auto=format&fit=crop&q=60',
|
||||
discount: 4,
|
||||
category: 'Smart Devices',
|
||||
brand: 'Modern',
|
||||
color: 'black',
|
||||
isFeatured: true,
|
||||
isTechGadget: true,
|
||||
},
|
||||
{
|
||||
id: 'prod-15',
|
||||
name: 'LG 674 litres Side by Side Refrigerator, Noble Steel',
|
||||
price: 85,
|
||||
oldPrice: 299,
|
||||
rating: 4.0,
|
||||
reviewsCount: 18,
|
||||
image: 'https://images.unsplash.com/photo-1571887463133-d900b77945d8?w=300&auto=format&fit=crop&q=60',
|
||||
imageHover: 'https://images.unsplash.com/photo-1584622650111-993a426fbf0a?w=300&auto=format&fit=crop&q=60',
|
||||
discount: 4,
|
||||
category: 'Smart Devices',
|
||||
brand: 'Nature',
|
||||
color: 'grey',
|
||||
isHot: true,
|
||||
},
|
||||
{
|
||||
id: 'prod-16',
|
||||
name: 'Logitech M350 WHITE Optical Wireless Mouse',
|
||||
price: 299,
|
||||
rating: 5.0,
|
||||
reviewsCount: 94,
|
||||
image: 'https://images.unsplash.com/photo-1615663245857-ac93bb7c39e7?w=300&auto=format&fit=crop&q=60',
|
||||
imageHover: 'https://images.unsplash.com/photo-1625842268584-8f3290447001?w=300&auto=format&fit=crop&q=60',
|
||||
category: 'Smart Devices',
|
||||
brand: 'Sparker',
|
||||
color: 'white',
|
||||
isFeatured: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const promoCards: PromoCardData[] = [
|
||||
{
|
||||
id: 'promo-1',
|
||||
badge: 'Weekend Discount',
|
||||
title: 'iPhone Vision',
|
||||
subtitle: 'Only for this week...',
|
||||
priceText: 'from $ 239.99',
|
||||
image: 'https://images.unsplash.com/photo-1510557880182-3d4d3cba35a5?w=350&auto=format&fit=crop&q=60',
|
||||
bgColor: 'bg-[#F2EDFD]', // soft light lavender
|
||||
textColor: 'text-[#4A154B]',
|
||||
badgeBg: 'bg-[#7B2CBF] text-white',
|
||||
},
|
||||
{
|
||||
id: 'promo-2',
|
||||
badge: 'Amazing Deal',
|
||||
title: 'Vibe Your Sound',
|
||||
subtitle: 'This Week Exclusively...',
|
||||
priceText: 'from $ 399.99',
|
||||
image: 'https://images.unsplash.com/photo-1545454675-3531b543be5d?w=350&auto=format&fit=crop&q=60',
|
||||
bgColor: 'bg-[#E3F2FD]', // soft light blue
|
||||
textColor: 'text-primary-dark',
|
||||
badgeBg: 'bg-primary text-white',
|
||||
},
|
||||
{
|
||||
id: 'promo-3',
|
||||
badge: 'Flat 30% OFF',
|
||||
title: 'True Harmony',
|
||||
subtitle: 'Limited Time Only...',
|
||||
priceText: 'from $ 189.99',
|
||||
image: 'https://images.unsplash.com/photo-1590658268037-6bf12165a8df?w=350&auto=format&fit=crop&q=60',
|
||||
bgColor: 'bg-[#FCE4EC]', // soft light pink
|
||||
textColor: 'text-[#880E4F]',
|
||||
badgeBg: 'bg-[#EC407A] text-white',
|
||||
},
|
||||
];
|
||||
|
||||
export const brands = [
|
||||
{ name: 'Amazon', logo: 'amazon' },
|
||||
{ name: 'AMD', logo: 'amd' },
|
||||
{ name: 'Facebook', logo: 'facebook' },
|
||||
{ name: 'Logitech', logo: 'logitech' },
|
||||
{ name: 'Apper', logo: 'apper' },
|
||||
{ name: 'Hooli', logo: 'hooli' },
|
||||
{ name: 'FedEx', logo: 'fedex' },
|
||||
{ name: 'PayPal', logo: 'paypal' },
|
||||
{ name: 'Netflix', logo: 'netflix' },
|
||||
{ name: 'Spotify', logo: 'spotify' },
|
||||
];
|
||||
|
||||
export const blogs: BlogArticle[] = [
|
||||
{
|
||||
id: 'blog-1',
|
||||
title: 'The Future of Wireless Audio: True Stereo Earbuds',
|
||||
excerpt: 'Explore how audio drivers and high bitrates are redefining our daily listening routines...',
|
||||
date: 'July 10, 2026',
|
||||
author: 'Sarah Connor',
|
||||
image: 'https://images.unsplash.com/photo-1590658268037-6bf12165a8df?w=400&auto=format&fit=crop&q=60',
|
||||
readTime: '5 min read',
|
||||
},
|
||||
{
|
||||
id: 'blog-2',
|
||||
title: 'Top 5 Tech Gadgets Every Student Needs in 2026',
|
||||
excerpt: 'From digital note-taking tablets to portable power delivery adapters, stay ahead of the class...',
|
||||
date: 'July 05, 2026',
|
||||
author: 'James Smith',
|
||||
image: 'https://images.unsplash.com/photo-1527443224154-c4a3942d3acf?w=400&auto=format&fit=crop&q=60',
|
||||
readTime: '8 min read',
|
||||
},
|
||||
{
|
||||
id: 'blog-3',
|
||||
title: 'How Smartwatches Changed Health Monitoring Forever',
|
||||
excerpt: 'Continuous heart-rate checks, oxygen metrics, and sleep charts fit cleanly on your wrist...',
|
||||
date: 'June 28, 2026',
|
||||
author: 'Dr. Evelyn L.',
|
||||
image: 'https://images.unsplash.com/photo-1579586337278-3befd40fd17a?w=400&auto=format&fit=crop&q=60',
|
||||
readTime: '6 min read',
|
||||
},
|
||||
];
|
||||
6
NewFile/src/src/utils/utils.ts
Normal file
6
NewFile/src/src/utils/utils.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
50
NewFile/src/tailwind.config.js
Normal file
50
NewFile/src/tailwind.config.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: {
|
||||
DEFAULT: '1rem',
|
||||
sm: '1.5rem',
|
||||
lg: '2rem',
|
||||
},
|
||||
screens: {
|
||||
sm: '640px',
|
||||
md: '768px',
|
||||
lg: '1024px',
|
||||
xl: '1280px',
|
||||
'2xl': '1400px',
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
DEFAULT: "#1976F3",
|
||||
hover: "#1565C0",
|
||||
light: "#E3F2FD",
|
||||
dark: "#0D47A1",
|
||||
},
|
||||
bgLight: "#F8F9FB",
|
||||
cardLight: "#FFFFFF",
|
||||
textMain: "#1E293B",
|
||||
textMuted: "#64748B",
|
||||
},
|
||||
borderRadius: {
|
||||
'theme': '12px',
|
||||
},
|
||||
boxShadow: {
|
||||
'soft': '0 4px 20px -2px rgba(0, 0, 0, 0.05), 0 2px 10px -1px rgba(0, 0, 0, 0.03)',
|
||||
'soft-hover': '0 10px 30px -5px rgba(0, 0, 0, 0.08), 0 4px 15px -2px rgba(0, 0, 0, 0.04)',
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'sans-serif'],
|
||||
outfit: ['Outfit', 'sans-serif'],
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
55
NewFile/src/tailwind.config.ts
Normal file
55
NewFile/src/tailwind.config.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import type { Config } from "tailwindcss";
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/layout/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/sections/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: {
|
||||
DEFAULT: '1rem',
|
||||
sm: '1.5rem',
|
||||
lg: '2rem',
|
||||
},
|
||||
screens: {
|
||||
sm: '640px',
|
||||
md: '768px',
|
||||
lg: '1024px',
|
||||
xl: '1280px',
|
||||
'2xl': '1400px',
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
DEFAULT: "#1976F3",
|
||||
hover: "#1565C0",
|
||||
light: "#E3F2FD",
|
||||
dark: "#0D47A1",
|
||||
},
|
||||
bgLight: "#F8F9FB",
|
||||
cardLight: "#FFFFFF",
|
||||
textMain: "#1E293B",
|
||||
textMuted: "#64748B",
|
||||
},
|
||||
borderRadius: {
|
||||
'theme': '12px', // changed from 16px to 12px per GEMINI.md rules
|
||||
},
|
||||
boxShadow: {
|
||||
'soft': '0 4px 20px -2px rgba(0, 0, 0, 0.05), 0 2px 10px -1px rgba(0, 0, 0, 0.03)',
|
||||
'soft-hover': '0 10px 30px -5px rgba(0, 0, 0, 0.08), 0 4px 15px -2px rgba(0, 0, 0, 0.04)',
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'sans-serif'],
|
||||
outfit: ['Outfit', 'sans-serif'],
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
export default config;
|
||||
26
NewFile/src/tsconfig.json
Normal file
26
NewFile/src/tsconfig.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules", "vite.config.ts", "src/main.tsx", "src/App.tsx"]
|
||||
}
|
||||
1
NewFile/src/tsconfig.tsbuildinfo
Normal file
1
NewFile/src/tsconfig.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
17
NewFile/src/vite.config.ts
Normal file
17
NewFile/src/vite.config.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
open: true,
|
||||
},
|
||||
});
|
||||
12
app/about/layout.tsx
Normal file
12
app/about/layout.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import type { ReactNode } from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'About Us | iFixKart',
|
||||
description:
|
||||
'iFixKart helps you buy certified refurbished gadgets, book device repairs, and keep electronics in use longer.',
|
||||
};
|
||||
|
||||
export default function AboutLayout({ children }: { children: ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
121
app/about/page.tsx
Normal file
121
app/about/page.tsx
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
'use client';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Mail, MapPin, Phone, Smartphone, Wrench } from 'lucide-react';
|
||||
import { AnnouncementBar } from '@/layout/AnnouncementBar';
|
||||
import { Header } from '@/layout/Header';
|
||||
import { Navbar } from '@/layout/Navbar';
|
||||
import { Footer } from '@/layout/Footer';
|
||||
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
|
||||
import { FloatingButtons } from '@/components/FloatingButtons';
|
||||
import {
|
||||
storefrontService,
|
||||
type FooterInfo,
|
||||
type StorefrontSettings,
|
||||
MOCK_FOOTER_INFO,
|
||||
} from '@/services/api/storefrontService';
|
||||
|
||||
export default function AboutPage() {
|
||||
const [footer, setFooter] = useState<FooterInfo>(MOCK_FOOTER_INFO);
|
||||
const [settings, setSettings] = useState<StorefrontSettings>({ store_name: 'iFixKart' });
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
Promise.all([
|
||||
storefrontService.getFooterInfo(),
|
||||
storefrontService.getSettings(),
|
||||
]).then(([info, store]) => {
|
||||
if (cancelled) return;
|
||||
if (info) setFooter(info);
|
||||
if (store) setSettings(store);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const storeName = settings.store_name || 'iFixKart';
|
||||
const phone = footer.phone || settings.support_phone || '';
|
||||
const email = footer.email || '';
|
||||
const address = footer.address || '';
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8F9FB] text-[#1E293B] flex flex-col font-sans">
|
||||
<AnnouncementBar />
|
||||
<Header />
|
||||
<StickyHeaderSpacer />
|
||||
<Navbar />
|
||||
|
||||
<main className="flex-grow py-10 pb-20">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[860px]">
|
||||
<h1 className="text-[28px] md:text-[32px] font-bold text-[#1E293B]">
|
||||
About Us
|
||||
</h1>
|
||||
<p className="text-[15px] text-gray-600 leading-relaxed mt-3">
|
||||
{storeName} sells tested refurbished phones and accessories, and books device repairs.
|
||||
You can shop online, get a screen or battery replaced, or exchange the phone you
|
||||
already have.
|
||||
</p>
|
||||
<p className="text-[15px] text-gray-600 leading-relaxed mt-3">
|
||||
Repairs are done at our Bengaluru service center or at your doorstep. Refurbished
|
||||
phones are checked before listing and sold with a limited warranty.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-8">
|
||||
<Link
|
||||
href="/shop"
|
||||
className="bg-white border border-gray-200 rounded-xl p-5 hover:border-primary/40 transition-colors"
|
||||
>
|
||||
<Smartphone className="w-5 h-5 text-primary mb-3" />
|
||||
<h2 className="text-[16px] font-bold text-[#1E293B]">Shop</h2>
|
||||
<p className="text-[13px] text-gray-500 mt-1 leading-relaxed">
|
||||
Refurbished phones, spare parts, and accessories.
|
||||
</p>
|
||||
</Link>
|
||||
<Link
|
||||
href="/repair-services"
|
||||
className="bg-white border border-gray-200 rounded-xl p-5 hover:border-primary/40 transition-colors"
|
||||
>
|
||||
<Wrench className="w-5 h-5 text-primary mb-3" />
|
||||
<h2 className="text-[16px] font-bold text-[#1E293B]">Repair</h2>
|
||||
<p className="text-[13px] text-gray-500 mt-1 leading-relaxed">
|
||||
Screens, batteries, charging ports, and other common jobs.
|
||||
</p>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 bg-white border border-gray-200 rounded-xl p-5 md:p-6 space-y-4">
|
||||
{address ? (
|
||||
<div className="flex gap-3">
|
||||
<MapPin className="w-4 h-4 text-gray-400 mt-0.5 shrink-0" />
|
||||
<p className="text-[14px] text-gray-600 leading-relaxed">{address}</p>
|
||||
</div>
|
||||
) : null}
|
||||
{phone ? (
|
||||
<a
|
||||
href={`tel:${phone.replace(/[^\d+]/g, '')}`}
|
||||
className="flex gap-3 text-[14px] text-gray-600 hover:text-primary"
|
||||
>
|
||||
<Phone className="w-4 h-4 text-gray-400 mt-0.5 shrink-0" />
|
||||
{phone}
|
||||
</a>
|
||||
) : null}
|
||||
{email ? (
|
||||
<a
|
||||
href={`mailto:${email}`}
|
||||
className="flex gap-3 text-[14px] text-gray-600 hover:text-primary"
|
||||
>
|
||||
<Mail className="w-4 h-4 text-gray-400 mt-0.5 shrink-0" />
|
||||
{email}
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
<FloatingButtons />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
5
app/account/loading.tsx
Normal file
5
app/account/loading.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { PageSkeleton } from '@/components/ui/Skeleton';
|
||||
|
||||
export default function Loading() {
|
||||
return <PageSkeleton variant="simple" />;
|
||||
}
|
||||
990
app/account/page.tsx
Normal file
990
app/account/page.tsx
Normal file
|
|
@ -0,0 +1,990 @@
|
|||
/**
|
||||
* @page Customer Account & Orders (`app/account/page.tsx`)
|
||||
* @purpose TechShop-styled customer account: profile, addresses, orders, settings.
|
||||
* @dependencies useAuthStore, apiFetch
|
||||
*/
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect, Suspense } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import {
|
||||
User,
|
||||
Package,
|
||||
MapPin,
|
||||
Settings,
|
||||
LogOut,
|
||||
Plus,
|
||||
Edit2,
|
||||
Trash2,
|
||||
Check,
|
||||
ShieldCheck,
|
||||
ShoppingBag,
|
||||
Loader2,
|
||||
KeyRound,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { apiFetch, clearTokens } from '@/services/api/client';
|
||||
import { formatCurrency } from '@/lib/utils';
|
||||
import GoogleAuthModal from '@/components/auth/GoogleAuthModal';
|
||||
import { AnnouncementBar } from '@/layout/AnnouncementBar';
|
||||
import { Header } from '@/layout/Header';
|
||||
import { Navbar } from '@/layout/Navbar';
|
||||
import { Footer } from '@/layout/Footer';
|
||||
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
|
||||
import { PhoneInput, PincodeInput } from '@/components/PhoneInput';
|
||||
import { validatePhone, validatePincode, validateAddress } from '@/lib/validation';
|
||||
import { FloatingButtons } from '@/components/FloatingButtons';
|
||||
|
||||
type TabType = 'dashboard' | 'orders' | 'addresses' | 'settings';
|
||||
|
||||
const VALID_TABS: TabType[] = [
|
||||
'dashboard',
|
||||
'orders',
|
||||
'addresses',
|
||||
'settings',
|
||||
];
|
||||
|
||||
function parseTab(raw: string | null): TabType {
|
||||
if (raw && VALID_TABS.includes(raw as TabType)) return raw as TabType;
|
||||
return 'dashboard';
|
||||
}
|
||||
|
||||
interface Address {
|
||||
address_id?: string;
|
||||
address_type: string;
|
||||
full_name: string;
|
||||
phone: string;
|
||||
street_address: string;
|
||||
city: string;
|
||||
state: string;
|
||||
pincode: string;
|
||||
is_default: boolean;
|
||||
}
|
||||
|
||||
interface Order {
|
||||
order_id: string;
|
||||
order_no: string;
|
||||
final_amount: number;
|
||||
status: string;
|
||||
payment_status: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
const TABS: { id: TabType; label: string; icon: typeof User }[] = [
|
||||
{ id: 'dashboard', label: 'Dashboard', icon: User },
|
||||
{ id: 'orders', label: 'Order History', icon: Package },
|
||||
{ id: 'addresses', label: 'Address Book', icon: MapPin },
|
||||
{ id: 'settings', label: 'Settings', icon: Settings },
|
||||
];
|
||||
|
||||
const inputClass =
|
||||
'w-full px-3 py-2.5 bg-white border border-[#e5e5e5] text-[13px] text-[#222] outline-none focus:border-primary transition-colors';
|
||||
|
||||
const labelClass =
|
||||
'block text-[11px] font-semibold text-[#666] uppercase tracking-wide mb-1.5';
|
||||
|
||||
function AccountDashboardPage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const {
|
||||
isAuthenticated,
|
||||
clearAuth,
|
||||
setAuth,
|
||||
authReady,
|
||||
bootstrapAuth,
|
||||
} = useAuthStore();
|
||||
const [authModalOpen, setAuthModalOpen] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<TabType>(() =>
|
||||
parseTab(searchParams.get('tab'))
|
||||
);
|
||||
|
||||
const [profile, setProfile] = useState<any>(null);
|
||||
const [addresses, setAddresses] = useState<Address[]>([]);
|
||||
const [orders, setOrders] = useState<Order[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const [addressModalOpen, setAddressModalOpen] = useState(false);
|
||||
const [editingAddress, setEditingAddress] = useState<Address | null>(null);
|
||||
const [addressForm, setAddressForm] = useState<Address>({
|
||||
address_type: 'SHIPPING',
|
||||
full_name: '',
|
||||
phone: '',
|
||||
street_address: '',
|
||||
city: '',
|
||||
state: '',
|
||||
pincode: '',
|
||||
is_default: false,
|
||||
});
|
||||
|
||||
const [settingsForm, setSettingsForm] = useState({
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
phone: '',
|
||||
});
|
||||
|
||||
const [actionLoading, setActionLoading] = useState(false);
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
void bootstrapAuth();
|
||||
}, [bootstrapAuth]);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveTab(parseTab(searchParams.get('tab')));
|
||||
}, [searchParams]);
|
||||
|
||||
const goToTab = (tab: TabType) => {
|
||||
setActiveTab(tab);
|
||||
setErrorMsg(null);
|
||||
const qs = tab === 'dashboard' ? '/account' : `/account?tab=${tab}`;
|
||||
router.replace(qs, { scroll: false });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!authReady) return;
|
||||
if (!isAuthenticated) {
|
||||
setAuthModalOpen(true);
|
||||
} else {
|
||||
setAuthModalOpen(false);
|
||||
fetchAccountData();
|
||||
}
|
||||
}, [isAuthenticated, authReady]);
|
||||
|
||||
const fetchAccountData = async () => {
|
||||
setLoading(true);
|
||||
setErrorMsg(null);
|
||||
try {
|
||||
const prof = await apiFetch<any>('/api/v1/customer/profile');
|
||||
setProfile(prof);
|
||||
setSettingsForm({
|
||||
first_name: prof.first_name || '',
|
||||
last_name: prof.last_name || '',
|
||||
phone: prof.phone || '',
|
||||
});
|
||||
|
||||
const addrs = await apiFetch<Address[]>(
|
||||
'/api/v1/customer/profile/addresses'
|
||||
);
|
||||
setAddresses(addrs);
|
||||
|
||||
const ords = await apiFetch<Order[]>('/api/v1/orders');
|
||||
setOrders(ords);
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
setErrorMsg(err.message || 'Failed to retrieve account records.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await apiFetch('/api/v1/customer/auth/logout', {
|
||||
method: 'POST',
|
||||
skipAuth: true,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Logout endpoint fail:', err);
|
||||
} finally {
|
||||
clearTokens();
|
||||
clearAuth();
|
||||
setProfile(null);
|
||||
setAddresses([]);
|
||||
setOrders([]);
|
||||
router.push('/');
|
||||
}
|
||||
};
|
||||
|
||||
const handleProfileUpdate = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setErrorMsg(null);
|
||||
if (settingsForm.phone) {
|
||||
const phoneErr = validatePhone(settingsForm.phone);
|
||||
if (phoneErr) {
|
||||
setErrorMsg(phoneErr);
|
||||
return;
|
||||
}
|
||||
}
|
||||
setActionLoading(true);
|
||||
try {
|
||||
const updated = await apiFetch<any>('/api/v1/customer/profile', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(settingsForm),
|
||||
});
|
||||
setProfile(updated);
|
||||
setAuth(useAuthStore.getState().accessToken || '', updated);
|
||||
alert('Profile updated successfully!');
|
||||
} catch (err: any) {
|
||||
setErrorMsg(err.message || 'Failed to update profile.');
|
||||
} finally {
|
||||
setActionLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddressSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setErrorMsg(null);
|
||||
|
||||
const phoneErr = validatePhone(addressForm.phone, true);
|
||||
if (phoneErr) {
|
||||
setErrorMsg(phoneErr);
|
||||
return;
|
||||
}
|
||||
const pincodeErr = validatePincode(addressForm.pincode, true);
|
||||
if (pincodeErr) {
|
||||
setErrorMsg(pincodeErr);
|
||||
return;
|
||||
}
|
||||
const addressErr = validateAddress(addressForm.street_address, true);
|
||||
if (addressErr) {
|
||||
setErrorMsg(addressErr);
|
||||
return;
|
||||
}
|
||||
|
||||
setActionLoading(true);
|
||||
try {
|
||||
if (editingAddress?.address_id) {
|
||||
await apiFetch<Address>(
|
||||
`/api/v1/customer/profile/addresses/${editingAddress.address_id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(addressForm),
|
||||
}
|
||||
);
|
||||
} else {
|
||||
await apiFetch<Address>('/api/v1/customer/profile/addresses', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(addressForm),
|
||||
});
|
||||
}
|
||||
setAddressModalOpen(false);
|
||||
setEditingAddress(null);
|
||||
const addrs = await apiFetch<Address[]>(
|
||||
'/api/v1/customer/profile/addresses'
|
||||
);
|
||||
setAddresses(addrs);
|
||||
} catch (err: any) {
|
||||
setErrorMsg(err.message || 'Failed to save address record.');
|
||||
} finally {
|
||||
setActionLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditAddressClick = (addr: Address) => {
|
||||
setEditingAddress(addr);
|
||||
setAddressForm({
|
||||
address_type: addr.address_type,
|
||||
full_name: addr.full_name,
|
||||
phone: addr.phone,
|
||||
street_address: addr.street_address,
|
||||
city: addr.city,
|
||||
state: addr.state,
|
||||
pincode: addr.pincode,
|
||||
is_default: addr.is_default,
|
||||
});
|
||||
setAddressModalOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteAddress = async (addressId: string) => {
|
||||
if (!confirm('Are you sure you want to delete this address?')) return;
|
||||
setActionLoading(true);
|
||||
try {
|
||||
await apiFetch(`/api/v1/customer/profile/addresses/${addressId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
const addrs = await apiFetch<Address[]>(
|
||||
'/api/v1/customer/profile/addresses'
|
||||
);
|
||||
setAddresses(addrs);
|
||||
} catch (err: any) {
|
||||
setErrorMsg(err.message || 'Failed to delete address.');
|
||||
} finally {
|
||||
setActionLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelOrder = async (orderId: string) => {
|
||||
if (!confirm('Are you sure you want to cancel this order?')) return;
|
||||
setActionLoading(true);
|
||||
try {
|
||||
await apiFetch(`/api/v1/orders/${orderId}/cancel`, { method: 'POST' });
|
||||
setOrders(
|
||||
orders.map((o) =>
|
||||
o.order_id === orderId ? { ...o, status: 'CANCELLED' } : o
|
||||
)
|
||||
);
|
||||
alert('Order cancelled successfully.');
|
||||
} catch (err: any) {
|
||||
setErrorMsg(err.message || 'Failed to cancel order.');
|
||||
} finally {
|
||||
setActionLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const openNewAddressModal = () => {
|
||||
setEditingAddress(null);
|
||||
setAddressForm({
|
||||
address_type: 'SHIPPING',
|
||||
full_name: '',
|
||||
phone: '',
|
||||
street_address: '',
|
||||
city: '',
|
||||
state: '',
|
||||
pincode: '',
|
||||
is_default: false,
|
||||
});
|
||||
setAddressModalOpen(true);
|
||||
};
|
||||
|
||||
const defaultAddress = addresses.find((a) => a.is_default);
|
||||
const activeTabMeta = TABS.find((t) => t.id === activeTab);
|
||||
|
||||
const pageShell = (children: React.ReactNode) => (
|
||||
<div className="min-h-screen bg-[#F8F9FB] text-[#1E293B] flex flex-col font-sans">
|
||||
<AnnouncementBar />
|
||||
<Header />
|
||||
<StickyHeaderSpacer />
|
||||
<Navbar />
|
||||
{children}
|
||||
<Footer />
|
||||
<FloatingButtons />
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!authReady) {
|
||||
return (
|
||||
<div className="w-full min-h-screen bg-white flex items-center justify-center">
|
||||
<Loader2 className="w-8 h-8 text-primary animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return pageShell(
|
||||
<>
|
||||
<main className="flex-grow pb-16">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1400px] py-10 flex justify-center">
|
||||
<div className="max-w-[420px] w-full bg-white border border-[#e5e5e5] overflow-hidden">
|
||||
<div className="px-6 pt-8 pb-2 text-center space-y-3">
|
||||
<div className="text-[24px] font-extrabold tracking-tight leading-none">
|
||||
<span className="text-primary">iFix</span>
|
||||
<span className="text-[#222]">Kart</span>
|
||||
</div>
|
||||
<div className="w-14 h-14 bg-primary/10 text-primary flex items-center justify-center mx-auto">
|
||||
<KeyRound className="w-7 h-7" />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<h2 className="text-[20px] font-bold text-[#222]">
|
||||
Sign in to your account
|
||||
</h2>
|
||||
<p className="text-[13px] text-gray-500 leading-relaxed px-2">
|
||||
View your profile, manage shipping addresses, and track
|
||||
order history in one place.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-6 py-6 space-y-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAuthModalOpen(true)}
|
||||
className="w-full bg-primary hover:bg-primary-hover text-white font-semibold py-3.5 text-[14px] transition cursor-pointer"
|
||||
>
|
||||
Continue with Google
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<GoogleAuthModal
|
||||
isOpen={authModalOpen}
|
||||
onClose={() => setAuthModalOpen(false)}
|
||||
onSuccess={() => {
|
||||
setAuthModalOpen(false);
|
||||
router.push('/');
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return pageShell(
|
||||
<>
|
||||
<main className="flex-grow pb-16 antialiased">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1400px] py-8">
|
||||
{/* Profile strip */}
|
||||
<div className="bg-white border border-[#e5e5e5] px-5 py-4 mb-6 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-3.5 min-w-0">
|
||||
<div className="w-12 h-12 shrink-0 bg-primary text-white flex items-center justify-center text-[15px] font-bold select-none">
|
||||
{profile?.first_name?.charAt(0).toUpperCase() || 'U'}
|
||||
{profile?.last_name?.charAt(0).toUpperCase() || ''}
|
||||
</div>
|
||||
<div className="min-w-0 text-left">
|
||||
<p className="text-[16px] font-bold text-[#222] truncate">
|
||||
{profile?.first_name} {profile?.last_name}
|
||||
</p>
|
||||
<p className="text-[12px] text-gray-500 truncate">
|
||||
{profile?.email}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLogout}
|
||||
className="inline-flex items-center gap-2 text-[13px] font-medium text-[#666] hover:text-primary transition-colors cursor-pointer self-start sm:self-auto"
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{errorMsg && (
|
||||
<div className="px-4 py-3 mb-6 text-[13px] text-red-600 bg-red-50 border border-red-200">
|
||||
{errorMsg}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className="w-full py-20 flex justify-center items-center bg-white border border-[#e5e5e5]">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-primary" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col lg:flex-row gap-6 lg:gap-8 items-start">
|
||||
{/* Sidebar — matches category rail language */}
|
||||
<aside className="w-full lg:w-[260px] shrink-0 bg-white border border-[#e5e5e5]">
|
||||
<ul className="flex flex-col">
|
||||
{TABS.map((tab, idx) => {
|
||||
const isActive = activeTab === tab.id;
|
||||
return (
|
||||
<li key={tab.id}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
goToTab(tab.id);
|
||||
}}
|
||||
className={`w-full flex items-center gap-3 px-5 py-[13px] text-[13px] font-medium transition-colors text-left cursor-pointer ${
|
||||
idx < TABS.length - 1
|
||||
? 'border-b border-[#ececec]'
|
||||
: ''
|
||||
} ${
|
||||
isActive
|
||||
? 'text-primary bg-[#f7f9fc]'
|
||||
: 'text-[#333] hover:text-primary hover:bg-[#f7f9fc]'
|
||||
}`}
|
||||
>
|
||||
<tab.icon
|
||||
className={`w-4 h-4 shrink-0 ${
|
||||
isActive ? 'text-primary' : 'text-gray-400'
|
||||
}`}
|
||||
/>
|
||||
{tab.label}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
{/* Content panel */}
|
||||
<div className="flex-1 min-w-0 w-full bg-white border border-[#e5e5e5]">
|
||||
<div className="flex items-center justify-between gap-4 px-5 md:px-6 py-4 border-b border-[#e5e5e5]">
|
||||
<h2 className="text-[18px] md:text-[20px] font-bold text-[#222] tracking-tight">
|
||||
{activeTabMeta?.label}
|
||||
</h2>
|
||||
{activeTab === 'addresses' ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={openNewAddressModal}
|
||||
className="inline-flex items-center gap-1.5 bg-primary hover:bg-primary-hover text-white text-[12px] font-semibold px-3.5 py-2 transition-colors cursor-pointer"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
New Address
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="p-5 md:p-6">
|
||||
{/* Dashboard */}
|
||||
{activeTab === 'dashboard' && (
|
||||
<div className="space-y-5">
|
||||
<p className="text-[13px] text-gray-500">
|
||||
Hello, {profile?.first_name}! View recent activity and
|
||||
manage your account here.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="border border-[#e5e5e5] p-5 flex flex-col justify-between min-h-[160px]">
|
||||
<div>
|
||||
<span className="text-[11px] font-semibold text-[#888] uppercase tracking-wide block mb-2.5">
|
||||
Default Shipping Address
|
||||
</span>
|
||||
{defaultAddress ? (
|
||||
<p className="text-[13px] text-[#444] leading-relaxed">
|
||||
<span className="font-bold text-[#222] block mb-1">
|
||||
{defaultAddress.full_name}
|
||||
</span>
|
||||
{defaultAddress.street_address}
|
||||
<br />
|
||||
{defaultAddress.city}, {defaultAddress.state} –{' '}
|
||||
{defaultAddress.pincode}
|
||||
<br />
|
||||
<span className="text-gray-500">
|
||||
Phone: {defaultAddress.phone}
|
||||
</span>
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-[13px] text-gray-500">
|
||||
No default address saved in address book.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => goToTab('addresses')}
|
||||
className="mt-4 text-primary hover:underline text-[13px] font-semibold text-left w-max cursor-pointer"
|
||||
>
|
||||
Manage Addresses
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="border border-[#e5e5e5] p-5 flex flex-col justify-between min-h-[160px]">
|
||||
<div>
|
||||
<span className="text-[11px] font-semibold text-[#888] uppercase tracking-wide block mb-2.5">
|
||||
Recent Order Activity
|
||||
</span>
|
||||
{orders.length > 0 ? (
|
||||
<div className="text-[13px] text-[#444] space-y-1">
|
||||
<p className="font-bold text-[#222]">
|
||||
Order {orders[0].order_no}
|
||||
</p>
|
||||
<p>
|
||||
Total:{' '}
|
||||
{formatCurrency(orders[0].final_amount)}
|
||||
</p>
|
||||
<p>
|
||||
Status:{' '}
|
||||
<span className="text-primary font-semibold">
|
||||
{orders[0].status.replace('_', ' ')}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-[13px] text-gray-500">
|
||||
No purchase order history recorded.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => goToTab('orders')}
|
||||
className="mt-4 text-primary hover:underline text-[13px] font-semibold text-left w-max cursor-pointer"
|
||||
>
|
||||
Track Purchases
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Orders */}
|
||||
{activeTab === 'orders' && (
|
||||
<div className="space-y-4">
|
||||
{orders.length === 0 ? (
|
||||
<div className="py-14 text-center border border-dashed border-[#e5e5e5]">
|
||||
<ShoppingBag className="w-10 h-10 mx-auto text-gray-300 mb-3" />
|
||||
<p className="text-[13px] font-semibold text-[#444]">
|
||||
No orders placed yet.
|
||||
</p>
|
||||
<Link
|
||||
href="/shop"
|
||||
className="inline-block mt-3 text-[13px] font-semibold text-primary hover:underline"
|
||||
>
|
||||
Continue Shopping
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="border border-[#e5e5e5] divide-y divide-[#e5e5e5]">
|
||||
{orders.map((order) => (
|
||||
<div
|
||||
key={order.order_id}
|
||||
className="px-4 py-4 flex flex-col sm:flex-row sm:items-center justify-between gap-3"
|
||||
>
|
||||
<div className="space-y-0.5 text-left">
|
||||
<span className="font-bold text-[#222] block text-[14px]">
|
||||
{order.order_no}
|
||||
</span>
|
||||
<span className="text-gray-400 block text-[11px]">
|
||||
Placed:{' '}
|
||||
{new Date(
|
||||
order.created_at.endsWith('Z') ||
|
||||
order.created_at.includes('+')
|
||||
? order.created_at
|
||||
: order.created_at + 'Z'
|
||||
).toLocaleDateString('en-IN', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3 sm:gap-5">
|
||||
<div className="text-left sm:text-right">
|
||||
<span className="font-bold text-[#222] block text-[14px]">
|
||||
{formatCurrency(order.final_amount)}
|
||||
</span>
|
||||
<span className="text-[10px] text-gray-400 uppercase block font-semibold tracking-wider">
|
||||
{order.payment_status}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
className={`text-[10px] font-bold px-2 py-0.5 uppercase tracking-wide border ${
|
||||
order.status === 'DELIVERED'
|
||||
? 'border-emerald-200 text-emerald-700 bg-emerald-50'
|
||||
: order.status === 'CANCELLED'
|
||||
? 'border-red-200 text-red-700 bg-red-50'
|
||||
: 'border-primary/20 text-primary bg-primary/5'
|
||||
}`}
|
||||
>
|
||||
{order.status.replace('_', ' ')}
|
||||
</span>
|
||||
{(order.status === 'ORDER_CREATED' ||
|
||||
order.status === 'PAYMENT_PENDING') && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
handleCancelOrder(order.order_id)
|
||||
}
|
||||
className="px-3 py-1 border border-red-200 text-red-600 hover:bg-red-50 font-semibold text-[11px] uppercase tracking-wider transition cursor-pointer"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Addresses */}
|
||||
{activeTab === 'addresses' && (
|
||||
<div className="space-y-4">
|
||||
{addresses.length === 0 ? (
|
||||
<div className="py-14 text-center border border-dashed border-[#e5e5e5]">
|
||||
<MapPin className="w-10 h-10 mx-auto text-gray-300 mb-3" />
|
||||
<p className="text-[13px] font-semibold text-[#444]">
|
||||
No addresses saved yet.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={openNewAddressModal}
|
||||
className="inline-block mt-3 text-[13px] font-semibold text-primary hover:underline cursor-pointer"
|
||||
>
|
||||
Add your first address
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{addresses.map((addr) => (
|
||||
<div
|
||||
key={addr.address_id}
|
||||
className={`p-4 border flex flex-col justify-between ${
|
||||
addr.is_default
|
||||
? 'border-primary'
|
||||
: 'border-[#e5e5e5]'
|
||||
}`}
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-[10px] font-bold uppercase bg-[#f5f5f5] px-2 py-0.5 text-[#666]">
|
||||
{addr.address_type}
|
||||
</span>
|
||||
{addr.is_default && (
|
||||
<span className="text-[10px] font-bold text-primary flex items-center gap-1">
|
||||
<Check className="w-3.5 h-3.5" />
|
||||
Default
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<h4 className="font-bold text-[#222] text-[14px] mb-1">
|
||||
{addr.full_name}
|
||||
</h4>
|
||||
<p className="text-[13px] text-[#555] leading-relaxed">
|
||||
{addr.street_address}
|
||||
<br />
|
||||
{addr.city}, {addr.state} – {addr.pincode}
|
||||
</p>
|
||||
<p className="text-[12px] text-gray-500 mt-2">
|
||||
Phone: {addr.phone}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-4 mt-4 pt-3 border-t border-[#ececec]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleEditAddressClick(addr)}
|
||||
className="text-[#666] hover:text-primary text-[12px] font-semibold flex items-center gap-1 cursor-pointer"
|
||||
>
|
||||
<Edit2 className="w-3.5 h-3.5" /> Edit
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
addr.address_id &&
|
||||
handleDeleteAddress(addr.address_id)
|
||||
}
|
||||
className="text-[#666] hover:text-red-600 text-[12px] font-semibold flex items-center gap-1 cursor-pointer"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" /> Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Settings */}
|
||||
{activeTab === 'settings' && (
|
||||
<form
|
||||
onSubmit={handleProfileUpdate}
|
||||
className="space-y-4 max-w-lg text-left"
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className={labelClass}>First Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settingsForm.first_name}
|
||||
onChange={(e) =>
|
||||
setSettingsForm({
|
||||
...settingsForm,
|
||||
first_name: e.target.value,
|
||||
})
|
||||
}
|
||||
required
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelClass}>Last Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settingsForm.last_name}
|
||||
onChange={(e) =>
|
||||
setSettingsForm({
|
||||
...settingsForm,
|
||||
last_name: e.target.value,
|
||||
})
|
||||
}
|
||||
required
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className={labelClass}>Email Address</label>
|
||||
<input
|
||||
type="email"
|
||||
value={profile?.email || ''}
|
||||
disabled
|
||||
className={`${inputClass} bg-[#f5f5f5] text-gray-500 cursor-not-allowed`}
|
||||
/>
|
||||
<span className="text-[11px] text-gray-400 block mt-1.5">
|
||||
Email is verified via Google login and cannot be
|
||||
changed.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<PhoneInput
|
||||
label="Phone Number"
|
||||
value={settingsForm.phone}
|
||||
onChange={(val) => setSettingsForm({ ...settingsForm, phone: val })}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={actionLoading}
|
||||
className="px-6 py-3 bg-primary hover:bg-primary-hover text-white font-semibold text-[13px] transition cursor-pointer disabled:opacity-60"
|
||||
>
|
||||
{actionLoading
|
||||
? 'Saving Changes...'
|
||||
: 'Save Profile Details'}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{addressModalOpen && (
|
||||
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-black/50 p-4 sm:p-6 overflow-y-auto">
|
||||
<div
|
||||
className="relative w-full max-w-md my-auto bg-white border border-[#e5e5e5] shadow-xl flex flex-col max-h-[min(640px,calc(100vh-2rem))]"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="address-modal-title"
|
||||
>
|
||||
<div className="flex items-center justify-between px-5 py-4 border-b border-[#e5e5e5] shrink-0 bg-white">
|
||||
<h3
|
||||
id="address-modal-title"
|
||||
className="text-[16px] font-bold text-[#222]"
|
||||
>
|
||||
{editingAddress ? 'Edit Address' : 'New Address'}
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAddressModalOpen(false)}
|
||||
className="p-1 text-gray-400 hover:text-[#222] transition cursor-pointer"
|
||||
aria-label="Close"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form
|
||||
onSubmit={handleAddressSubmit}
|
||||
className="p-5 space-y-4 text-left overflow-y-auto min-h-0"
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className={labelClass}>Full Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={addressForm.full_name}
|
||||
onChange={(e) =>
|
||||
setAddressForm({
|
||||
...addressForm,
|
||||
full_name: e.target.value,
|
||||
})
|
||||
}
|
||||
required
|
||||
placeholder="John Doe"
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
<PhoneInput
|
||||
label="Phone"
|
||||
required
|
||||
value={addressForm.phone}
|
||||
onChange={(val) => setAddressForm({ ...addressForm, phone: val })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className={labelClass}>Street Address</label>
|
||||
<input
|
||||
type="text"
|
||||
value={addressForm.street_address}
|
||||
onChange={(e) =>
|
||||
setAddressForm({
|
||||
...addressForm,
|
||||
street_address: e.target.value,
|
||||
})
|
||||
}
|
||||
required
|
||||
placeholder="123 Technology Park Drive"
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div>
|
||||
<label className={labelClass}>City</label>
|
||||
<input
|
||||
type="text"
|
||||
value={addressForm.city}
|
||||
onChange={(e) =>
|
||||
setAddressForm({ ...addressForm, city: e.target.value })
|
||||
}
|
||||
required
|
||||
placeholder="Chennai"
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelClass}>State</label>
|
||||
<input
|
||||
type="text"
|
||||
value={addressForm.state}
|
||||
onChange={(e) =>
|
||||
setAddressForm({ ...addressForm, state: e.target.value })
|
||||
}
|
||||
required
|
||||
placeholder="Tamil Nadu"
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
<PincodeInput
|
||||
label="Pincode"
|
||||
required
|
||||
value={addressForm.pincode}
|
||||
onChange={(val) => setAddressForm({ ...addressForm, pincode: val })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="flex items-center gap-2 pt-1 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={addressForm.is_default}
|
||||
onChange={(e) =>
|
||||
setAddressForm({
|
||||
...addressForm,
|
||||
is_default: e.target.checked,
|
||||
})
|
||||
}
|
||||
className="w-4 h-4 accent-[var(--color-primary,#1976F3)] border-[#e5e5e5]"
|
||||
/>
|
||||
<span className="text-[13px] text-[#444] font-medium">
|
||||
Set as default shipping address
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div className="flex gap-3 pt-4 border-t border-[#e5e5e5]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAddressModalOpen(false)}
|
||||
disabled={actionLoading}
|
||||
className="flex-1 py-2.5 border border-[#e5e5e5] text-[13px] font-semibold text-[#333] hover:bg-[#f7f9fc] transition cursor-pointer"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={actionLoading}
|
||||
className="flex-1 py-2.5 bg-primary hover:bg-primary-hover text-white text-[13px] font-semibold transition disabled:opacity-50 cursor-pointer"
|
||||
>
|
||||
{actionLoading ? 'Saving...' : 'Save Address'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AccountPage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="w-full min-h-screen bg-white flex items-center justify-center">
|
||||
<Loader2 className="w-8 h-8 text-primary animate-spin" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<AccountDashboardPage />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
31
app/api/revalidate/route.ts
Normal file
31
app/api/revalidate/route.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { revalidateTag } from 'next/cache';
|
||||
|
||||
// Validate tag pattern matching allowlist: products, categories, layout, or product-<slug>
|
||||
const ALLOWED_TAG_REGEX = /^(products|categories|layout|brands|product-[a-zA-Z0-9_-]+)$/;
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const authHeader = req.headers.get('authorization');
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return NextResponse.json({ message: 'Missing or malformed Authorization header' }, { status: 401 });
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
if (token !== process.env.REVALIDATION_TOKEN) {
|
||||
return NextResponse.json({ message: 'Unauthorized access token' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await req.json();
|
||||
const tag = body?.tag;
|
||||
|
||||
if (!tag || !ALLOWED_TAG_REGEX.test(tag)) {
|
||||
return NextResponse.json({ message: 'Invalid or restricted revalidation tag' }, { status: 400 });
|
||||
}
|
||||
|
||||
revalidateTag(tag);
|
||||
return NextResponse.json({ revalidated: true, tag, timestamp: Date.now() });
|
||||
} catch (e: any) {
|
||||
return NextResponse.json({ message: 'Malformed JSON payload body or internal revalidation error' }, { status: 400 });
|
||||
}
|
||||
}
|
||||
59
app/blog/[slug]/page.tsx
Normal file
59
app/blog/[slug]/page.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
'use client';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { AnnouncementBar } from '@/layout/AnnouncementBar';
|
||||
import { Header } from '@/layout/Header';
|
||||
import { Navbar } from '@/layout/Navbar';
|
||||
import { Footer } from '@/layout/Footer';
|
||||
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
|
||||
import { FloatingButtons } from '@/components/FloatingButtons';
|
||||
import { BlogArticleView } from '@/components/blog/BlogViews';
|
||||
import { DEMO_BLOG_ARTICLES, findBlogArticle } from '@/lib/blogDefaults';
|
||||
|
||||
export default function BlogArticlePage() {
|
||||
const params = useParams<{ slug: string }>();
|
||||
const slug = String(params?.slug || '');
|
||||
const article = useMemo(
|
||||
() => findBlogArticle(DEMO_BLOG_ARTICLES, slug),
|
||||
[slug]
|
||||
);
|
||||
const related = useMemo(() => {
|
||||
if (!article) return [];
|
||||
return DEMO_BLOG_ARTICLES.filter(
|
||||
(a) => a.slug !== article.slug && a.category === article.category
|
||||
).slice(0, 2);
|
||||
}, [article]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8F9FB] text-[#1E293B] flex flex-col font-sans">
|
||||
<AnnouncementBar />
|
||||
<Header />
|
||||
<StickyHeaderSpacer />
|
||||
<Navbar />
|
||||
<main className="flex-grow py-10 pb-20">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1400px]">
|
||||
{article ? (
|
||||
<BlogArticleView article={article} related={related} />
|
||||
) : (
|
||||
<div className="bg-white border border-gray-200 rounded-2xl p-10 text-center max-w-lg mx-auto">
|
||||
<h1 className="text-lg font-bold text-[#1E293B]">Article not found</h1>
|
||||
<p className="text-[13px] text-gray-500 mt-2 mb-6">
|
||||
This story is missing or no longer published.
|
||||
</p>
|
||||
<Link
|
||||
href="/blog"
|
||||
className="inline-flex items-center justify-center h-10 px-5 rounded-lg bg-primary text-white text-[13px] font-semibold"
|
||||
>
|
||||
Back to News & Blog
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
<FloatingButtons />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
12
app/blog/layout.tsx
Normal file
12
app/blog/layout.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import type { ReactNode } from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'News & Blog | iFixKart',
|
||||
description:
|
||||
'Tech news, refurbished buying guides, and phone repair tips from iFixKart.',
|
||||
};
|
||||
|
||||
export default function BlogLayout({ children }: { children: ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
54
app/blog/page.tsx
Normal file
54
app/blog/page.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
'use client';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { AnnouncementBar } from '@/layout/AnnouncementBar';
|
||||
import { Header } from '@/layout/Header';
|
||||
import { Navbar } from '@/layout/Navbar';
|
||||
import { Footer } from '@/layout/Footer';
|
||||
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
|
||||
import { FloatingButtons } from '@/components/FloatingButtons';
|
||||
import { BlogListing } from '@/components/blog/BlogViews';
|
||||
import { DEMO_BLOG_ARTICLES, BlogArticle, BlogCategory } from '@/lib/blogDefaults';
|
||||
import { storefrontService } from '@/services/api/storefrontService';
|
||||
|
||||
export default function BlogIndexPage() {
|
||||
const [articles, setArticles] = useState(DEMO_BLOG_ARTICLES);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
storefrontService.getBlogPosts().then((posts) => {
|
||||
if (!cancelled && posts && posts.length > 0) {
|
||||
const mapped: BlogArticle[] = posts.map((p: any) => ({
|
||||
slug: (p.title || '').toLowerCase().replace(/[^\w\s-]/g, '').replace(/[\s_-]+/g, '-'),
|
||||
title: p.title || '',
|
||||
excerpt: p.excerpt || '',
|
||||
author: typeof p.author === 'string' ? p.author : (p.author?.name || 'iFixKart Team'),
|
||||
date: p.date || '10 Sep 2026',
|
||||
readTime: '5 min read',
|
||||
category: (['News', 'Reviews', 'Guides', 'Repair'].includes(p.category) ? p.category : 'Guides') as BlogCategory,
|
||||
image: p.image_url || 'https://images.unsplash.com/photo-1591799264318-7e6ef8ddb7ea?w=800&q=80',
|
||||
body: [p.content || p.excerpt || ''],
|
||||
}));
|
||||
setArticles(mapped);
|
||||
}
|
||||
}).catch(() => {});
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8F9FB] text-[#1E293B] flex flex-col font-sans">
|
||||
<AnnouncementBar />
|
||||
<Header />
|
||||
<StickyHeaderSpacer />
|
||||
<Navbar />
|
||||
<main className="flex-grow py-10 pb-20">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1400px]">
|
||||
<BlogListing articles={articles} />
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
<FloatingButtons />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
5
app/cart/loading.tsx
Normal file
5
app/cart/loading.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { PageSkeleton } from '@/components/ui/Skeleton';
|
||||
|
||||
export default function Loading() {
|
||||
return <PageSkeleton variant="cart" />;
|
||||
}
|
||||
300
app/cart/page.tsx
Normal file
300
app/cart/page.tsx
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
/**
|
||||
* @page Full Shopping Cart (`app/cart/page.tsx`)
|
||||
* @purpose Customer full cart page with quantity modification, line totals,
|
||||
* free-shipping progress, promo input, and checkout CTA.
|
||||
*/
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useCartStore } from '@/store/cartStore';
|
||||
import { formatCurrency, getImageUrl } from '@/lib/utils';
|
||||
import { requireLoginForCheckout } from '@/lib/requireLoginForCheckout';
|
||||
import { Header } from '@/layout/Header';
|
||||
import { Footer } from '@/layout/Footer';
|
||||
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
|
||||
import BlurHashImage from '@/components/ui/BlurHashImage';
|
||||
import {
|
||||
Trash2,
|
||||
Plus,
|
||||
Minus,
|
||||
ArrowRight,
|
||||
ShoppingBag,
|
||||
Truck,
|
||||
Tag,
|
||||
} from 'lucide-react';
|
||||
|
||||
const FREE_SHIPPING_THRESHOLD = 999;
|
||||
|
||||
function resolveItemImage(item: {
|
||||
product: { images?: { image_url?: string }[]; name?: string };
|
||||
selectedVariant: { images?: { image_url?: string }[] };
|
||||
}): string {
|
||||
const fromVariant = item.selectedVariant?.images?.[0]?.image_url;
|
||||
const fromProduct = item.product?.images?.[0]?.image_url;
|
||||
return getImageUrl(fromVariant || fromProduct || '');
|
||||
}
|
||||
|
||||
export default function CartPage() {
|
||||
const router = useRouter();
|
||||
const cart = useCartStore((state) => state.cart);
|
||||
const removeFromCart = useCartStore((state) => state.removeFromCart);
|
||||
const updateQuantity = useCartStore((state) => state.updateQuantity);
|
||||
const getCartTotal = useCartStore((state) => state.getCartTotal);
|
||||
|
||||
const subtotal = getCartTotal();
|
||||
const shipping = subtotal > 0 && subtotal < FREE_SHIPPING_THRESHOLD ? 15.0 : 0;
|
||||
const total = subtotal + shipping;
|
||||
const progressPercent = Math.min(100, (subtotal / FREE_SHIPPING_THRESHOLD) * 100);
|
||||
const amountNeeded = Math.max(0, FREE_SHIPPING_THRESHOLD - subtotal);
|
||||
|
||||
return (
|
||||
<div className="w-full min-h-screen bg-[#F8F9FB] flex flex-col">
|
||||
<Header />
|
||||
<StickyHeaderSpacer />
|
||||
|
||||
<main className="flex-grow py-8 md:py-10">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{cart.length > 0 && (
|
||||
<div className="flex justify-end mb-6">
|
||||
<Link
|
||||
href="/shop"
|
||||
className="text-[13px] font-semibold text-primary hover:underline"
|
||||
>
|
||||
Continue shopping
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{cart.length === 0 ? (
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-10 md:p-14 text-center max-w-lg mx-auto shadow-sm">
|
||||
<div className="w-16 h-16 rounded-full bg-primary-light text-primary flex items-center justify-center mx-auto mb-4">
|
||||
<ShoppingBag className="w-7 h-7" />
|
||||
</div>
|
||||
<h2 className="text-lg font-bold text-[#1a1a1a]">
|
||||
Your cart is empty
|
||||
</h2>
|
||||
<p className="text-[13px] text-gray-500 mt-1.5 mb-6">
|
||||
Looks like you haven't added any products yet.
|
||||
</p>
|
||||
<Link
|
||||
href="/shop"
|
||||
className="inline-flex items-center gap-1.5 bg-primary hover:brightness-95 text-white text-[13px] font-bold px-6 py-3 rounded-lg transition"
|
||||
>
|
||||
Explore products <ArrowRight className="w-4 h-4" />
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 lg:gap-8 items-start">
|
||||
{/* Items */}
|
||||
<div className="lg:col-span-2 bg-white border border-gray-200 rounded-xl overflow-hidden shadow-sm">
|
||||
<div className="px-4 md:px-5 py-3.5 border-b border-gray-100 bg-gray-50/80 flex items-center justify-between text-[12px] font-bold text-gray-500 uppercase tracking-wide">
|
||||
<span>Product</span>
|
||||
<span className="hidden sm:inline">Subtotal</span>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-gray-100">
|
||||
{cart.map((item) => {
|
||||
const img = resolveItemImage(item);
|
||||
const priceNum = typeof item.selectedVariant.price === 'number'
|
||||
? item.selectedVariant.price
|
||||
: parseFloat(String(item.selectedVariant.price || 0));
|
||||
const lineTotal = priceNum * item.quantity;
|
||||
const atMax =
|
||||
typeof item.selectedVariant.available_stock ===
|
||||
'number' &&
|
||||
item.quantity >= item.selectedVariant.available_stock;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className="p-4 md:p-5 flex items-start sm:items-center gap-3 md:gap-4"
|
||||
>
|
||||
<Link
|
||||
href={`/products/${item.product.slug}`}
|
||||
className="relative w-20 h-20 rounded-lg border border-gray-100 overflow-hidden bg-gray-50 shrink-0"
|
||||
>
|
||||
{img ? (
|
||||
<BlurHashImage
|
||||
src={img}
|
||||
blurHash={(item.product as any).blur_hash || null}
|
||||
alt={item.product.name}
|
||||
fill
|
||||
sizes="80px"
|
||||
className="object-contain p-1"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<ShoppingBag className="w-5 h-5 text-gray-300" />
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<Link
|
||||
href={`/products/${item.product.slug}`}
|
||||
className="text-[13px] font-semibold text-[#1a1a1a] hover:text-primary line-clamp-2 transition-colors"
|
||||
>
|
||||
{item.product.name}
|
||||
</Link>
|
||||
<span className="text-[11px] text-gray-400 block mt-0.5">
|
||||
SKU: {item.selectedVariant.sku}
|
||||
</span>
|
||||
<span className="text-[13px] font-bold text-primary mt-1 block">
|
||||
{formatCurrency(item.selectedVariant.price)}
|
||||
</span>
|
||||
|
||||
<div className="flex items-center gap-3 mt-2.5">
|
||||
<div className="inline-flex items-center border border-gray-200 rounded-lg overflow-hidden bg-white">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
updateQuantity(item.id, item.quantity - 1)
|
||||
}
|
||||
className="p-2 hover:bg-gray-50 text-gray-600 cursor-pointer"
|
||||
aria-label="Decrease quantity"
|
||||
>
|
||||
<Minus className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<span className="px-3 text-[13px] font-bold text-[#1a1a1a] min-w-[2rem] text-center">
|
||||
{item.quantity}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
updateQuantity(item.id, item.quantity + 1)
|
||||
}
|
||||
disabled={atMax}
|
||||
className="p-2 hover:bg-gray-50 text-gray-600 disabled:opacity-30 cursor-pointer"
|
||||
aria-label="Increase quantity"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeFromCart(item.id)}
|
||||
className="text-gray-400 hover:text-red-500 p-1.5 rounded-md hover:bg-red-50 transition cursor-pointer"
|
||||
aria-label="Remove item"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-[13px] font-bold text-[#1a1a1a] shrink-0 pt-1 sm:pt-0">
|
||||
{formatCurrency(lineTotal)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary */}
|
||||
<div className="lg:sticky lg:top-28 space-y-4">
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-5 md:p-6 shadow-sm space-y-4 text-[13px]">
|
||||
<h3 className="text-[14px] font-bold text-[#1a1a1a] border-b border-gray-100 pb-3">
|
||||
Order summary
|
||||
</h3>
|
||||
|
||||
{/* Free shipping progress */}
|
||||
<div className="rounded-lg bg-primary/5 border border-primary/10 p-3 space-y-2">
|
||||
<div className="flex items-start gap-2">
|
||||
<Truck className="w-4 h-4 text-primary shrink-0 mt-0.5" />
|
||||
<p className="text-[12px] text-gray-600 leading-snug">
|
||||
{shipping === 0 ? (
|
||||
<span className="font-semibold text-emerald-600">
|
||||
You've unlocked free shipping!
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
Add{' '}
|
||||
<span className="font-bold text-primary">
|
||||
{formatCurrency(amountNeeded)}
|
||||
</span>{' '}
|
||||
more for free shipping
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-1.5 bg-white rounded-full overflow-hidden border border-primary/10">
|
||||
<div
|
||||
className="h-full bg-primary rounded-full transition-all duration-300"
|
||||
style={{ width: `${progressPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2.5 text-gray-500">
|
||||
<div className="flex justify-between">
|
||||
<span>Subtotal</span>
|
||||
<span className="font-semibold text-[#1a1a1a]">
|
||||
{formatCurrency(subtotal)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span>Shipping</span>
|
||||
<span>
|
||||
{shipping === 0 ? (
|
||||
<strong className="text-emerald-600 text-[11px] font-bold uppercase">
|
||||
Free
|
||||
</strong>
|
||||
) : (
|
||||
<span className="font-semibold text-[#1a1a1a]">
|
||||
{formatCurrency(shipping)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 pt-1 border-t border-gray-100">
|
||||
<div className="relative flex-1">
|
||||
<Tag className="w-3.5 h-3.5 text-gray-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Promo code"
|
||||
className="w-full bg-gray-50 border border-gray-200 rounded-lg pl-9 pr-3 py-2.5 outline-none text-[13px] focus:border-primary focus:ring-2 focus:ring-primary/15"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="bg-primary hover:brightness-95 text-white font-bold px-4 py-2.5 rounded-lg text-[13px] transition cursor-pointer"
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-200 pt-3 flex justify-between items-center">
|
||||
<span className="font-bold text-[#1a1a1a]">Total</span>
|
||||
<span className="text-lg font-bold text-primary">
|
||||
{formatCurrency(total)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!requireLoginForCheckout('/checkout')) return;
|
||||
router.push('/checkout');
|
||||
}}
|
||||
className="w-full bg-primary hover:brightness-95 text-white font-bold py-3.5 rounded-lg text-center transition flex items-center justify-center gap-1.5 text-[14px] cursor-pointer"
|
||||
>
|
||||
Proceed to checkout <ArrowRight className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<p className="text-[11px] text-gray-400 text-center">
|
||||
Taxes calculated at checkout
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
5
app/categories/loading.tsx
Normal file
5
app/categories/loading.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { PageSkeleton } from '@/components/ui/Skeleton';
|
||||
|
||||
export default function Loading() {
|
||||
return <PageSkeleton variant="simple" />;
|
||||
}
|
||||
118
app/categories/page.tsx
Normal file
118
app/categories/page.tsx
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Layers } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { AnnouncementBar } from '@/layout/AnnouncementBar';
|
||||
import { Header } from '@/layout/Header';
|
||||
import { Navbar } from '@/layout/Navbar';
|
||||
import { Footer } from '@/layout/Footer';
|
||||
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
|
||||
import { catalogService, CategoryResponse } from '@/services/api/catalogService';
|
||||
import { getImageUrl } from '@/lib/utils';
|
||||
|
||||
export default function AllCategoriesPage() {
|
||||
const [categories, setCategories] = useState<CategoryResponse[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
catalogService.getCategories()
|
||||
.then((res) => {
|
||||
setLoading(false);
|
||||
if (res && res.length > 0) {
|
||||
const sorted = [...res].sort((a, b) => {
|
||||
const orderA = parseInt(a.sort_order || '0', 10);
|
||||
const orderB = parseInt(b.sort_order || '0', 10);
|
||||
return orderA - orderB;
|
||||
});
|
||||
setCategories(sorted);
|
||||
} else {
|
||||
setCategories([]);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to load categories:", err);
|
||||
setLoading(false);
|
||||
setError(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8F9FB] text-[#1E293B] flex flex-col font-sans">
|
||||
<AnnouncementBar />
|
||||
<Header />
|
||||
<StickyHeaderSpacer />
|
||||
<Navbar />
|
||||
|
||||
<main className="flex-grow py-8 pb-24 text-gray-800 antialiased">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{loading ? (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-6">
|
||||
{[1, 2, 3, 4, 5, 6, 7, 8].map((i) => (
|
||||
<div key={i} className="flex flex-col items-center animate-pulse">
|
||||
<div className="w-full aspect-square bg-gray-200 rounded-theme mb-3" />
|
||||
<div className="h-4 bg-gray-200 rounded w-2/3 mb-1" />
|
||||
<div className="h-3 bg-gray-200 rounded w-1/3" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : error || categories.length === 0 ? (
|
||||
<div className="bg-white border border-gray-250 rounded-2xl p-12 text-center max-w-lg mx-auto shadow-xs">
|
||||
<Layers className="w-16 h-16 text-gray-300 mx-auto mb-4 stroke-1" />
|
||||
<h2 className="text-lg font-bold text-gray-800">No Categories Available</h2>
|
||||
<p className="text-xs text-gray-400 mt-1 mb-6">We could not retrieve catalog categories at this moment.</p>
|
||||
<Link href="/" className="bg-[#1877f2] text-white text-xs font-bold px-6 py-3 rounded-lg hover:bg-[#1565c0] transition-colors inline-block">
|
||||
Go to Homepage
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-6">
|
||||
{categories.map((cat: any) => {
|
||||
const catImage = getImageUrl(cat.image_url || cat.image || cat.icon_url);
|
||||
return (
|
||||
<motion.div
|
||||
key={cat.category_id || cat.slug}
|
||||
whileHover={{ y: -6 }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 25 }}
|
||||
>
|
||||
<Link
|
||||
href={`/products?category=${cat.slug}`}
|
||||
className="flex flex-col items-center group text-center"
|
||||
>
|
||||
{/* Image Box */}
|
||||
<div className="w-full aspect-square bg-white rounded-theme flex items-center justify-center p-6 border border-gray-200 group-hover:border-blue-200 group-hover:bg-blue-50/10 transition-all duration-300 relative overflow-hidden shadow-xs">
|
||||
{catImage ? (
|
||||
<motion.img
|
||||
whileHover={{ scale: 1.06 }}
|
||||
src={catImage}
|
||||
alt={cat.name}
|
||||
className="max-h-full max-w-full object-contain rounded-lg drop-shadow-md"
|
||||
/>
|
||||
) : (
|
||||
<Layers className="w-12 h-12 text-gray-400 group-hover:text-primary transition-colors stroke-[1.5]" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Title & Count */}
|
||||
<h3 className="text-[13px] font-bold text-gray-800 mt-3.5 group-hover:text-primary transition-colors line-clamp-1">
|
||||
{cat.name}
|
||||
</h3>
|
||||
<span className="text-[10px] font-bold text-gray-400 mt-0.5 tracking-wide">
|
||||
{cat.product_count !== undefined ? `${cat.product_count} Products` : 'Catalog Item'}
|
||||
</span>
|
||||
</Link>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
5
app/checkout/loading.tsx
Normal file
5
app/checkout/loading.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { PageSkeleton } from '@/components/ui/Skeleton';
|
||||
|
||||
export default function Loading() {
|
||||
return <PageSkeleton variant="cart" />;
|
||||
}
|
||||
1336
app/checkout/page.tsx
Normal file
1336
app/checkout/page.tsx
Normal file
File diff suppressed because it is too large
Load diff
5
app/compare/loading.tsx
Normal file
5
app/compare/loading.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { PageSkeleton } from '@/components/ui/Skeleton';
|
||||
|
||||
export default function Loading() {
|
||||
return <PageSkeleton variant="simple" />;
|
||||
}
|
||||
244
app/compare/page.tsx
Normal file
244
app/compare/page.tsx
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
/**
|
||||
* @page Compare Page (`app/compare/page.tsx`)
|
||||
* @purpose Product specification comparison matrix comparing up to 4 selected products.
|
||||
*/
|
||||
'use client';
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { useCartStore } from '@/store/cartStore';
|
||||
import { catalogService, ProductResponse } from '@/services/api/catalogService';
|
||||
import { mapProductCardToProductResponse } from '@/utils/productMapper';
|
||||
import { formatCatalogPriceLabel, getImageUrl } from '@/lib/utils';
|
||||
import { ArrowLeftRight, Trash2 } from 'lucide-react';
|
||||
import { AnnouncementBar } from '@/layout/AnnouncementBar';
|
||||
import { Header } from '@/layout/Header';
|
||||
import { Navbar } from '@/layout/Navbar';
|
||||
import { Footer } from '@/layout/Footer';
|
||||
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
|
||||
import { FloatingButtons } from '@/components/FloatingButtons';
|
||||
import { CompareProductPicker } from '@/components/compare/CompareProductPicker';
|
||||
import { COMPARE_LIMIT } from '@/lib/compare';
|
||||
import {
|
||||
buildTypeCompareRows,
|
||||
sameTypeCompareList,
|
||||
type ProductSpecItem,
|
||||
} from '@/lib/compareSpecs';
|
||||
|
||||
export default function ComparePage() {
|
||||
const compareList = useCartStore((state) => state.compareList) || [];
|
||||
const toggleCompare = useCartStore((state) => state.toggleCompare);
|
||||
const addToCart = useCartStore((state) => state.addToCart);
|
||||
const hasHydrated = useCartStore((state) => state._hasHydrated);
|
||||
const [ready, setReady] = useState(false);
|
||||
const [enriched, setEnriched] = useState<Record<string, ProductResponse>>({});
|
||||
const [specMap, setSpecMap] = useState<Record<string, ProductSpecItem[]>>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (hasHydrated) {
|
||||
const list = useCartStore.getState().compareList;
|
||||
const pruned = sameTypeCompareList(list);
|
||||
if (pruned.length !== list.length) {
|
||||
useCartStore.setState({ compareList: pruned });
|
||||
}
|
||||
setReady(true);
|
||||
return;
|
||||
}
|
||||
const t = window.setTimeout(() => setReady(true), 250);
|
||||
return () => window.clearTimeout(t);
|
||||
}, [hasHydrated]);
|
||||
|
||||
const productKey = compareList.map((p) => p.product_id).join('|');
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
if (compareList.length === 0) {
|
||||
setEnriched({});
|
||||
setSpecMap({});
|
||||
return;
|
||||
}
|
||||
|
||||
Promise.all(
|
||||
compareList.map(async (product) => {
|
||||
if (!product.slug) return null;
|
||||
try {
|
||||
const data = await catalogService.getProductDetail(product.slug);
|
||||
const raw = data?.product || data;
|
||||
const full = mapProductCardToProductResponse(raw);
|
||||
const specs = Array.isArray(data?.specifications)
|
||||
? data.specifications
|
||||
.map((item: { label?: string; value?: string }) => ({
|
||||
label: String(item?.label || '').trim(),
|
||||
value: String(item?.value || '').trim(),
|
||||
}))
|
||||
.filter((item: ProductSpecItem) => item.label && item.value)
|
||||
: [];
|
||||
return {
|
||||
id: product.product_id,
|
||||
product: {
|
||||
...product,
|
||||
...full,
|
||||
product_id: product.product_id,
|
||||
slug: product.slug || full.slug,
|
||||
brand_name: full.brand_name || product.brand_name,
|
||||
images: full.images?.length ? full.images : product.images,
|
||||
variants: full.variants?.length ? full.variants : product.variants,
|
||||
} as ProductResponse,
|
||||
specs,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
).then((rows) => {
|
||||
if (cancelled) return;
|
||||
const nextProducts: Record<string, ProductResponse> = {};
|
||||
const nextSpecs: Record<string, ProductSpecItem[]> = {};
|
||||
rows.forEach((row) => {
|
||||
if (!row) return;
|
||||
nextProducts[row.id] = row.product;
|
||||
nextSpecs[row.id] = row.specs;
|
||||
});
|
||||
setEnriched(nextProducts);
|
||||
setSpecMap(nextSpecs);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [productKey]);
|
||||
|
||||
const products = useMemo(
|
||||
() => compareList.map((p) => enriched[p.product_id] || p),
|
||||
[compareList, enriched]
|
||||
);
|
||||
const specRows = useMemo(
|
||||
() => buildTypeCompareRows(products, specMap),
|
||||
[products, specMap]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white text-[#1E293B] flex flex-col font-sans">
|
||||
<AnnouncementBar />
|
||||
<Header />
|
||||
<StickyHeaderSpacer />
|
||||
<Navbar />
|
||||
|
||||
<main className="flex-grow w-full bg-[#f8f9fb] py-8">
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
{ready && compareList.length < COMPARE_LIMIT ? (
|
||||
<div className="flex justify-end mb-4">
|
||||
<CompareProductPicker variant="button" />
|
||||
</div>
|
||||
) : null}
|
||||
{!ready ? (
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-8 animate-pulse h-48" />
|
||||
) : compareList.length === 0 ? (
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-12 text-center max-w-lg mx-auto">
|
||||
<ArrowLeftRight className="w-16 h-16 text-gray-300 mx-auto mb-4 stroke-1" />
|
||||
<h2 className="text-lg font-bold text-gray-900">No Products Selected for Comparison</h2>
|
||||
<p className="text-xs text-gray-400 mt-1 mb-6">
|
||||
Click the compare icon on product cards to compare specifications side-by-side.
|
||||
</p>
|
||||
<Link
|
||||
href="/shop"
|
||||
className="bg-primary text-white text-xs font-bold px-6 py-3 rounded-lg hover:brightness-95 transition-colors inline-block"
|
||||
>
|
||||
Browse Catalog
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white border border-gray-200 rounded-xl overflow-x-auto">
|
||||
<table className="w-full text-left text-xs">
|
||||
<thead>
|
||||
<tr className="bg-gray-50 border-b border-gray-200 text-gray-900">
|
||||
<th className="p-4 w-44 font-extrabold">Feature</th>
|
||||
{products.map((product) => {
|
||||
const img = getImageUrl(product.images?.[0]?.image_url);
|
||||
return (
|
||||
<th key={product.product_id} className="p-4 min-w-[200px]">
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<div className="relative w-20 h-20 rounded-lg border border-gray-200 overflow-hidden bg-gray-50 mb-2">
|
||||
{img ? (
|
||||
<Image src={img} alt={product.name} fill className="object-contain p-1" unoptimized />
|
||||
) : null}
|
||||
</div>
|
||||
<Link
|
||||
href={`/products/${product.slug}`}
|
||||
className="font-extrabold text-gray-900 line-clamp-2 hover:text-primary"
|
||||
>
|
||||
{product.name}
|
||||
</Link>
|
||||
<span className="text-xs font-bold text-gray-900 mt-1">
|
||||
{formatCatalogPriceLabel(product)}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleCompare(product)}
|
||||
className="text-[10px] text-red-500 font-bold hover:underline mt-2 flex items-center gap-1 cursor-pointer"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" /> Remove
|
||||
</button>
|
||||
</div>
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100 text-gray-800">
|
||||
<tr>
|
||||
<td className="p-4 font-bold bg-gray-50">Price</td>
|
||||
{products.map((p) => (
|
||||
<td key={p.product_id} className="p-4 text-center font-extrabold">
|
||||
{formatCatalogPriceLabel(p)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
{specRows.map((row) => (
|
||||
<tr key={row.label}>
|
||||
<td className="p-4 font-bold bg-gray-50">{row.label}</td>
|
||||
{row.values.map((value, index) => (
|
||||
<td key={`${products[index]?.product_id}-${row.label}`} className="p-4 text-center text-gray-700">
|
||||
{value.text}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
<tr>
|
||||
<td className="p-4 font-bold bg-gray-50">Rating</td>
|
||||
{products.map((p) => (
|
||||
<td key={p.product_id} className="p-4 text-center font-bold text-amber-500">
|
||||
★ {p.rating || 4.9} / 5.0
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-4 font-bold bg-gray-50">Action</td>
|
||||
{products.map((p) => (
|
||||
<td key={p.product_id} className="p-4 text-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const variant = p.variants?.[0];
|
||||
if (variant) void addToCart(p, variant, 1);
|
||||
}}
|
||||
className="bg-primary hover:brightness-95 text-white text-[11px] font-bold px-3 py-1.5 rounded-md transition-colors cursor-pointer"
|
||||
>
|
||||
Add to Cart
|
||||
</button>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
<FloatingButtons />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
12
app/contact/layout.tsx
Normal file
12
app/contact/layout.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import type { ReactNode } from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Contact Us | iFixKart',
|
||||
description:
|
||||
'Call, email, or visit iFixKart. Find our support phone, email, and service center address.',
|
||||
};
|
||||
|
||||
export default function ContactLayout({ children }: { children: ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
152
app/contact/page.tsx
Normal file
152
app/contact/page.tsx
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
'use client';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Mail, MapPin, Phone } from 'lucide-react';
|
||||
import { AnnouncementBar } from '@/layout/AnnouncementBar';
|
||||
import { Header } from '@/layout/Header';
|
||||
import { Navbar } from '@/layout/Navbar';
|
||||
import { Footer } from '@/layout/Footer';
|
||||
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
|
||||
import { FloatingButtons } from '@/components/FloatingButtons';
|
||||
import {
|
||||
storefrontService,
|
||||
type FooterInfo,
|
||||
type StorefrontSettings,
|
||||
MOCK_FOOTER_INFO,
|
||||
} from '@/services/api/storefrontService';
|
||||
|
||||
function telHref(phone: string): string {
|
||||
return `tel:${phone.replace(/[^\d+]/g, '')}`;
|
||||
}
|
||||
|
||||
function mapsHref(address: string): string {
|
||||
return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(address)}`;
|
||||
}
|
||||
|
||||
export default function ContactPage() {
|
||||
const [footer, setFooter] = useState<FooterInfo>(MOCK_FOOTER_INFO);
|
||||
const [settings, setSettings] = useState<StorefrontSettings>({ store_name: 'iFixKart' });
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
Promise.all([
|
||||
storefrontService.getFooterInfo(),
|
||||
storefrontService.getSettings(),
|
||||
]).then(([info, store]) => {
|
||||
if (cancelled) return;
|
||||
if (info) setFooter(info);
|
||||
if (store) setSettings(store);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const storeName = settings.store_name || 'iFixKart';
|
||||
const phone = footer.phone || settings.support_phone || '';
|
||||
const email = footer.email || '';
|
||||
const address = footer.address || '';
|
||||
const socials = (footer.social_links || []).filter((s) => s?.url);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8F9FB] text-[#1E293B] flex flex-col font-sans">
|
||||
<AnnouncementBar />
|
||||
<Header />
|
||||
<StickyHeaderSpacer />
|
||||
<Navbar />
|
||||
<main className="flex-grow py-10 pb-20">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[900px]">
|
||||
<h1 className="text-[28px] md:text-[32px] font-bold text-[#1E293B]">
|
||||
Contact Us
|
||||
</h1>
|
||||
<p className="text-[14px] text-gray-500 mt-2 max-w-xl">
|
||||
Reach {storeName} for orders, repairs, and product support. We typically respond within one business day.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-8">
|
||||
{phone ? (
|
||||
<a
|
||||
href={telHref(phone)}
|
||||
className="bg-white border border-gray-200 rounded-xl p-5 hover:border-primary/40 transition-colors"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-lg bg-primary/10 text-primary flex items-center justify-center mb-3">
|
||||
<Phone className="w-5 h-5" />
|
||||
</div>
|
||||
<h2 className="text-[12px] font-bold uppercase tracking-wide text-gray-400">
|
||||
Phone
|
||||
</h2>
|
||||
<p className="text-[16px] font-bold text-[#1E293B] mt-1">{phone}</p>
|
||||
<p className="text-[12px] text-gray-500 mt-1">Call customer support</p>
|
||||
</a>
|
||||
) : null}
|
||||
|
||||
{email ? (
|
||||
<a
|
||||
href={`mailto:${email}`}
|
||||
className="bg-white border border-gray-200 rounded-xl p-5 hover:border-primary/40 transition-colors"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-lg bg-primary/10 text-primary flex items-center justify-center mb-3">
|
||||
<Mail className="w-5 h-5" />
|
||||
</div>
|
||||
<h2 className="text-[12px] font-bold uppercase tracking-wide text-gray-400">
|
||||
Email
|
||||
</h2>
|
||||
<p className="text-[16px] font-bold text-[#1E293B] mt-1 break-all">{email}</p>
|
||||
<p className="text-[12px] text-gray-500 mt-1">Write to our support team</p>
|
||||
</a>
|
||||
) : null}
|
||||
|
||||
{address ? (
|
||||
<a
|
||||
href={mapsHref(address)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="bg-white border border-gray-200 rounded-xl p-5 hover:border-primary/40 transition-colors"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-lg bg-primary/10 text-primary flex items-center justify-center mb-3">
|
||||
<MapPin className="w-5 h-5" />
|
||||
</div>
|
||||
<h2 className="text-[12px] font-bold uppercase tracking-wide text-gray-400">
|
||||
Address
|
||||
</h2>
|
||||
<p className="text-[14px] font-semibold text-[#1E293B] mt-1 leading-relaxed">
|
||||
{address}
|
||||
</p>
|
||||
<p className="text-[12px] text-primary font-semibold mt-2">View on map →</p>
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{socials.length > 0 ? (
|
||||
<div className="mt-8 bg-white border border-gray-200 rounded-xl p-5">
|
||||
<h2 className="text-[13px] font-bold text-[#1E293B]">Follow us</h2>
|
||||
<div className="flex flex-wrap gap-2 mt-3">
|
||||
{socials.map((s, i) => (
|
||||
<a
|
||||
key={`${s.platform}-${i}`}
|
||||
href={s.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center h-9 px-3 rounded-lg border border-gray-200 text-[13px] font-semibold text-gray-700 hover:border-primary hover:text-primary"
|
||||
>
|
||||
{s.platform}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<p className="text-[13px] text-gray-500 mt-8">
|
||||
Looking for quick answers?{' '}
|
||||
<Link href="/faqs" className="text-primary font-semibold hover:underline">
|
||||
Browse FAQs
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
<FloatingButtons />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
36
app/error.tsx
Normal file
36
app/error.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
'use client';
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { AlertTriangle, RefreshCw } from 'lucide-react';
|
||||
|
||||
interface ErrorProps {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
export default function Error({ error, reset }: ErrorProps) {
|
||||
useEffect(() => {
|
||||
console.error('Storefront error boundary caught:', error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<div className="w-full min-h-screen bg-[#F8F9FB] flex flex-col items-center justify-center p-6 font-sans">
|
||||
<div className="max-w-md w-full bg-white border border-gray-250 rounded-2xl p-8 text-center shadow-soft">
|
||||
<div className="w-12 h-12 bg-red-50 text-red-500 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<AlertTriangle size={24} className="stroke-[2.5]" />
|
||||
</div>
|
||||
<h2 className="text-lg font-extrabold text-gray-900 mb-2 font-outfit">Something Went Wrong</h2>
|
||||
<p className="text-xs text-gray-500 font-medium mb-6">
|
||||
{error.message || 'We ran into an unexpected error loading this page. Please try refreshing.'}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => reset()}
|
||||
className="inline-flex items-center justify-center gap-2 bg-[#0073EC] hover:bg-[#1565C0] text-white font-bold text-xs uppercase tracking-wider py-3.5 px-6 rounded-xl transition-all shadow-md active:scale-[0.99] cursor-pointer"
|
||||
>
|
||||
<RefreshCw size={14} className="animate-spin-slow" />
|
||||
<span>Try Again</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
71
app/faqs/[category]/page.tsx
Normal file
71
app/faqs/[category]/page.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
'use client';
|
||||
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { AnnouncementBar } from '@/layout/AnnouncementBar';
|
||||
import { Header } from '@/layout/Header';
|
||||
import { Navbar } from '@/layout/Navbar';
|
||||
import { Footer } from '@/layout/Footer';
|
||||
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
|
||||
import { FloatingButtons } from '@/components/FloatingButtons';
|
||||
import { FaqCategoryPage } from '@/components/faq/FaqPageView';
|
||||
import { storefrontService } from '@/services/api/storefrontService';
|
||||
import { parseFaqPageData } from '@/lib/faqPageLayout';
|
||||
import {
|
||||
DEFAULT_FAQ_PAGE,
|
||||
matchFaqCategory,
|
||||
type FaqPageData,
|
||||
} from '@/lib/faqDefaults';
|
||||
|
||||
export default function FaqCategoryRoutePage() {
|
||||
const params = useParams<{ category: string }>();
|
||||
const slug = String(params?.category || '');
|
||||
const [data, setData] = useState<FaqPageData>(DEFAULT_FAQ_PAGE);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
storefrontService.getFaqLayout().then((raw) => {
|
||||
if (!cancelled) setData(parseFaqPageData(raw));
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const category = useMemo(
|
||||
() => matchFaqCategory(data.categories, slug),
|
||||
[data.categories, slug]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8F9FB] text-[#1E293B] flex flex-col font-sans">
|
||||
<AnnouncementBar />
|
||||
<Header />
|
||||
<StickyHeaderSpacer />
|
||||
<Navbar />
|
||||
<main className="flex-grow py-10 pb-20">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1400px]">
|
||||
{category ? (
|
||||
<FaqCategoryPage data={data} category={category} />
|
||||
) : (
|
||||
<div className="bg-white border border-gray-200 rounded-2xl p-10 text-center max-w-lg mx-auto">
|
||||
<h1 className="text-lg font-bold text-[#1E293B]">FAQ category not found</h1>
|
||||
<p className="text-[13px] text-gray-500 mt-2 mb-6">
|
||||
This category is missing or no longer published.
|
||||
</p>
|
||||
<Link
|
||||
href="/faqs"
|
||||
className="inline-flex items-center justify-center h-10 px-5 rounded-lg bg-primary text-white text-[13px] font-semibold hover:bg-primary-hover"
|
||||
>
|
||||
Browse all FAQs
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
<FloatingButtons />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
16
app/faqs/layout.tsx
Normal file
16
app/faqs/layout.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import type { ReactNode } from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'FAQs | iFixKart',
|
||||
description:
|
||||
'Find answers about refurbished mobiles, repairs, exchange, accessories, and buyback on iFixKart.',
|
||||
};
|
||||
|
||||
export default function FaqsLayout({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return children;
|
||||
}
|
||||
43
app/faqs/page.tsx
Normal file
43
app/faqs/page.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
'use client';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { AnnouncementBar } from '@/layout/AnnouncementBar';
|
||||
import { Header } from '@/layout/Header';
|
||||
import { Navbar } from '@/layout/Navbar';
|
||||
import { Footer } from '@/layout/Footer';
|
||||
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
|
||||
import { FloatingButtons } from '@/components/FloatingButtons';
|
||||
import { FaqHub } from '@/components/faq/FaqPageView';
|
||||
import { storefrontService } from '@/services/api/storefrontService';
|
||||
import { parseFaqPageData } from '@/lib/faqPageLayout';
|
||||
import { DEFAULT_FAQ_PAGE, type FaqPageData } from '@/lib/faqDefaults';
|
||||
|
||||
export default function FaqsPage() {
|
||||
const [data, setData] = useState<FaqPageData>(DEFAULT_FAQ_PAGE);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
storefrontService.getFaqLayout().then((raw) => {
|
||||
if (!cancelled) setData(parseFaqPageData(raw));
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8F9FB] text-[#1E293B] flex flex-col font-sans">
|
||||
<AnnouncementBar />
|
||||
<Header />
|
||||
<StickyHeaderSpacer />
|
||||
<Navbar />
|
||||
<main className="flex-grow py-10 pb-20">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1400px]">
|
||||
<FaqHub data={data} />
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
<FloatingButtons />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
12
app/feedback/layout.tsx
Normal file
12
app/feedback/layout.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import type { ReactNode } from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Feedback | iFixKart',
|
||||
description:
|
||||
'Send iFixKart feedback about orders, repairs, payments, or the website. We read every message.',
|
||||
};
|
||||
|
||||
export default function FeedbackLayout({ children }: { children: ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
280
app/feedback/page.tsx
Normal file
280
app/feedback/page.tsx
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
'use client';
|
||||
|
||||
import React, { FormEvent, useEffect, useMemo, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { CheckCircle2, Lightbulb, MessageSquare, Star, TriangleAlert } from 'lucide-react';
|
||||
import { AnnouncementBar } from '@/layout/AnnouncementBar';
|
||||
import { Header } from '@/layout/Header';
|
||||
import { Navbar } from '@/layout/Navbar';
|
||||
import { Footer } from '@/layout/Footer';
|
||||
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
|
||||
import { FloatingButtons } from '@/components/FloatingButtons';
|
||||
import {
|
||||
storefrontService,
|
||||
type FooterInfo,
|
||||
type StorefrontSettings,
|
||||
MOCK_FOOTER_INFO,
|
||||
} from '@/services/api/storefrontService';
|
||||
import { buildFeedbackPage } from '@/lib/feedbackContent';
|
||||
|
||||
const HIGHLIGHT_ICONS = {
|
||||
idea: Lightbulb,
|
||||
issue: TriangleAlert,
|
||||
praise: MessageSquare,
|
||||
} as const;
|
||||
|
||||
const fieldClass =
|
||||
'w-full px-3 py-2.5 bg-white border border-[#e5e5e5] rounded-lg text-[13px] text-[#222] outline-none focus:border-primary transition-colors';
|
||||
|
||||
const labelClass = 'block text-[12px] font-bold text-[#1E293B] mb-1.5';
|
||||
|
||||
export default function FeedbackPage() {
|
||||
const [footer, setFooter] = useState<FooterInfo>(MOCK_FOOTER_INFO);
|
||||
const [settings, setSettings] = useState<StorefrontSettings>({ store_name: 'iFixKart' });
|
||||
const [name, setName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [topic, setTopic] = useState('suggestion');
|
||||
const [orderNo, setOrderNo] = useState('');
|
||||
const [rating, setRating] = useState(0);
|
||||
const [message, setMessage] = useState('');
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
Promise.all([
|
||||
storefrontService.getFooterInfo(),
|
||||
storefrontService.getSettings(),
|
||||
]).then(([info, store]) => {
|
||||
if (cancelled) return;
|
||||
if (info) setFooter(info);
|
||||
if (store) setSettings(store);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const storeName = settings.store_name || 'iFixKart';
|
||||
const supportEmail = footer.email || 'support@ifixkart.com';
|
||||
const doc = useMemo(() => buildFeedbackPage(storeName), [storeName]);
|
||||
|
||||
const handleSubmit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
const topicLabel = doc.topics.find((t) => t.id === topic)?.label || topic;
|
||||
const body = [
|
||||
`Name: ${name}`,
|
||||
`Email: ${email}`,
|
||||
`Topic: ${topicLabel}`,
|
||||
orderNo ? `Order / job: ${orderNo}` : '',
|
||||
rating ? `Rating: ${rating} / 5` : '',
|
||||
'',
|
||||
message,
|
||||
]
|
||||
.filter((line) => line !== '')
|
||||
.join('\n');
|
||||
|
||||
const href = `mailto:${supportEmail}?subject=${encodeURIComponent(
|
||||
`${storeName} feedback — ${topicLabel}`
|
||||
)}&body=${encodeURIComponent(body)}`;
|
||||
window.location.href = href;
|
||||
setSubmitted(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8F9FB] text-[#1E293B] flex flex-col font-sans">
|
||||
<AnnouncementBar />
|
||||
<Header />
|
||||
<StickyHeaderSpacer />
|
||||
<Navbar />
|
||||
|
||||
<main className="flex-grow py-10 pb-20">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[900px]">
|
||||
<h1 className="text-[28px] md:text-[32px] font-bold text-[#1E293B]">
|
||||
{doc.heading}
|
||||
</h1>
|
||||
<p className="text-[14px] text-gray-500 mt-2 max-w-2xl">{doc.intro}</p>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-8">
|
||||
{doc.highlights.map((item) => {
|
||||
const Icon =
|
||||
HIGHLIGHT_ICONS[item.id as keyof typeof HIGHLIGHT_ICONS] || MessageSquare;
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className="bg-white border border-gray-200 rounded-xl p-5"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-lg bg-primary/10 text-primary flex items-center justify-center mb-3">
|
||||
<Icon className="w-5 h-5" />
|
||||
</div>
|
||||
<h2 className="text-[16px] font-bold text-[#1E293B]">{item.title}</h2>
|
||||
<p className="text-[13px] text-gray-500 mt-1 leading-relaxed">
|
||||
{item.detail}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<article className="mt-8 bg-white border border-gray-200 rounded-xl overflow-hidden">
|
||||
<div className="p-6 md:p-8">
|
||||
{submitted ? (
|
||||
<div className="py-6 text-center">
|
||||
<CheckCircle2 className="w-10 h-10 text-primary mx-auto mb-3" />
|
||||
<h2 className="text-[20px] font-bold text-[#1E293B]">Thanks for the feedback</h2>
|
||||
<p className="text-[14px] text-gray-600 mt-2 max-w-md mx-auto leading-relaxed">
|
||||
Your mail app should open with the message ready to send to {supportEmail}.
|
||||
If it did not, copy your note and email us directly.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSubmitted(false);
|
||||
setMessage('');
|
||||
}}
|
||||
className="mt-6 text-[13px] font-semibold text-primary hover:underline cursor-pointer"
|
||||
>
|
||||
Send another message
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<h2 className="text-[16px] font-bold text-[#1E293B] mb-1">Send a message</h2>
|
||||
<p className="text-[13px] text-gray-500 mb-4">
|
||||
Fields marked with * are required.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label htmlFor="fb-name" className={labelClass}>
|
||||
Name *
|
||||
</label>
|
||||
<input
|
||||
id="fb-name"
|
||||
required
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className={fieldClass}
|
||||
autoComplete="name"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="fb-email" className={labelClass}>
|
||||
Email *
|
||||
</label>
|
||||
<input
|
||||
id="fb-email"
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className={fieldClass}
|
||||
autoComplete="email"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label htmlFor="fb-topic" className={labelClass}>
|
||||
Topic *
|
||||
</label>
|
||||
<select
|
||||
id="fb-topic"
|
||||
required
|
||||
value={topic}
|
||||
onChange={(e) => setTopic(e.target.value)}
|
||||
className={`${fieldClass} cursor-pointer`}
|
||||
>
|
||||
{doc.topics.map((t) => (
|
||||
<option key={t.id} value={t.id}>
|
||||
{t.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="fb-order" className={labelClass}>
|
||||
Order or job ID
|
||||
</label>
|
||||
<input
|
||||
id="fb-order"
|
||||
value={orderNo}
|
||||
onChange={(e) => setOrderNo(e.target.value)}
|
||||
className={fieldClass}
|
||||
placeholder="Optional"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className={labelClass}>How was your experience?</p>
|
||||
<div className="flex items-center gap-1">
|
||||
{[1, 2, 3, 4, 5].map((stars) => (
|
||||
<button
|
||||
key={stars}
|
||||
type="button"
|
||||
aria-label={`${stars} star${stars === 1 ? '' : 's'}`}
|
||||
onClick={() => setRating(rating === stars ? 0 : stars)}
|
||||
className="p-0.5 cursor-pointer"
|
||||
>
|
||||
<Star
|
||||
size={22}
|
||||
className={
|
||||
stars <= rating
|
||||
? 'fill-amber-400 text-amber-400'
|
||||
: 'text-gray-200'
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="fb-message" className={labelClass}>
|
||||
Message *
|
||||
</label>
|
||||
<textarea
|
||||
id="fb-message"
|
||||
required
|
||||
rows={6}
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
className={`${fieldClass} resize-y min-h-[140px]`}
|
||||
placeholder="Tell us what happened or what we should improve."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full sm:w-auto px-6 py-3 bg-primary text-white text-[14px] font-semibold rounded-lg hover:brightness-95 cursor-pointer"
|
||||
>
|
||||
Send feedback
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
<p className="text-[13px] text-gray-500 mt-8 pt-6 border-t border-gray-100">
|
||||
For a return or payment issue, use{' '}
|
||||
<Link href="/returns" className="text-primary font-semibold hover:underline">
|
||||
Returns
|
||||
</Link>{' '}
|
||||
or{' '}
|
||||
<Link href="/payments" className="text-primary font-semibold hover:underline">
|
||||
Payments
|
||||
</Link>
|
||||
. Need answers first?{' '}
|
||||
<Link href="/help" className="text-primary font-semibold hover:underline">
|
||||
Help Center
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
<FloatingButtons />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
199
app/globals.css
Normal file
199
app/globals.css
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Outfit:wght@300;400;500;600;700;800;900&display=swap');
|
||||
@import "tailwindcss";
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
body {
|
||||
background-color: #F8F9FB;
|
||||
color: #1E293B;
|
||||
font-family: 'Inter', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom Swiper Styles */
|
||||
.swiper-pagination-bullet-active {
|
||||
background: #1976F3 !important;
|
||||
width: 24px !important;
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
|
||||
.swiper-button-next,
|
||||
.swiper-button-prev {
|
||||
color: #1976F3 !important;
|
||||
background: white;
|
||||
width: 40px !important;
|
||||
height: 40px !important;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.swiper-button-next::after,
|
||||
.swiper-button-prev::after {
|
||||
font-size: 16px !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* TechShop-style hero carousel (Slider Revolution-like) */
|
||||
.techshop-hero .swiper-wrapper {
|
||||
transition-timing-function: cubic-bezier(0.22, 0.61, 0.36, 1) !important;
|
||||
}
|
||||
|
||||
.techshop-hero-pagination .swiper-pagination-bullet {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
opacity: 1;
|
||||
margin: 0 4px !important;
|
||||
border-radius: 999px;
|
||||
transition: width 0.25s ease, background 0.25s ease;
|
||||
}
|
||||
|
||||
.techshop-hero-pagination .swiper-pagination-bullet-active {
|
||||
width: 22px !important;
|
||||
background: #1976F3 !important;
|
||||
border-radius: 999px !important;
|
||||
}
|
||||
|
||||
/* Layer entrance animations on active slide */
|
||||
.techshop-layer {
|
||||
opacity: 0;
|
||||
transform: translateY(22px);
|
||||
transition:
|
||||
opacity 0.55s cubic-bezier(0.22, 0.61, 0.36, 1),
|
||||
transform 0.55s cubic-bezier(0.22, 0.61, 0.36, 1);
|
||||
}
|
||||
|
||||
.techshop-layer-media {
|
||||
transform: translateX(36px);
|
||||
}
|
||||
|
||||
.techshop-hero-bg {
|
||||
transform: scale(1.06);
|
||||
transition: transform 0.85s cubic-bezier(0.22, 0.61, 0.36, 1);
|
||||
}
|
||||
|
||||
.swiper-slide-active .techshop-layer,
|
||||
.swiper-slide-duplicate-active .techshop-layer {
|
||||
opacity: 1;
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
.swiper-slide-active .techshop-layer-1,
|
||||
.swiper-slide-duplicate-active .techshop-layer-1 {
|
||||
transition-delay: 0.12s;
|
||||
}
|
||||
|
||||
.swiper-slide-active .techshop-layer-2,
|
||||
.swiper-slide-duplicate-active .techshop-layer-2 {
|
||||
transition-delay: 0.22s;
|
||||
}
|
||||
|
||||
.swiper-slide-active .techshop-layer-3,
|
||||
.swiper-slide-duplicate-active .techshop-layer-3 {
|
||||
transition-delay: 0.32s;
|
||||
}
|
||||
|
||||
.swiper-slide-active .techshop-layer-4,
|
||||
.swiper-slide-duplicate-active .techshop-layer-4 {
|
||||
transition-delay: 0.42s;
|
||||
}
|
||||
|
||||
.swiper-slide-active .techshop-layer-media,
|
||||
.swiper-slide-duplicate-active .techshop-layer-media {
|
||||
transition-delay: 0.18s;
|
||||
}
|
||||
|
||||
.swiper-slide-active .techshop-hero-bg,
|
||||
.swiper-slide-duplicate-active .techshop-hero-bg {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.techshop-hero .swiper-wrapper {
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
|
||||
.techshop-layer,
|
||||
.techshop-hero-bg {
|
||||
opacity: 1 !important;
|
||||
transform: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Hide scrollbars but keep scrolling */
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
/* Direct font overrides */
|
||||
body, p, span, a, input, select, button {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6, .font-outfit {
|
||||
font-family: 'Outfit', sans-serif;
|
||||
}
|
||||
|
||||
/* Force container max-width to 1400px on large displays */
|
||||
@media (min-width: 1400px) {
|
||||
.container {
|
||||
max-width: 1400px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom Utilities for Purchased Template Colors & Shadows */
|
||||
.bg-bgLight { background-color: #F8F9FB; }
|
||||
.bg-cardLight { background-color: #FFFFFF; }
|
||||
.text-textMain { color: #1E293B; }
|
||||
.text-textMuted { color: #64748B; }
|
||||
.bg-primary { background-color: #1976F3; }
|
||||
.bg-primary-hover:hover { background-color: #1565C0; }
|
||||
.bg-primary-light { background-color: #E3F2FD; }
|
||||
.text-primary { color: #1976F3; }
|
||||
.border-primary { border-color: #1976F3; }
|
||||
.shadow-soft { box-shadow: 0 4px 20px -2px rgba(0, 0, 0, 0.05), 0 2px 10px -1px rgba(0, 0, 0, 0.03); }
|
||||
.shadow-soft-hover:hover { box-shadow: 0 10px 30px -5px rgba(0, 0, 0, 0.08), 0 4px 15px -2px rgba(0, 0, 0, 0.04); }
|
||||
|
||||
/* Rich HTML product descriptions (bulk import + admin editor) */
|
||||
.product-description-html ul,
|
||||
.product-description-html ul li {
|
||||
list-style-type: disc !important;
|
||||
}
|
||||
.product-description-html ol,
|
||||
.product-description-html ol li {
|
||||
list-style-type: decimal !important;
|
||||
}
|
||||
.product-description-html ul,
|
||||
.product-description-html ol {
|
||||
padding-left: 1.75rem !important;
|
||||
margin-top: 0.5rem !important;
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
.product-description-html li {
|
||||
display: list-item !important;
|
||||
margin-bottom: 0.25rem !important;
|
||||
}
|
||||
.product-description-html [style*="text-align: center"],
|
||||
.product-description-html p[style*="text-align: center"] {
|
||||
text-align: center !important;
|
||||
}
|
||||
.product-description-html [style*="text-align: right"],
|
||||
.product-description-html p[style*="text-align: right"] {
|
||||
text-align: right !important;
|
||||
}
|
||||
.product-description-html [style*="text-align: justify"],
|
||||
.product-description-html p[style*="text-align: justify"] {
|
||||
text-align: justify !important;
|
||||
}
|
||||
.product-description-html strong,
|
||||
.product-description-html b {
|
||||
font-weight: 700;
|
||||
}
|
||||
12
app/help/layout.tsx
Normal file
12
app/help/layout.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import type { ReactNode } from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Help Center | iFixKart',
|
||||
description:
|
||||
'Get help with iFixKart orders, repairs, payments, returns, and FAQs. Contact support or browse help topics.',
|
||||
};
|
||||
|
||||
export default function HelpLayout({ children }: { children: ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
197
app/help/page.tsx
Normal file
197
app/help/page.tsx
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
'use client';
|
||||
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
CircleHelp,
|
||||
CreditCard,
|
||||
LifeBuoy,
|
||||
Mail,
|
||||
Phone,
|
||||
RotateCcw,
|
||||
ShoppingBag,
|
||||
Truck,
|
||||
Wrench,
|
||||
} from 'lucide-react';
|
||||
import { AnnouncementBar } from '@/layout/AnnouncementBar';
|
||||
import { Header } from '@/layout/Header';
|
||||
import { Navbar } from '@/layout/Navbar';
|
||||
import { Footer } from '@/layout/Footer';
|
||||
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
|
||||
import { FloatingButtons } from '@/components/FloatingButtons';
|
||||
import {
|
||||
storefrontService,
|
||||
type FooterInfo,
|
||||
type StorefrontSettings,
|
||||
MOCK_FOOTER_INFO,
|
||||
} from '@/services/api/storefrontService';
|
||||
import { buildHelpDoc } from '@/lib/helpCenterContent';
|
||||
|
||||
const TOPIC_ICONS = {
|
||||
faqs: CircleHelp,
|
||||
orders: Truck,
|
||||
payments: CreditCard,
|
||||
returns: RotateCcw,
|
||||
repairs: Wrench,
|
||||
shop: ShoppingBag,
|
||||
} as const;
|
||||
|
||||
function telHref(phone: string): string {
|
||||
return `tel:${phone.replace(/[^\d+]/g, '')}`;
|
||||
}
|
||||
|
||||
export default function HelpCenterPage() {
|
||||
const [footer, setFooter] = useState<FooterInfo>(MOCK_FOOTER_INFO);
|
||||
const [settings, setSettings] = useState<StorefrontSettings>({ store_name: 'iFixKart' });
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
Promise.all([
|
||||
storefrontService.getFooterInfo(),
|
||||
storefrontService.getSettings(),
|
||||
]).then(([info, store]) => {
|
||||
if (cancelled) return;
|
||||
if (info) setFooter(info);
|
||||
if (store) setSettings(store);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const storeName = settings.store_name || 'iFixKart';
|
||||
const phone = footer.phone || settings.support_phone || '';
|
||||
const email = footer.email || '';
|
||||
const doc = useMemo(() => buildHelpDoc(storeName), [storeName]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8F9FB] text-[#1E293B] flex flex-col font-sans">
|
||||
<AnnouncementBar />
|
||||
<Header />
|
||||
<StickyHeaderSpacer />
|
||||
<Navbar />
|
||||
|
||||
<main className="flex-grow py-10 pb-20">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[900px]">
|
||||
<h1 className="text-[28px] md:text-[32px] font-bold text-[#1E293B]">
|
||||
{doc.heading}
|
||||
</h1>
|
||||
<p className="text-[14px] text-gray-500 mt-2 max-w-2xl">{doc.intro}</p>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-8">
|
||||
{doc.topics.map((item) => {
|
||||
const Icon = TOPIC_ICONS[item.id as keyof typeof TOPIC_ICONS] || CircleHelp;
|
||||
return (
|
||||
<Link
|
||||
key={item.id}
|
||||
href={item.href}
|
||||
className="bg-white border border-gray-200 rounded-xl p-5 hover:border-primary/40 transition-colors"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-lg bg-primary/10 text-primary flex items-center justify-center mb-3">
|
||||
<Icon className="w-5 h-5" />
|
||||
</div>
|
||||
<h2 className="text-[16px] font-bold text-[#1E293B]">{item.title}</h2>
|
||||
<p className="text-[13px] text-gray-500 mt-1 leading-relaxed">
|
||||
{item.detail}
|
||||
</p>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 bg-white border border-gray-200 rounded-xl p-5 md:p-6">
|
||||
<h2 className="text-[13px] font-bold text-[#1E293B]">Browse FAQs</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 mt-3">
|
||||
{doc.faqLinks.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="rounded-lg border border-gray-200 px-4 py-3 hover:border-primary/40 transition-colors"
|
||||
>
|
||||
<p className="text-[14px] font-bold text-[#1E293B]">{item.title}</p>
|
||||
<p className="text-[12px] text-gray-500 mt-0.5 leading-relaxed">
|
||||
{item.detail}
|
||||
</p>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(phone || email) && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4">
|
||||
{phone ? (
|
||||
<a
|
||||
href={telHref(phone)}
|
||||
className="bg-white border border-gray-200 rounded-xl p-5 hover:border-primary/40 transition-colors"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-lg bg-primary/10 text-primary flex items-center justify-center mb-3">
|
||||
<Phone className="w-5 h-5" />
|
||||
</div>
|
||||
<h2 className="text-[12px] font-bold uppercase tracking-wide text-gray-400">
|
||||
Phone
|
||||
</h2>
|
||||
<p className="text-[16px] font-bold text-[#1E293B] mt-1">{phone}</p>
|
||||
<p className="text-[12px] text-gray-500 mt-1">Call customer support</p>
|
||||
</a>
|
||||
) : null}
|
||||
{email ? (
|
||||
<a
|
||||
href={`mailto:${email}`}
|
||||
className="bg-white border border-gray-200 rounded-xl p-5 hover:border-primary/40 transition-colors"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-lg bg-primary/10 text-primary flex items-center justify-center mb-3">
|
||||
<Mail className="w-5 h-5" />
|
||||
</div>
|
||||
<h2 className="text-[12px] font-bold uppercase tracking-wide text-gray-400">
|
||||
Email
|
||||
</h2>
|
||||
<p className="text-[16px] font-bold text-[#1E293B] mt-1 break-all">{email}</p>
|
||||
<p className="text-[12px] text-gray-500 mt-1">Write to our support team</p>
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<article className="mt-8 bg-white border border-gray-200 rounded-xl overflow-hidden">
|
||||
<div className="p-6 md:p-8">
|
||||
<div className="flex items-center gap-2 text-primary mb-6">
|
||||
<LifeBuoy className="w-5 h-5" />
|
||||
<p className="text-[13px] font-semibold">
|
||||
Support is available Monday to Saturday, 10:00 AM – 6:30 PM
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-7">
|
||||
{doc.sections.map((section) => (
|
||||
<section key={section.id} id={section.id}>
|
||||
<h3 className="text-[16px] font-bold text-[#1E293B] mb-2">
|
||||
{section.title}
|
||||
</h3>
|
||||
{section.paragraphs.map((p, i) => (
|
||||
<p
|
||||
key={i}
|
||||
className="text-[14px] text-gray-600 leading-relaxed mb-3 last:mb-0"
|
||||
>
|
||||
{p}
|
||||
</p>
|
||||
))}
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="text-[13px] text-gray-500 mt-8 pt-6 border-t border-gray-100">
|
||||
Prefer a full contact page?{' '}
|
||||
<Link href="/contact" className="text-primary font-semibold hover:underline">
|
||||
Contact Us
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
<FloatingButtons />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
BIN
app/icon.png
Normal file
BIN
app/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 176 KiB |
37
app/layout.tsx
Normal file
37
app/layout.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import type { Metadata } from 'next';
|
||||
import './globals.css';
|
||||
import '../styles/mobile-overrides.css';
|
||||
import { AuthBootstrap } from '@/components/auth/AuthBootstrap';
|
||||
import { StorefrontChrome } from '@/components/store/StorefrontChrome';
|
||||
import { ClientModalsContainer } from '@/components/ClientModalsContainer';
|
||||
import { Toaster } from 'sonner';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'iFixKart TechShop | Electronics & Certified Repair Hub',
|
||||
description:
|
||||
'Shop top smartphones, laptops, smartwatches, OEM replacement spare parts, and book certified device repair services.',
|
||||
openGraph: {
|
||||
title: 'iFixKart TechShop | Electronics & Certified Repair Hub',
|
||||
description:
|
||||
'Shop top smartphones, laptops, smartwatches, OEM replacement spare parts, and book certified device repair services.',
|
||||
type: 'website',
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" className="h-full">
|
||||
<body className="flex flex-col min-h-screen bg-[#F8F9FB] text-[#1E293B] font-sans antialiased">
|
||||
<Toaster position="top-right" richColors closeButton expand={true} />
|
||||
<AuthBootstrap />
|
||||
<StorefrontChrome />
|
||||
<ClientModalsContainer />
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
5
app/loading.tsx
Normal file
5
app/loading.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { HomePageSkeleton } from '@/components/ui/Skeleton';
|
||||
|
||||
export default function Loading() {
|
||||
return <HomePageSkeleton />;
|
||||
}
|
||||
194
app/page.tsx
Normal file
194
app/page.tsx
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
import React from 'react';
|
||||
import { AnnouncementBar } from '../layout/AnnouncementBar';
|
||||
import { Header } from '../layout/Header';
|
||||
import { Navbar } from '../layout/Navbar';
|
||||
import { Footer } from '../layout/Footer';
|
||||
import { FloatingButtons } from '../components/FloatingButtons';
|
||||
import { StickyHeaderSpacer } from '../components/StickyHeaderSpacer';
|
||||
|
||||
import { HomeHeroRow } from '../sections/HomeHeroRow';
|
||||
import { PromoCardsSection } from '../sections/PromoCardsSection';
|
||||
import { CategoriesSection } from '../sections/CategoriesSection';
|
||||
import { NewArrivalsSection } from '../sections/NewArrivalsSection';
|
||||
import { PromoBannersSection } from '../sections/PromoBannersSection';
|
||||
import { HotDealsSection } from '../sections/HotDealsSection';
|
||||
import { RecentlyLaunchedSection } from '../sections/RecentlyLaunchedSection';
|
||||
import { BrandsSection } from '../sections/BrandsSection';
|
||||
import { UltimateTechSection } from '../sections/UltimateTechSection';
|
||||
import { ArticlesSection } from '../sections/ArticlesSection';
|
||||
import { FeaturesSection } from '../sections/FeaturesSection';
|
||||
|
||||
import { catalogServerService } from '../services/api/catalogServerService';
|
||||
import { mapCatalogProductToProduct } from '../utils/productMapper';
|
||||
import { Product } from '../types';
|
||||
import {
|
||||
buildHomepageSections,
|
||||
isSectionVisible,
|
||||
normalizeLayoutPayload,
|
||||
} from '../lib/layoutConfig';
|
||||
import { sortHomeSectionsByOrder } from '../lib/techshopHome';
|
||||
|
||||
/**
|
||||
* Always fetch fresh layout so admin Visible/Hidden + display_order apply.
|
||||
* Homepage stack matches TechShop demo:
|
||||
* https://demo.templatemonster.com/demo/501755.html
|
||||
*/
|
||||
export const dynamic = 'force-dynamic';
|
||||
export const revalidate = 0;
|
||||
|
||||
export default async function Home() {
|
||||
let layoutData: any[] = [];
|
||||
let dbProducts: any[] = [];
|
||||
let categoriesData: any[] = [];
|
||||
let brandsData: any[] = [];
|
||||
let productsError = false;
|
||||
|
||||
const [productsResult, layoutResult, categoriesResult, brandsResult] =
|
||||
await Promise.allSettled([
|
||||
catalogServerService.getProducts(),
|
||||
catalogServerService.fetchLayoutConfigOnServer(),
|
||||
catalogServerService.getCategories(),
|
||||
catalogServerService.getBrands(),
|
||||
]);
|
||||
|
||||
if (productsResult.status === 'fulfilled') {
|
||||
dbProducts = productsResult.value || [];
|
||||
} else {
|
||||
console.error('Homepage products fetch failed:', productsResult.reason);
|
||||
productsError = true;
|
||||
}
|
||||
|
||||
if (layoutResult.status === 'fulfilled') {
|
||||
layoutData = normalizeLayoutPayload(layoutResult.value);
|
||||
} else {
|
||||
console.error('Homepage layout fetch failed:', layoutResult.reason);
|
||||
}
|
||||
|
||||
if (categoriesResult.status === 'fulfilled') {
|
||||
categoriesData = categoriesResult.value || [];
|
||||
}
|
||||
|
||||
if (brandsResult.status === 'fulfilled') {
|
||||
brandsData = brandsResult.value || [];
|
||||
}
|
||||
|
||||
const catalogProducts: Product[] =
|
||||
dbProducts.length > 0 ? dbProducts.map(mapCatalogProductToProduct) : [];
|
||||
|
||||
const { activeSections, sectionsData, layoutLoaded } =
|
||||
buildHomepageSections(layoutData);
|
||||
|
||||
/** Honor dashboard Visible/Hidden (`metadata_json.is_active`) for every section. */
|
||||
const show = (key: string) =>
|
||||
isSectionVisible(activeSections, key, layoutLoaded);
|
||||
|
||||
const orderedKeys = sortHomeSectionsByOrder(sectionsData).map((s) => s.key);
|
||||
const productsLoading = false;
|
||||
|
||||
const sectionNodes: Record<string, React.ReactNode> = {
|
||||
hero_slider: show('hero_slider') ? (
|
||||
<HomeHeroRow
|
||||
key="hero_slider"
|
||||
heroConfig={sectionsData.hero_slider}
|
||||
showHero
|
||||
initialCategories={categoriesData}
|
||||
/>
|
||||
) : null,
|
||||
promo_cards: show('promo_cards') ? (
|
||||
<PromoCardsSection
|
||||
key="promo_cards"
|
||||
config={sectionsData.promo_cards}
|
||||
/>
|
||||
) : null,
|
||||
featured_categories: show('featured_categories') ? (
|
||||
<CategoriesSection
|
||||
key="featured_categories"
|
||||
config={sectionsData.featured_categories}
|
||||
initialCategories={categoriesData}
|
||||
/>
|
||||
) : null,
|
||||
editorial_story: show('editorial_story') ? (
|
||||
<NewArrivalsSection
|
||||
key="editorial_story"
|
||||
config={sectionsData.editorial_story}
|
||||
products={catalogProducts}
|
||||
loading={productsLoading}
|
||||
error={productsError}
|
||||
/>
|
||||
) : null,
|
||||
promo_banners: show('promo_banners') ? (
|
||||
<PromoBannersSection
|
||||
key="promo_banners"
|
||||
config={sectionsData.promo_banners}
|
||||
/>
|
||||
) : null,
|
||||
deal_of_the_day: show('deal_of_the_day') ? (
|
||||
<HotDealsSection
|
||||
key="deal_of_the_day"
|
||||
config={sectionsData.deal_of_the_day}
|
||||
products={catalogProducts}
|
||||
loading={productsLoading}
|
||||
error={productsError}
|
||||
/>
|
||||
) : null,
|
||||
recently_launched: show('recently_launched') ? (
|
||||
<RecentlyLaunchedSection
|
||||
key="recently_launched"
|
||||
config={sectionsData.recently_launched}
|
||||
products={catalogProducts}
|
||||
loading={productsLoading}
|
||||
error={productsError}
|
||||
/>
|
||||
) : null,
|
||||
official_brands: show('official_brands') ? (
|
||||
<BrandsSection
|
||||
key="official_brands"
|
||||
config={sectionsData.official_brands}
|
||||
initialBrands={brandsData}
|
||||
/>
|
||||
) : null,
|
||||
ultimate_tech: show('ultimate_tech') ? (
|
||||
<UltimateTechSection
|
||||
key="ultimate_tech"
|
||||
config={sectionsData.ultimate_tech}
|
||||
products={catalogProducts}
|
||||
loading={productsLoading}
|
||||
error={productsError}
|
||||
/>
|
||||
) : null,
|
||||
tech_blog: show('tech_blog') ? (
|
||||
<ArticlesSection key="tech_blog" config={sectionsData.tech_blog} />
|
||||
) : null,
|
||||
trust_badges: show('trust_badges') ? (
|
||||
<FeaturesSection
|
||||
key="trust_badges"
|
||||
config={sectionsData.trust_badges}
|
||||
/>
|
||||
) : null,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8F9FB] text-[#1E293B] flex flex-col font-sans">
|
||||
<AnnouncementBar
|
||||
config={sectionsData.announcement_bar}
|
||||
isActive={show('announcement_bar')}
|
||||
/>
|
||||
|
||||
<Header />
|
||||
|
||||
<Navbar
|
||||
config={sectionsData.nav_links}
|
||||
isActive={show('nav_links')}
|
||||
/>
|
||||
{/* After Header + Navbar so sticky chrome reserves full height */}
|
||||
<StickyHeaderSpacer />
|
||||
|
||||
<main className="flex-grow">
|
||||
{orderedKeys.map((key) => sectionNodes[key] ?? null)}
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
<FloatingButtons />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
12
app/payments/layout.tsx
Normal file
12
app/payments/layout.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import type { ReactNode } from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Payments | iFixKart',
|
||||
description:
|
||||
'How to pay on iFixKart: cards, UPI, net banking, wallets, repair advances, refunds, and payment security.',
|
||||
};
|
||||
|
||||
export default function PaymentsLayout({ children }: { children: ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
176
app/payments/page.tsx
Normal file
176
app/payments/page.tsx
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
'use client';
|
||||
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { CreditCard, Landmark, RefreshCw, ShieldCheck, Smartphone, Wallet } from 'lucide-react';
|
||||
import { AnnouncementBar } from '@/layout/AnnouncementBar';
|
||||
import { Header } from '@/layout/Header';
|
||||
import { Navbar } from '@/layout/Navbar';
|
||||
import { Footer } from '@/layout/Footer';
|
||||
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
|
||||
import { FloatingButtons } from '@/components/FloatingButtons';
|
||||
import {
|
||||
storefrontService,
|
||||
type FooterInfo,
|
||||
type StorefrontSettings,
|
||||
MOCK_FOOTER_INFO,
|
||||
} from '@/services/api/storefrontService';
|
||||
import { buildPaymentsDoc } from '@/lib/paymentsContent';
|
||||
import { PaymentMethodBadges } from '@/components/PaymentMethodBadges';
|
||||
|
||||
const HIGHLIGHT_ICONS = {
|
||||
cards: CreditCard,
|
||||
upi: Smartphone,
|
||||
netbanking: Landmark,
|
||||
wallets: Wallet,
|
||||
} as const;
|
||||
|
||||
function telHref(phone: string): string {
|
||||
return `tel:${phone.replace(/[^\d+]/g, '')}`;
|
||||
}
|
||||
|
||||
export default function PaymentsPage() {
|
||||
const [footer, setFooter] = useState<FooterInfo>(MOCK_FOOTER_INFO);
|
||||
const [settings, setSettings] = useState<StorefrontSettings>({ store_name: 'iFixKart' });
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
Promise.all([
|
||||
storefrontService.getFooterInfo(),
|
||||
storefrontService.getSettings(),
|
||||
]).then(([info, store]) => {
|
||||
if (cancelled) return;
|
||||
if (info) setFooter(info);
|
||||
if (store) setSettings(store);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const storeName = settings.store_name || 'iFixKart';
|
||||
const phone = footer.phone || settings.support_phone || '';
|
||||
const email = footer.email || '';
|
||||
const doc = useMemo(() => buildPaymentsDoc(storeName), [storeName]);
|
||||
const methods = (footer.payment_methods || []).filter((p) => p?.name || p?.icon_url);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8F9FB] text-[#1E293B] flex flex-col font-sans">
|
||||
<AnnouncementBar />
|
||||
<Header />
|
||||
<StickyHeaderSpacer />
|
||||
<Navbar />
|
||||
|
||||
<main className="flex-grow py-10 pb-20">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[900px]">
|
||||
<h1 className="text-[28px] md:text-[32px] font-bold text-[#1E293B]">
|
||||
{doc.heading}
|
||||
</h1>
|
||||
<p className="text-[14px] text-gray-500 mt-2 max-w-2xl">{doc.intro}</p>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-8">
|
||||
{doc.highlights.map((item) => {
|
||||
const Icon =
|
||||
HIGHLIGHT_ICONS[item.id as keyof typeof HIGHLIGHT_ICONS] || CreditCard;
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className="bg-white border border-gray-200 rounded-xl p-5"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-lg bg-primary/10 text-primary flex items-center justify-center mb-3">
|
||||
<Icon className="w-5 h-5" />
|
||||
</div>
|
||||
<h2 className="text-[16px] font-bold text-[#1E293B]">{item.title}</h2>
|
||||
<p className="text-[13px] text-gray-500 mt-1 leading-relaxed">
|
||||
{item.detail}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 bg-white border border-gray-200 rounded-xl p-5">
|
||||
<h2 className="text-[13px] font-bold text-[#1E293B]">We accept</h2>
|
||||
<PaymentMethodBadges methods={methods} className="mt-3" />
|
||||
</div>
|
||||
|
||||
<article className="mt-8 bg-white border border-gray-200 rounded-xl overflow-hidden">
|
||||
<div className="p-6 md:p-8">
|
||||
<div className="flex items-center gap-2 text-primary mb-6">
|
||||
<ShieldCheck className="w-5 h-5" />
|
||||
<p className="text-[13px] font-semibold">
|
||||
Checkout is processed on a secure payment page. Last updated: 11 September 2026
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-7">
|
||||
{doc.sections.map((section) => (
|
||||
<section key={section.id} id={section.id}>
|
||||
<h3 className="text-[16px] font-bold text-[#1E293B] mb-2">
|
||||
{section.title}
|
||||
</h3>
|
||||
{section.paragraphs.map((p, i) => (
|
||||
<p
|
||||
key={i}
|
||||
className="text-[14px] text-gray-600 leading-relaxed mb-3 last:mb-0"
|
||||
>
|
||||
{p}
|
||||
</p>
|
||||
))}
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<section className="mt-8 pt-6 border-t border-gray-100">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<RefreshCw className="w-4 h-4 text-gray-400" />
|
||||
<h3 className="text-[16px] font-bold text-[#1E293B]">Need help with a payment?</h3>
|
||||
</div>
|
||||
<p className="text-[14px] text-gray-600 leading-relaxed">
|
||||
Share your order number and payment reference with {storeName} support. You can
|
||||
also read{' '}
|
||||
<Link href="/faqs" className="text-primary font-semibold hover:underline">
|
||||
payment FAQs
|
||||
</Link>{' '}
|
||||
or our{' '}
|
||||
<Link href="/terms" className="text-primary font-semibold hover:underline">
|
||||
Terms & Policy
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<div className="mt-3 text-[14px] text-[#1E293B] space-y-1">
|
||||
{email ? (
|
||||
<p>
|
||||
Email:{' '}
|
||||
<a
|
||||
href={`mailto:${email}`}
|
||||
className="text-primary font-semibold hover:underline"
|
||||
>
|
||||
{email}
|
||||
</a>
|
||||
</p>
|
||||
) : null}
|
||||
{phone ? (
|
||||
<p>
|
||||
Phone:{' '}
|
||||
<a
|
||||
href={telHref(phone)}
|
||||
className="text-primary font-semibold hover:underline"
|
||||
>
|
||||
{phone}
|
||||
</a>
|
||||
</p>
|
||||
) : null}
|
||||
<p className="text-gray-500">Monday to Saturday, 10:00 AM – 6:30 PM</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
<FloatingButtons />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
12
app/privacy/layout.tsx
Normal file
12
app/privacy/layout.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import type { ReactNode } from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Privacy Policy | iFixKart',
|
||||
description:
|
||||
'How iFixKart collects, uses, and protects your personal information when you shop or book a repair.',
|
||||
};
|
||||
|
||||
export default function PrivacyLayout({ children }: { children: ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
7
app/privacy/page.tsx
Normal file
7
app/privacy/page.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import { LegalPage } from '@/components/legal/LegalPage';
|
||||
|
||||
export default function PrivacyRoutePage() {
|
||||
return <LegalPage initialTab="privacy" />;
|
||||
}
|
||||
5
app/products/[slug]/loading.tsx
Normal file
5
app/products/[slug]/loading.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { PageSkeleton } from '@/components/ui/Skeleton';
|
||||
|
||||
export default function Loading() {
|
||||
return <PageSkeleton variant="product" />;
|
||||
}
|
||||
426
app/products/[slug]/page.tsx
Normal file
426
app/products/[slug]/page.tsx
Normal file
|
|
@ -0,0 +1,426 @@
|
|||
import React from 'react';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { AnnouncementBar } from '@/layout/AnnouncementBar';
|
||||
import { Header } from '@/layout/Header';
|
||||
import { Navbar } from '@/layout/Navbar';
|
||||
import { Footer } from '@/layout/Footer';
|
||||
import { FloatingButtons } from '@/components/FloatingButtons';
|
||||
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
|
||||
|
||||
import { catalogServerService } from '@/services/api/catalogServerService';
|
||||
|
||||
|
||||
import { ProductInteractiveGrid } from '@/components/pdp/ProductInteractiveGrid';
|
||||
import { ProductTabsWrapper } from '@/components/pdp/ProductTabsWrapper';
|
||||
import { QuickComparisonTable } from '@/components/pdp/QuickComparisonTable';
|
||||
import { YouMayAlsoLike } from '@/components/pdp/YouMayAlsoLike';
|
||||
|
||||
interface ProductDetailPageProps {
|
||||
params: Promise<{ slug: string }>;
|
||||
}
|
||||
|
||||
const HIDDEN_SPEC_KEYS = [
|
||||
'cost_price',
|
||||
'costprice',
|
||||
'parent_media_key',
|
||||
'parent_category',
|
||||
'parent_id',
|
||||
'media_key',
|
||||
'sku',
|
||||
'barcode',
|
||||
'compare_price',
|
||||
];
|
||||
|
||||
function normalizeSpecKey(code: string): string {
|
||||
return String(code || '').trim().toLowerCase().replace(/[\s-]+/g, '_');
|
||||
}
|
||||
|
||||
function humanizeSpecLabel(code: string): string {
|
||||
const key = normalizeSpecKey(code);
|
||||
const labels: Record<string, string> = {
|
||||
quality: 'Quality',
|
||||
qul: 'Quality',
|
||||
qual: 'Quality',
|
||||
ram: 'RAM',
|
||||
storage: 'Storage',
|
||||
color: 'Color',
|
||||
colour: 'Color',
|
||||
clr: 'Color',
|
||||
size: 'Size',
|
||||
condition: 'Condition',
|
||||
in_box_contents: 'In the box',
|
||||
warranty_months: 'Warranty',
|
||||
country_of_origin: 'Country of origin',
|
||||
device_model: 'Device model',
|
||||
device_series: 'Device series',
|
||||
};
|
||||
if (labels[key]) return labels[key];
|
||||
return code
|
||||
.replace(/[_-]+/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
function looksLikeId(value?: string | null): boolean {
|
||||
if (!value) return true;
|
||||
if (value.length > 18 && /[0-9]/.test(value)) return true;
|
||||
if (/^[0-9A-Z]{16,}$/i.test(value.replace(/[_-]/g, ''))) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function extractDeviceModel(product: any, specifications: any[] = []): string {
|
||||
if (product.device_model_name && !looksLikeId(product.device_model_name)) {
|
||||
return product.device_model_name;
|
||||
}
|
||||
if (typeof product.device_model === 'object' && product.device_model?.name) {
|
||||
return product.device_model.name;
|
||||
}
|
||||
if (typeof product.device_model === 'string' && product.device_model && !looksLikeId(product.device_model)) {
|
||||
return product.device_model;
|
||||
}
|
||||
|
||||
if (Array.isArray(specifications)) {
|
||||
const modelRef = specifications.find((s: any) =>
|
||||
['model reference', 'device model', 'model', 'device_model', 'model_reference'].includes(
|
||||
String(s.label || s.code || s.name || '').toLowerCase()
|
||||
)
|
||||
);
|
||||
if (modelRef && modelRef.value && modelRef.value !== 'N/A' && !looksLikeId(modelRef.value)) {
|
||||
return modelRef.value;
|
||||
}
|
||||
|
||||
const seriesRef = specifications.find((s: any) =>
|
||||
['series', 'device series', 'device_series'].includes(
|
||||
String(s.label || s.code || s.name || '').toLowerCase()
|
||||
)
|
||||
);
|
||||
if (seriesRef && seriesRef.value && seriesRef.value !== 'N/A' && !looksLikeId(seriesRef.value)) {
|
||||
return seriesRef.value;
|
||||
}
|
||||
}
|
||||
|
||||
if (product.device_series_name && !looksLikeId(product.device_series_name)) {
|
||||
return product.device_series_name;
|
||||
}
|
||||
if (typeof product.device_series === 'object' && product.device_series?.name) {
|
||||
return product.device_series.name;
|
||||
}
|
||||
if (typeof product.device_series === 'string' && product.device_series && !looksLikeId(product.device_series)) {
|
||||
return product.device_series;
|
||||
}
|
||||
|
||||
const attrRows = variantAttributeRows(product);
|
||||
const devAttr = attrRows.find((a) =>
|
||||
['device_model', 'device_series', 'compatible_device', 'supported_device', 'model_reference', 'model'].includes(a.key)
|
||||
);
|
||||
if (devAttr && devAttr.value && !looksLikeId(devAttr.value)) {
|
||||
return devAttr.value;
|
||||
}
|
||||
|
||||
const title = String(product.name || product.title || '').trim();
|
||||
if (title) {
|
||||
const cleaned = title
|
||||
.replace(/\s*(?:with\s+touch\s+screen|with\s+touch|display|screen|touch\s+glass|combo|battery|panel|housing|back\s+glass|charging\s+port|flex\s+cable|spare\s+parts|replacement)\b.*/i, '')
|
||||
.replace(/^buy\s+/i, '')
|
||||
.trim();
|
||||
if (cleaned) return cleaned;
|
||||
return title;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function variantAttributeRows(product: any): { key: string; label: string; value: string }[] {
|
||||
const collected = new Map<string, Set<string>>();
|
||||
(product.variants || []).forEach((variant: any) => {
|
||||
(variant.attributes || []).forEach((attr: any) => {
|
||||
const code = String(attr.attribute_code || '');
|
||||
const key = normalizeSpecKey(code);
|
||||
if (!code || HIDDEN_SPEC_KEYS.some((h) => key === h || key.includes(h))) return;
|
||||
if (key.startsWith('parent_') || key.includes('cost')) return;
|
||||
const value = String(attr.attribute_value || '').trim();
|
||||
if (!value) return;
|
||||
if (!collected.has(key)) collected.set(key, new Set());
|
||||
collected.get(key)!.add(value);
|
||||
});
|
||||
});
|
||||
|
||||
return Array.from(collected.entries()).map(([key, values]) => ({
|
||||
key,
|
||||
label: humanizeSpecLabel(key),
|
||||
value: Array.from(values).join(', '),
|
||||
}));
|
||||
}
|
||||
|
||||
function groupSpecs(specifications: any[], product: any) {
|
||||
const groups: { title: string; rows: { label: string; value: string }[] }[] = [];
|
||||
const attrRows = variantAttributeRows(product);
|
||||
|
||||
const mapped = (specifications || []).map((spec: any) => ({
|
||||
group: String(spec.group || spec.section || spec.category || '').trim(),
|
||||
label: String(spec.label || spec.name || spec.key || ''),
|
||||
value: String(spec.value ?? spec.val ?? ''),
|
||||
})).filter((s) => s.label);
|
||||
|
||||
const brandValue = !looksLikeId(product.brand_name)
|
||||
? product.brand_name
|
||||
: !looksLikeId(product.brand_id)
|
||||
? product.brand_id
|
||||
: 'Gallery';
|
||||
|
||||
const used = new Set<string>();
|
||||
const take = (keys: string[]) =>
|
||||
attrRows
|
||||
.filter((row) => keys.includes(row.key))
|
||||
.map((row) => {
|
||||
used.add(row.key);
|
||||
return { label: row.label, value: row.value };
|
||||
});
|
||||
|
||||
const general = [
|
||||
{ label: 'Brand', value: brandValue },
|
||||
{ label: 'Model', value: product.name },
|
||||
...take(['device_series', 'device_model', 'condition', 'country_of_origin']),
|
||||
];
|
||||
const memory = take(['ram', 'storage', 'memory']);
|
||||
const appearance = take(['color', 'colour']);
|
||||
const pack = [
|
||||
...(product.warranty_type ? [{ label: 'Warranty Type', value: product.warranty_type }] : []),
|
||||
...(product.warranty_summary ? [{ label: 'Warranty Details', value: product.warranty_summary }] : []),
|
||||
...take(['in_box_contents', 'warranty_months', 'warranty']),
|
||||
];
|
||||
const leftover = attrRows
|
||||
.filter((row) => !used.has(row.key))
|
||||
.map((row) => ({ label: row.label, value: row.value }));
|
||||
|
||||
groups.push({ title: 'GENERAL INFORMATION', rows: general });
|
||||
if (memory.length) groups.push({ title: 'MEMORY & STORAGE', rows: memory });
|
||||
if (appearance.length) groups.push({ title: 'APPEARANCE', rows: appearance });
|
||||
if (pack.length) groups.push({ title: 'PACKAGE & WARRANTY', rows: pack });
|
||||
if (leftover.length) groups.push({ title: 'ADDITIONAL DETAILS', rows: leftover });
|
||||
|
||||
if (mapped.some((s) => s.group)) {
|
||||
const bucket: Record<string, { label: string; value: string }[]> = {};
|
||||
mapped.forEach((s) => {
|
||||
const key = (s.group || 'PRODUCT DETAILS').toUpperCase();
|
||||
if (!bucket[key]) bucket[key] = [];
|
||||
bucket[key].push({ label: s.label, value: s.value });
|
||||
});
|
||||
Object.entries(bucket).forEach(([title, rows]) => {
|
||||
if (rows.length) groups.push({ title, rows });
|
||||
});
|
||||
return groups;
|
||||
}
|
||||
|
||||
if (mapped.length) {
|
||||
groups.push({
|
||||
title: 'PRODUCT DETAILS',
|
||||
rows: mapped.map((s) => ({ label: s.label, value: s.value })),
|
||||
});
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
export default async function ProductDetailPage({ params }: ProductDetailPageProps) {
|
||||
const { slug } = await params;
|
||||
|
||||
if (!slug) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
let data: any = null;
|
||||
try {
|
||||
data = await catalogServerService.getProductDetail(slug);
|
||||
} catch (err) {
|
||||
console.error('Failed to load product details on server:', err);
|
||||
return notFound();
|
||||
}
|
||||
|
||||
if (!data || !data.product) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const { product, reviews = [], related_products = [], specifications = [] } = data;
|
||||
const related = related_products.slice(0, 5);
|
||||
const comparisonProducts = [product, ...related_products.filter((p: any) => p.product_id !== product.product_id)].slice(0, 5);
|
||||
const specGroups = groupSpecs(specifications, product);
|
||||
|
||||
const additionalInfoRows: { label: string; value: string }[] = [];
|
||||
|
||||
// Supported Brand (auto-fetched)
|
||||
let brandVal = product.brand_name || product.brand?.name || '';
|
||||
if (looksLikeId(brandVal)) {
|
||||
brandVal = '';
|
||||
}
|
||||
if (!brandVal) {
|
||||
const brandAttr = variantAttributeRows(product).find((a) => a.key === 'brand' || a.key === 'brand_name');
|
||||
if (brandAttr && !looksLikeId(brandAttr.value)) {
|
||||
brandVal = brandAttr.value;
|
||||
}
|
||||
}
|
||||
if (brandVal) {
|
||||
additionalInfoRows.push({ label: 'Supported Brand', value: brandVal });
|
||||
}
|
||||
|
||||
// Supported Device (auto-fetched exact device model)
|
||||
let deviceVal = extractDeviceModel(product, specifications);
|
||||
if (deviceVal) {
|
||||
additionalInfoRows.push({ label: 'Supported Device', value: deviceVal });
|
||||
}
|
||||
|
||||
const wType = product.warranty_type || 'Brand Warranty';
|
||||
additionalInfoRows.push({ label: 'Warranty Type', value: wType });
|
||||
|
||||
if (product.warranty_summary || product.warranty_details) {
|
||||
additionalInfoRows.push({
|
||||
label: 'Warranty Details',
|
||||
value: product.warranty_summary || product.warranty_details,
|
||||
});
|
||||
}
|
||||
|
||||
const attrRowsForPack = variantAttributeRows(product);
|
||||
attrRowsForPack.forEach((attr) => {
|
||||
if (['in_box_contents', 'warranty_months', 'warranty'].includes(attr.key)) {
|
||||
if (!additionalInfoRows.some((p) => p.label.toLowerCase() === attr.label.toLowerCase())) {
|
||||
additionalInfoRows.push({ label: attr.label, value: attr.value });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const descriptionHtml = (
|
||||
<div>
|
||||
{product.description ? (
|
||||
<div className="text-[13px] text-[#555] leading-relaxed px-5 py-4">
|
||||
<div
|
||||
className="product-description-html prose prose-sm max-w-none text-[13px] text-[#555] leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: product.description }}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-[13px] text-gray-400 px-5 py-6">
|
||||
No description available for this product.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{additionalInfoRows.length > 0 && (
|
||||
<div className="border-t border-[#e0e0e0]">
|
||||
<table className="w-full border-collapse text-[13px]">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th
|
||||
colSpan={2}
|
||||
className="text-left text-primary font-bold uppercase tracking-wide text-[13px] bg-[#f6f7f9] px-5 py-3 border-b border-[#e0e0e0]"
|
||||
>
|
||||
ADDITIONAL INFORMATION
|
||||
</th>
|
||||
</tr>
|
||||
{additionalInfoRows.map((row, idx) => {
|
||||
const isLast = idx === additionalInfoRows.length - 1;
|
||||
return (
|
||||
<tr key={`pw-${row.label}`}>
|
||||
<td
|
||||
className={`w-[28%] align-top font-semibold text-[#444] px-5 py-3.5 ${
|
||||
isLast ? '' : 'border-b border-[#e0e0e0]'
|
||||
}`}
|
||||
>
|
||||
{row.label}
|
||||
</td>
|
||||
<td
|
||||
className={`align-top text-[#555] px-5 py-3.5 ${
|
||||
isLast ? '' : 'border-b border-[#e0e0e0]'
|
||||
}`}
|
||||
>
|
||||
{row.value}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const specsHtml = (
|
||||
<table className="w-full border-collapse text-[13px]">
|
||||
<tbody>
|
||||
{specGroups.map((group, groupIdx) => (
|
||||
<React.Fragment key={group.title}>
|
||||
<tr>
|
||||
<th
|
||||
colSpan={2}
|
||||
className={`text-left text-primary font-bold uppercase tracking-wide text-[13px] bg-[#f6f7f9] px-5 py-3 border-b border-[#e0e0e0] ${
|
||||
groupIdx > 0 ? 'border-t border-[#e0e0e0]' : ''
|
||||
}`}
|
||||
>
|
||||
{group.title}
|
||||
</th>
|
||||
</tr>
|
||||
{group.rows.map((row, rowIdx) => {
|
||||
const isLast =
|
||||
groupIdx === specGroups.length - 1 && rowIdx === group.rows.length - 1;
|
||||
return (
|
||||
<tr key={`${group.title}-${row.label}`}>
|
||||
<td
|
||||
className={`w-[28%] align-top font-semibold text-[#444] px-5 py-3.5 ${
|
||||
isLast ? '' : 'border-b border-[#e0e0e0]'
|
||||
}`}
|
||||
>
|
||||
{row.label}
|
||||
</td>
|
||||
<td
|
||||
className={`align-top text-[#555] px-5 py-3.5 ${
|
||||
isLast ? '' : 'border-b border-[#e0e0e0]'
|
||||
}`}
|
||||
>
|
||||
{row.value}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white text-[#1E293B] flex flex-col font-sans">
|
||||
<AnnouncementBar />
|
||||
<Header />
|
||||
<StickyHeaderSpacer />
|
||||
<Navbar />
|
||||
|
||||
<main className="flex-grow pb-24 text-gray-800 antialiased">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-5">
|
||||
<ProductInteractiveGrid
|
||||
product={product}
|
||||
initialReviewsCount={reviews.length}
|
||||
relatedProducts={related}
|
||||
/>
|
||||
|
||||
<div className="mt-10">
|
||||
<ProductTabsWrapper
|
||||
descriptionHtml={descriptionHtml}
|
||||
specsHtml={specsHtml}
|
||||
showSpecifications={product.show_specifications ?? true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{related.length > 0 && <YouMayAlsoLike products={related} />}
|
||||
|
||||
{comparisonProducts.length > 0 && (
|
||||
<QuickComparisonTable products={comparisonProducts} />
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
<FloatingButtons />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
5
app/products/loading.tsx
Normal file
5
app/products/loading.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { PageSkeleton } from '@/components/ui/Skeleton';
|
||||
|
||||
export default function Loading() {
|
||||
return <PageSkeleton variant="catalog" />;
|
||||
}
|
||||
91
app/products/page.tsx
Normal file
91
app/products/page.tsx
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
/**
|
||||
* @page Products Catalog Router Shell (`app/products/page.tsx`)
|
||||
* @purpose Evaluates whether the selected category has `is_parent_feature = True`.
|
||||
* If enabled, renders <ParentFeatureCatalog category={category} /> (Brand -> Series -> Model hierarchical mode).
|
||||
* Otherwise, renders <StandardCatalog /> (Standard sidebar filter mode).
|
||||
*/
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, Suspense } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { catalogService, CategoryResponse } from '@/services/api/catalogService';
|
||||
import { ParentFeatureCatalog } from '@/components/catalog/ParentFeatureCatalog';
|
||||
import { StandardCatalog } from '@/components/catalog/StandardCatalog';
|
||||
|
||||
function CatalogPageContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const categoryParam = searchParams.get('category');
|
||||
const brandParam = searchParams.get('brand');
|
||||
const searchQuery = searchParams.get('search');
|
||||
|
||||
const [categories, setCategories] = useState<CategoryResponse[]>([]);
|
||||
const [activeCategory, setActiveCategory] = useState<CategoryResponse | null>(null);
|
||||
const [isLoadingCategory, setIsLoadingCategory] = useState<boolean>(true);
|
||||
|
||||
// Fetch Categories and evaluate parent feature flag
|
||||
useEffect(() => {
|
||||
setIsLoadingCategory(true);
|
||||
catalogService
|
||||
.getCategories()
|
||||
.then((catData) => {
|
||||
setCategories(catData);
|
||||
if (categoryParam) {
|
||||
const found = catData.find(
|
||||
(c) =>
|
||||
c.category_id === categoryParam ||
|
||||
c.slug.toLowerCase() === categoryParam.toLowerCase()
|
||||
);
|
||||
setActiveCategory(found || null);
|
||||
} else {
|
||||
setActiveCategory(null);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setCategories([]);
|
||||
setActiveCategory(null);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoadingCategory(false);
|
||||
});
|
||||
}, [categoryParam]);
|
||||
|
||||
if (isLoadingCategory) {
|
||||
return (
|
||||
<div className="w-full bg-[#f8f9fa] min-h-screen py-12 font-sans flex items-center justify-center">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="w-8 h-8 border-3 border-red-600 border-t-transparent rounded-full animate-spin" />
|
||||
<p className="text-xs font-semibold text-gray-500">Loading catalog...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If a category is selected AND has is_parent_feature enabled -> Render ParentFeatureCatalog
|
||||
if (activeCategory && activeCategory.is_parent_feature) {
|
||||
return <ParentFeatureCatalog category={activeCategory} />;
|
||||
}
|
||||
|
||||
// Otherwise -> Render StandardCatalog
|
||||
return (
|
||||
<StandardCatalog
|
||||
initialCategory={activeCategory}
|
||||
categoryParam={categoryParam}
|
||||
brandParam={brandParam}
|
||||
searchQuery={searchQuery}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ProductsPage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="w-full bg-[#f8f9fa] min-h-screen py-12 font-sans flex items-center justify-center">
|
||||
<div className="w-8 h-8 border-3 border-red-600 border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<CatalogPageContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
5
app/repair-services/loading.tsx
Normal file
5
app/repair-services/loading.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { PageSkeleton } from '@/components/ui/Skeleton';
|
||||
|
||||
export default function Loading() {
|
||||
return <PageSkeleton variant="simple" />;
|
||||
}
|
||||
9
app/repair-services/page.tsx
Normal file
9
app/repair-services/page.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
* @page Repair Services Route (`app/repair-services/page.tsx`)
|
||||
* @purpose Renders device repair booking portal for /repair-services URLs.
|
||||
*/
|
||||
import RepairPortalPage from '../services/page';
|
||||
|
||||
export default function RepairServicesRoute() {
|
||||
return <RepairPortalPage />;
|
||||
}
|
||||
12
app/returns/layout.tsx
Normal file
12
app/returns/layout.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import type { ReactNode } from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Returns | iFixKart',
|
||||
description:
|
||||
'Return unused accessories, report a defective delivery, or start a warranty claim on an iFixKart order.',
|
||||
};
|
||||
|
||||
export default function ReturnsLayout({ children }: { children: ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
179
app/returns/page.tsx
Normal file
179
app/returns/page.tsx
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
'use client';
|
||||
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Clock, Package, RefreshCw, ShieldCheck, Undo2 } from 'lucide-react';
|
||||
import { AnnouncementBar } from '@/layout/AnnouncementBar';
|
||||
import { Header } from '@/layout/Header';
|
||||
import { Navbar } from '@/layout/Navbar';
|
||||
import { Footer } from '@/layout/Footer';
|
||||
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
|
||||
import { FloatingButtons } from '@/components/FloatingButtons';
|
||||
import {
|
||||
storefrontService,
|
||||
type FooterInfo,
|
||||
type StorefrontSettings,
|
||||
MOCK_FOOTER_INFO,
|
||||
} from '@/services/api/storefrontService';
|
||||
import { buildReturnsDoc } from '@/lib/returnsContent';
|
||||
|
||||
const HIGHLIGHT_ICONS = {
|
||||
window: Clock,
|
||||
doa: Package,
|
||||
refund: RefreshCw,
|
||||
warranty: ShieldCheck,
|
||||
} as const;
|
||||
|
||||
function telHref(phone: string): string {
|
||||
return `tel:${phone.replace(/[^\d+]/g, '')}`;
|
||||
}
|
||||
|
||||
export default function ReturnsPage() {
|
||||
const [footer, setFooter] = useState<FooterInfo>(MOCK_FOOTER_INFO);
|
||||
const [settings, setSettings] = useState<StorefrontSettings>({ store_name: 'iFixKart' });
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
Promise.all([
|
||||
storefrontService.getFooterInfo(),
|
||||
storefrontService.getSettings(),
|
||||
]).then(([info, store]) => {
|
||||
if (cancelled) return;
|
||||
if (info) setFooter(info);
|
||||
if (store) setSettings(store);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const storeName = settings.store_name || 'iFixKart';
|
||||
const phone = footer.phone || settings.support_phone || '';
|
||||
const email = footer.email || '';
|
||||
const doc = useMemo(() => buildReturnsDoc(storeName), [storeName]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8F9FB] text-[#1E293B] flex flex-col font-sans">
|
||||
<AnnouncementBar />
|
||||
<Header />
|
||||
<StickyHeaderSpacer />
|
||||
<Navbar />
|
||||
|
||||
<main className="flex-grow py-10 pb-20">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[900px]">
|
||||
<h1 className="text-[28px] md:text-[32px] font-bold text-[#1E293B]">
|
||||
{doc.heading}
|
||||
</h1>
|
||||
<p className="text-[14px] text-gray-500 mt-2 max-w-2xl">{doc.intro}</p>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-8">
|
||||
{doc.highlights.map((item) => {
|
||||
const Icon =
|
||||
HIGHLIGHT_ICONS[item.id as keyof typeof HIGHLIGHT_ICONS] || Undo2;
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className="bg-white border border-gray-200 rounded-xl p-5"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-lg bg-primary/10 text-primary flex items-center justify-center mb-3">
|
||||
<Icon className="w-5 h-5" />
|
||||
</div>
|
||||
<h2 className="text-[16px] font-bold text-[#1E293B]">{item.title}</h2>
|
||||
<p className="text-[13px] text-gray-500 mt-1 leading-relaxed">
|
||||
{item.detail}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<article className="mt-8 bg-white border border-gray-200 rounded-xl overflow-hidden">
|
||||
<div className="p-6 md:p-8">
|
||||
<div className="flex items-center gap-2 text-primary mb-6">
|
||||
<Undo2 className="w-5 h-5" />
|
||||
<p className="text-[13px] font-semibold">
|
||||
Approved pickups need an original invoice. Last updated: 11 September 2026
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-7">
|
||||
{doc.sections.map((section) => (
|
||||
<section key={section.id} id={section.id}>
|
||||
<h3 className="text-[16px] font-bold text-[#1E293B] mb-2">
|
||||
{section.title}
|
||||
</h3>
|
||||
{section.paragraphs.map((p, i) => (
|
||||
<p
|
||||
key={i}
|
||||
className="text-[14px] text-gray-600 leading-relaxed mb-3 last:mb-0"
|
||||
>
|
||||
{p}
|
||||
</p>
|
||||
))}
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<section className="mt-8 pt-6 border-t border-gray-100">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<RefreshCw className="w-4 h-4 text-gray-400" />
|
||||
<h3 className="text-[16px] font-bold text-[#1E293B]">Need help with a return?</h3>
|
||||
</div>
|
||||
<p className="text-[14px] text-gray-600 leading-relaxed">
|
||||
Write to{' '}
|
||||
<a
|
||||
href="mailto:returns@ifixkart.com"
|
||||
className="text-primary font-semibold hover:underline"
|
||||
>
|
||||
returns@ifixkart.com
|
||||
</a>{' '}
|
||||
with your order number. You can also read{' '}
|
||||
<Link href="/faqs" className="text-primary font-semibold hover:underline">
|
||||
FAQs
|
||||
</Link>
|
||||
,{' '}
|
||||
<Link href="/payments" className="text-primary font-semibold hover:underline">
|
||||
Payments
|
||||
</Link>
|
||||
, or our{' '}
|
||||
<Link href="/terms" className="text-primary font-semibold hover:underline">
|
||||
Terms & Policy
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<div className="mt-3 text-[14px] text-[#1E293B] space-y-1">
|
||||
{email ? (
|
||||
<p>
|
||||
Support:{' '}
|
||||
<a
|
||||
href={`mailto:${email}`}
|
||||
className="text-primary font-semibold hover:underline"
|
||||
>
|
||||
{email}
|
||||
</a>
|
||||
</p>
|
||||
) : null}
|
||||
{phone ? (
|
||||
<p>
|
||||
Phone:{' '}
|
||||
<a
|
||||
href={telHref(phone)}
|
||||
className="text-primary font-semibold hover:underline"
|
||||
>
|
||||
{phone}
|
||||
</a>
|
||||
</p>
|
||||
) : null}
|
||||
<p className="text-gray-500">Monday to Saturday, 10:00 AM – 6:30 PM</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
<FloatingButtons />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
5
app/services/loading.tsx
Normal file
5
app/services/loading.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { PageSkeleton } from '@/components/ui/Skeleton';
|
||||
|
||||
export default function Loading() {
|
||||
return <PageSkeleton variant="simple" />;
|
||||
}
|
||||
1612
app/services/page.tsx
Normal file
1612
app/services/page.tsx
Normal file
File diff suppressed because it is too large
Load diff
554
app/services/success/page.tsx
Normal file
554
app/services/success/page.tsx
Normal file
|
|
@ -0,0 +1,554 @@
|
|||
/**
|
||||
* @page Service Booking Success & Payment Confirmation (`app/services/success/page.tsx`)
|
||||
* @purpose Storefront-chrome confirmation with booking + deposit payment summary.
|
||||
*/
|
||||
'use client';
|
||||
|
||||
import { Suspense, useEffect, useMemo, useState, type ReactNode } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { toast } from 'sonner';
|
||||
import { formatCurrency } from '@/lib/utils';
|
||||
import { serviceBookingService } from '@/services/api/serviceBooking';
|
||||
import { setAccessToken } from '@/services/api/client';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { parseMetadata } from '@/lib/layoutConfig';
|
||||
import {
|
||||
findServiceSection,
|
||||
isServiceSectionVisible,
|
||||
normalizeServiceLayout,
|
||||
} from '@/lib/servicePageLayout';
|
||||
import {
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
Home,
|
||||
Loader2,
|
||||
CreditCard,
|
||||
ShieldCheck,
|
||||
Smartphone,
|
||||
Wrench,
|
||||
ArrowRight,
|
||||
} from 'lucide-react';
|
||||
import { AnnouncementBar } from '@/layout/AnnouncementBar';
|
||||
import { Header } from '@/layout/Header';
|
||||
import { Navbar } from '@/layout/Navbar';
|
||||
import { Footer } from '@/layout/Footer';
|
||||
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
|
||||
import { FloatingButtons } from '@/components/FloatingButtons';
|
||||
import { ClientModalsContainer } from '@/components/ClientModalsContainer';
|
||||
|
||||
function PageShell({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8F9FB] text-[#1E293B] flex flex-col font-sans">
|
||||
<AnnouncementBar />
|
||||
<Header />
|
||||
<StickyHeaderSpacer />
|
||||
<Navbar />
|
||||
<main className="flex-grow pb-16">{children}</main>
|
||||
<Footer />
|
||||
<FloatingButtons />
|
||||
<ClientModalsContainer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function unwrapJob(raw: any): any {
|
||||
if (!raw) return null;
|
||||
if (raw.job && typeof raw.job === 'object') return { ...raw, ...raw.job };
|
||||
return raw;
|
||||
}
|
||||
|
||||
function firstPayment(job: any): any | null {
|
||||
if (!job) return null;
|
||||
const list = Array.isArray(job.payments)
|
||||
? job.payments
|
||||
: Array.isArray(job.payment_records)
|
||||
? job.payment_records
|
||||
: job.payment
|
||||
? [job.payment]
|
||||
: [];
|
||||
return list[0] || null;
|
||||
}
|
||||
|
||||
function alreadyVerifiedMessage(message?: string) {
|
||||
const text = String(message || '').toLowerCase();
|
||||
return (
|
||||
text.includes('already verified') ||
|
||||
text.includes('already captured') ||
|
||||
text.includes('already processed')
|
||||
);
|
||||
}
|
||||
|
||||
function BookingSuccessContent() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { accessToken, authReady, bootstrapAuth } = useAuthStore();
|
||||
|
||||
const jobNoParam = searchParams.get('job_no') || 'SRV-BOOKING';
|
||||
const jobId = searchParams.get('job_id') || '';
|
||||
const statusParam = searchParams.get('status') || '';
|
||||
const qPaymentId = searchParams.get('payment_id') || '';
|
||||
const qRzpPaymentId = searchParams.get('rzp_payment_id') || '';
|
||||
const qRzpOrderId = searchParams.get('rzp_order_id') || '';
|
||||
const qRzpSignature = searchParams.get('rzp_signature') || '';
|
||||
const qAmount = Number(searchParams.get('amount') || 0);
|
||||
const qMethod = searchParams.get('method') || '';
|
||||
const needsVerify = searchParams.get('verify') === '1';
|
||||
|
||||
const [jobDetails, setJobDetails] = useState<any | null>(null);
|
||||
const [layoutCMS, setLayoutCMS] = useState<any[]>([]);
|
||||
const [verifyState, setVerifyState] = useState<'idle' | 'verifying' | 'done'>(
|
||||
needsVerify ? 'verifying' : 'idle'
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
void bootstrapAuth();
|
||||
}, [bootstrapAuth]);
|
||||
|
||||
useEffect(() => {
|
||||
if (accessToken) setAccessToken(accessToken);
|
||||
}, [accessToken]);
|
||||
|
||||
useEffect(() => {
|
||||
toast.dismiss();
|
||||
serviceBookingService.rememberLocalJob({
|
||||
job_id: jobId || undefined,
|
||||
job_no: jobNoParam !== 'SRV-BOOKING' ? jobNoParam : undefined,
|
||||
});
|
||||
}, [jobId, jobNoParam]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!needsVerify) {
|
||||
setVerifyState('done');
|
||||
if (jobId) void loadJobDetails(jobId);
|
||||
return;
|
||||
}
|
||||
if (!authReady) return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
const finish = (verified: boolean) => {
|
||||
if (cancelled) return;
|
||||
setVerifyState('done');
|
||||
const next = new URLSearchParams(searchParams.toString());
|
||||
next.delete('verify');
|
||||
next.delete('rzp_signature');
|
||||
if (verified) next.set('status', 'PAID');
|
||||
router.replace(`/services/success?${next.toString()}`);
|
||||
if (jobId) void loadJobDetails(jobId);
|
||||
};
|
||||
|
||||
const runVerify = async () => {
|
||||
const token = useAuthStore.getState().accessToken;
|
||||
if (token) setAccessToken(token);
|
||||
|
||||
if (!qPaymentId || !qRzpOrderId || !qRzpPaymentId || !qRzpSignature) {
|
||||
finish(Boolean(qRzpPaymentId));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let timer = 0;
|
||||
const verifyPromise = serviceBookingService.verifyServicePayment({
|
||||
payment_id: qPaymentId,
|
||||
razorpay_order_id: qRzpOrderId,
|
||||
razorpay_payment_id: qRzpPaymentId,
|
||||
razorpay_signature: qRzpSignature,
|
||||
});
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
timer = window.setTimeout(() => reject(new Error('timeout')), 10000);
|
||||
});
|
||||
try {
|
||||
await Promise.race([verifyPromise, timeoutPromise]);
|
||||
} finally {
|
||||
window.clearTimeout(timer);
|
||||
}
|
||||
finish(true);
|
||||
} catch (err: any) {
|
||||
if (alreadyVerifiedMessage(err?.message) || err?.message === 'timeout') {
|
||||
finish(true);
|
||||
return;
|
||||
}
|
||||
toast.error(err?.message || 'Payment confirmation is taking longer than expected.');
|
||||
finish(Boolean(qRzpPaymentId));
|
||||
}
|
||||
};
|
||||
|
||||
void runVerify();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [authReady, needsVerify, jobId]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadLayout = async () => {
|
||||
try {
|
||||
const cms = await serviceBookingService.fetchServicePageLayout();
|
||||
setLayoutCMS(normalizeServiceLayout(cms));
|
||||
} catch {
|
||||
/* defaults */
|
||||
}
|
||||
};
|
||||
void loadLayout();
|
||||
}, []);
|
||||
|
||||
const loadJobDetails = async (id: string) => {
|
||||
try {
|
||||
const data = await serviceBookingService.fetchServiceJob(id);
|
||||
setJobDetails(data);
|
||||
const item = unwrapJob(data);
|
||||
serviceBookingService.rememberLocalJob({
|
||||
job_id: item?.job_id || id,
|
||||
job_no: item?.job_no || jobNoParam,
|
||||
});
|
||||
} catch {
|
||||
/* query-param fallback */
|
||||
}
|
||||
};
|
||||
|
||||
const job = unwrapJob(jobDetails);
|
||||
const payment = firstPayment(job);
|
||||
const isPaid =
|
||||
statusParam === 'PAID' ||
|
||||
Boolean(qRzpPaymentId) ||
|
||||
String(payment?.status || payment?.payment_status || '').toUpperCase() ===
|
||||
'CAPTURED';
|
||||
const isPending = statusParam === 'PENDING' && !isPaid;
|
||||
|
||||
const jobNo = job?.job_no || jobNoParam;
|
||||
const status = job?.status || (isPending ? 'PENDING' : 'BOOKED');
|
||||
const deviceLabel = [job?.device_brand, job?.device_model].filter(Boolean).join(' ');
|
||||
const serviceName =
|
||||
job?.service_name ||
|
||||
job?.custom_service_name ||
|
||||
job?.repair_category ||
|
||||
'';
|
||||
const variantName =
|
||||
job?.variant_name ||
|
||||
job?.repair_variant_name ||
|
||||
payment?.variant_name ||
|
||||
'';
|
||||
|
||||
const basePrice = Number(
|
||||
job?.base_price ?? job?.quoted_price ?? payment?.base_amount ?? 0
|
||||
);
|
||||
const paidFromPayment = Number(payment?.amount ?? payment?.paid_amount ?? 0);
|
||||
const paidFromQuery = qAmount > 1000 ? qAmount / 100 : qAmount;
|
||||
const paidAmount =
|
||||
paidFromPayment > 0
|
||||
? paidFromPayment
|
||||
: paidFromQuery > 0
|
||||
? paidFromQuery
|
||||
: basePrice > 0
|
||||
? basePrice * 0.2
|
||||
: 0;
|
||||
const doorstepFee = job?.fulfillment_type === 'DOORSTEP_PICKUP' ? 250 : 0;
|
||||
const totalQuote = basePrice + doorstepFee;
|
||||
const remaining = Math.max(totalQuote - paidAmount, 0);
|
||||
const paymentMethod =
|
||||
payment?.provider ||
|
||||
payment?.payment_method ||
|
||||
qMethod ||
|
||||
(isPending ? '—' : 'Razorpay');
|
||||
const paymentType = payment?.payment_type || (isPending ? 'ADVANCE' : 'ADVANCE');
|
||||
const paymentStatus =
|
||||
payment?.status ||
|
||||
payment?.payment_status ||
|
||||
(isPaid ? 'CAPTURED' : isPending ? 'PENDING' : 'CAPTURED');
|
||||
const gatewayPaymentId =
|
||||
payment?.razorpay_payment_id ||
|
||||
payment?.gateway_payment_id ||
|
||||
payment?.provider_payment_id ||
|
||||
qRzpPaymentId ||
|
||||
'';
|
||||
const internalPaymentId = payment?.payment_id || qPaymentId || '';
|
||||
const paidAt = payment?.paid_at || payment?.created_at || payment?.verified_at;
|
||||
const appointmentStart =
|
||||
job?.appointment?.scheduled_start || job?.scheduled_start;
|
||||
|
||||
const howItWorksCMS = findServiceSection(layoutCMS, 'service_how_it_works');
|
||||
const howMeta = parseMetadata(howItWorksCMS);
|
||||
const showHow = isServiceSectionVisible(layoutCMS, 'service_how_it_works');
|
||||
const nextSteps = howMeta.steps || [
|
||||
{
|
||||
number: '1',
|
||||
title: 'Technician Assignment',
|
||||
desc: 'Our system matches a certified repair specialist for your device model.',
|
||||
},
|
||||
{
|
||||
number: '2',
|
||||
title: 'On-Site Service',
|
||||
desc: 'Technician visits your location at the requested time slot to fix your device.',
|
||||
},
|
||||
{
|
||||
number: '3',
|
||||
title: 'Inspect & Settle',
|
||||
desc: 'Verify your device after repair, pay remaining balance, and get warranty cover.',
|
||||
},
|
||||
];
|
||||
const nextTitle = howItWorksCMS?.title || 'What happens next?';
|
||||
const nextSubtitle =
|
||||
howItWorksCMS?.subtitle || 'Your repair journey continues with these steps';
|
||||
|
||||
const infoRows = useMemo(() => {
|
||||
const rows: { label: string; value: ReactNode }[] = [
|
||||
{ label: 'Booking reference', value: <span className="font-mono">{jobNo}</span> },
|
||||
{ label: 'Job status', value: status },
|
||||
];
|
||||
if (deviceLabel) rows.push({ label: 'Device', value: deviceLabel });
|
||||
if (serviceName) rows.push({ label: 'Service', value: serviceName });
|
||||
if (variantName) rows.push({ label: 'Price variant', value: variantName });
|
||||
if (appointmentStart) {
|
||||
rows.push({
|
||||
label: 'Scheduled slot',
|
||||
value: new Date(appointmentStart).toLocaleString(),
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
}, [jobNo, status, deviceLabel, serviceName, variantName, appointmentStart]);
|
||||
|
||||
if (verifyState === 'verifying' || (needsVerify && !authReady)) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1400px] py-24 flex flex-col items-center justify-center gap-3 text-center">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-primary" />
|
||||
<p className="text-[14px] font-semibold text-[#1a1a1a]">
|
||||
Verifying payment
|
||||
</p>
|
||||
<p className="text-[12px] text-gray-500">
|
||||
Please wait a moment. Do not refresh this page.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1400px] py-8 space-y-6">
|
||||
<div className="bg-white border border-[#e5e5e5] p-4 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-[11px] font-semibold text-primary uppercase tracking-wider">
|
||||
Repair booking
|
||||
</div>
|
||||
<h1 className="text-xl font-bold text-[#222] mt-0.5">
|
||||
{isPending ? 'Payment confirmation' : 'Payment verified'}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="text-[12px] text-gray-500 font-semibold">
|
||||
Reference:{' '}
|
||||
<span className="font-mono font-bold text-[#222]">{jobNo}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-[#e5e5e5] p-6 sm:p-8 space-y-6">
|
||||
<div className="flex flex-col md:flex-row md:items-center gap-5">
|
||||
<div
|
||||
className={`w-14 h-14 shrink-0 flex items-center justify-center border ${
|
||||
isPending
|
||||
? 'bg-amber-50 border-amber-200 text-amber-600'
|
||||
: 'bg-emerald-50 border-emerald-200 text-emerald-600'
|
||||
}`}
|
||||
>
|
||||
{isPending ? (
|
||||
<Clock className="w-7 h-7" />
|
||||
) : (
|
||||
<CheckCircle2 className="w-7 h-7" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-2xl font-bold text-[#222] tracking-tight">
|
||||
{isPending ? 'Booking received' : 'Payment verified'}
|
||||
</h2>
|
||||
<p className="text-gray-500 text-sm mt-1 max-w-xl leading-relaxed">
|
||||
Service order{' '}
|
||||
<span className="font-mono font-bold text-[#222]">{jobNo}</span>{' '}
|
||||
{isPending
|
||||
? 'is saved. Complete the advance payment to lock your technician slot.'
|
||||
: 'is confirmed. Your advance payment is complete and our team is preparing your repair schedule.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
||||
<div className="border border-[#e5e5e5] p-5 space-y-4">
|
||||
<div className="flex items-center gap-2 text-[11px] font-semibold uppercase tracking-wider text-[#222] border-b border-[#e5e5e5] pb-2">
|
||||
<Smartphone className="w-4 h-4 text-primary" />
|
||||
Booking details
|
||||
</div>
|
||||
<dl className="space-y-3 text-[13px]">
|
||||
{infoRows.map((row) => (
|
||||
<div key={row.label} className="flex justify-between gap-4">
|
||||
<dt className="text-gray-500">{row.label}</dt>
|
||||
<dd className="font-semibold text-[#222] text-right">{row.value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div className="border border-[#e5e5e5] p-5 space-y-4">
|
||||
<div className="flex items-center gap-2 text-[11px] font-semibold uppercase tracking-wider text-[#222] border-b border-[#e5e5e5] pb-2">
|
||||
<CreditCard className="w-4 h-4 text-primary" />
|
||||
Payment information
|
||||
</div>
|
||||
<dl className="space-y-3 text-[13px]">
|
||||
<div className="flex justify-between gap-4">
|
||||
<dt className="text-gray-500">Payment type</dt>
|
||||
<dd className="font-semibold text-[#222]">{paymentType}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between gap-4">
|
||||
<dt className="text-gray-500">Method</dt>
|
||||
<dd className="font-semibold text-[#222]">{paymentMethod}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between gap-4">
|
||||
<dt className="text-gray-500">Payment status</dt>
|
||||
<dd
|
||||
className={`font-bold ${
|
||||
isPending ? 'text-amber-600' : 'text-emerald-600'
|
||||
}`}
|
||||
>
|
||||
{['CAPTURED', 'PAID', 'SUCCESS', 'COMPLETED'].includes(String(paymentStatus).toUpperCase()) ? 'PAID' : String(paymentStatus).replace(/_/g, ' ')}
|
||||
</dd>
|
||||
</div>
|
||||
{basePrice > 0 && (
|
||||
<div className="flex justify-between gap-4">
|
||||
<dt className="text-gray-500">Base service quote</dt>
|
||||
<dd className="font-semibold text-[#222]">
|
||||
{formatCurrency(basePrice)}
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between gap-4">
|
||||
<dt className="text-gray-500">
|
||||
{isPending ? 'Advance due (20%)' : 'Advance paid (20%)'}
|
||||
</dt>
|
||||
<dd className="font-bold text-primary">
|
||||
{paidAmount > 0 ? formatCurrency(paidAmount) : '—'}
|
||||
</dd>
|
||||
</div>
|
||||
{basePrice > 0 && (
|
||||
<div className="flex justify-between gap-4">
|
||||
<dt className="text-gray-500">Balance due at pickup</dt>
|
||||
<dd className="font-semibold text-[#222]">
|
||||
{formatCurrency(remaining)}
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{internalPaymentId ? (
|
||||
<div className="flex justify-between gap-4">
|
||||
<dt className="text-gray-500">Payment ID</dt>
|
||||
<dd className="font-mono text-[12px] font-semibold text-[#222] break-all text-right">
|
||||
{internalPaymentId}
|
||||
</dd>
|
||||
</div>
|
||||
) : null}
|
||||
{gatewayPaymentId ? (
|
||||
<div className="flex justify-between gap-4">
|
||||
<dt className="text-gray-500">Razorpay payment ID</dt>
|
||||
<dd className="font-mono text-[12px] font-semibold text-[#222] break-all text-right">
|
||||
{gatewayPaymentId}
|
||||
</dd>
|
||||
</div>
|
||||
) : null}
|
||||
{paidAt ? (
|
||||
<div className="flex justify-between gap-4">
|
||||
<dt className="text-gray-500">Paid at</dt>
|
||||
<dd className="font-semibold text-[#222] text-right">
|
||||
{new Date(paidAt).toLocaleString()}
|
||||
</dd>
|
||||
</div>
|
||||
) : null}
|
||||
</dl>
|
||||
|
||||
<div className="flex items-start gap-2 bg-[#F8F9FB] border border-[#e5e5e5] p-3 text-[12px] text-gray-500">
|
||||
<ShieldCheck className="w-4 h-4 text-primary shrink-0 mt-0.5" />
|
||||
<span>
|
||||
Remaining balance is collected after the repair is completed
|
||||
and verified. Warranty applies to the selected variant.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showHow && (
|
||||
<div className="space-y-4 pt-2 border-t border-[#e5e5e5]">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-primary font-bold text-xs uppercase tracking-wider">
|
||||
<Wrench className="w-4 h-4" />
|
||||
Next steps
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-[#222] mt-1">{nextTitle}</h3>
|
||||
{nextSubtitle ? (
|
||||
<p className="text-gray-500 text-xs mt-1">{nextSubtitle}</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div
|
||||
className={`grid grid-cols-1 gap-4 ${
|
||||
nextSteps.length >= 3
|
||||
? 'md:grid-cols-3'
|
||||
: nextSteps.length === 2
|
||||
? 'md:grid-cols-2'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
{nextSteps.map((step: any, idx: number) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="bg-[#F8F9FB] p-5 border border-[#e5e5e5] space-y-2"
|
||||
>
|
||||
<span className="w-8 h-8 bg-primary text-white font-bold text-sm flex items-center justify-center">
|
||||
{step.number || idx + 1}
|
||||
</span>
|
||||
<div className="font-bold text-[#222] text-sm">{step.title}</div>
|
||||
<div className="text-xs text-gray-500 leading-relaxed">
|
||||
{step.desc}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-3 pt-2 border-t border-[#e5e5e5]">
|
||||
<Link
|
||||
href={`/services/tracking/${encodeURIComponent(jobId || jobNo)}`}
|
||||
className="px-6 py-3 bg-primary hover:bg-primary-hover text-white font-bold text-[13px] flex items-center justify-center gap-2"
|
||||
>
|
||||
<Clock className="w-4 h-4" />
|
||||
Track repair status
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</Link>
|
||||
<Link
|
||||
href="/services"
|
||||
className="px-6 py-3 border border-[#e5e5e5] text-[#333] font-semibold text-[13px] hover:bg-[#f7f9fc] flex items-center justify-center gap-2"
|
||||
>
|
||||
Book another repair
|
||||
</Link>
|
||||
<Link
|
||||
href="/"
|
||||
className="px-6 py-3 border border-[#e5e5e5] text-[#333] font-semibold text-[13px] hover:bg-[#f7f9fc] flex items-center justify-center gap-2"
|
||||
>
|
||||
<Home className="w-4 h-4" />
|
||||
Return to storefront
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ServiceBookingSuccessPage() {
|
||||
return (
|
||||
<PageShell>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="py-20 flex flex-col items-center justify-center text-gray-400 space-y-3">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-primary" />
|
||||
<span className="text-[13px] font-semibold">
|
||||
Verifying payment
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<BookingSuccessContent />
|
||||
</Suspense>
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue