feat: add new UI components for product reviews, promotions, seller summaries, support center, and request forms
- Created ProductReviewCard component for displaying product reviews with options to edit and delete. - Implemented PromoCard component for promotional content with navigation support. - Added RoundImageLabel component for displaying images with labels in a circular format. - Developed SellerSummary component to showcase seller information and actions. - Introduced SupportCenter component for user support with FAQs and action buttons. - Created SupportRequestForm component for submitting support requests with attachments. - Added Tabs component for organizing content into tabbed sections, including product descriptions and reviews. - Implemented utility function for class name merging. - Defined TypeScript interfaces for User, Product, Order, and OrderItem. - Configured TypeScript settings in tsconfig.json for improved type checking and module resolution.
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": ["next/core-web-vitals", "next/typescript"]
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
.env
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -0,0 +1,8 @@
|
||||
⨯ Another next dev server is already running.
|
||||
|
||||
- Local: http://localhost:3000
|
||||
- PID: 5652
|
||||
- Dir: C:\Users\CTL\Desktop\RDB\RDB-BUYER
|
||||
- Log: .next\dev\logs\next-development.log
|
||||
|
||||
Run taskkill /PID 5652 /F to stop it.
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
> rdb-buyer@0.1.0 dev
|
||||
> next dev --port 3010
|
||||
|
||||
▲ Next.js 16.2.9 (Turbopack)
|
||||
- Local: http://localhost:3010
|
||||
- Network: http://192.168.1.231:3010
|
||||
✓ Ready in 7.3s
|
||||
[?25h
|
||||
@@ -0,0 +1,9 @@
|
||||
⚠ Port 3000 is in use by process 7664, using available port 3001 instead.
|
||||
⨯ Another next dev server is already running.
|
||||
|
||||
- Local: http://localhost:3000
|
||||
- PID: 7664
|
||||
- Dir: C:\Users\CTL\Desktop\RDB\RDB-BUYER
|
||||
- Log: .next\dev\logs\next-development.log
|
||||
|
||||
Run taskkill /PID 7664 /F to stop it.
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
> rdb-buyer@0.1.0 dev
|
||||
> next dev
|
||||
|
||||
▲ Next.js 16.2.9 (Turbopack)
|
||||
- Local: http://localhost:3001
|
||||
- Network: http://192.168.1.231:3001
|
||||
✓ Ready in 632ms
|
||||
[?25h
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
allowedDevOrigins: ["192.168.1.231"],
|
||||
turbopack: {
|
||||
root: "c:/Users/CTL/Desktop/RDB/RDB-BUYER",
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "rdb-buyer",
|
||||
"version": "0.1.0",
|
||||
"description": "RDB Buyer Platform",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.3.1",
|
||||
"@types/node": "^26.0.1",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-config-next": "^16.2.9",
|
||||
"postcss": "^8.5.15",
|
||||
"tailwindcss": "^4.3.1",
|
||||
"typescript": "^6.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1",
|
||||
"next": "^16.2.9",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"tailwind-merge": "^3.6.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 2.1 MiB |
@@ -0,0 +1,6 @@
|
||||
<svg width="256" height="256" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="256" height="256" rx="128" fill="#ad5a17"/>
|
||||
<path d="M76 139c0-22 17-39 39-39h22c19 0 33 13 33 30 0 16-13 28-31 28H98c-13 0-22-8-22-19Zm23 0c0 5 4 8 10 8h28c7 0 11-4 11-10 0-7-5-11-13-11h-20c-9 0-16 6-16 13Z" fill="#fff3dc"/>
|
||||
<path d="M105 127c10 0 18 8 18 18s-8 18-18 18-18-8-18-18 8-18 18-18Zm0 12a6 6 0 1 0 0 12 6 6 0 0 0 0-12ZM158 94h28l-18 29h-18l8-29Z" fill="#fff3dc"/>
|
||||
<path d="M174 151c10-1 19-7 25-16 3-4 9-5 13-2s5 9 2 13c-10 15-25 24-43 25-5 0-9-4-10-9 0-6 5-10 13-11Z" fill="#fff3dc"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 614 B |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
@@ -0,0 +1,17 @@
|
||||
<svg width="256" height="256" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<radialGradient id="bg" cx="50%" cy="45%" r="60%">
|
||||
<stop offset="0" stop-color="#176247"/>
|
||||
<stop offset="1" stop-color="#06251b"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<rect width="256" height="256" fill="url(#bg)"/>
|
||||
<g opacity=".55" fill="#0d7a50">
|
||||
<ellipse cx="56" cy="60" rx="42" ry="16" transform="rotate(-35 56 60)"/>
|
||||
<ellipse cx="190" cy="54" rx="48" ry="17" transform="rotate(28 190 54)"/>
|
||||
<ellipse cx="72" cy="178" rx="50" ry="18" transform="rotate(34 72 178)"/>
|
||||
<ellipse cx="188" cy="174" rx="44" ry="16" transform="rotate(-33 188 174)"/>
|
||||
<ellipse cx="128" cy="116" rx="64" ry="20" transform="rotate(-12 128 116)"/>
|
||||
</g>
|
||||
<text x="128" y="137" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="42" font-weight="700" fill="#ffffff">Leafy</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 920 B |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 456 KiB |
|
After Width: | Height: | Size: 712 KiB |
|
After Width: | Height: | Size: 682 KiB |
|
After Width: | Height: | Size: 437 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 452 KiB |
@@ -0,0 +1,49 @@
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<main className="min-h-screen flex items-center justify-center px-6">
|
||||
<div className="w-full max-w-sm space-y-6">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-2xl font-semibold text-gray-900 dark:text-white">Sign in</h1>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Welcome back to RDB Buyer.</p>
|
||||
</div>
|
||||
|
||||
<form className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 placeholder-gray-400 focus:border-gray-500 focus:outline-none focus:ring-1 focus:ring-gray-500 dark:border-gray-700 dark:bg-gray-900 dark:text-white"
|
||||
placeholder="you@company.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 placeholder-gray-400 focus:border-gray-500 focus:outline-none focus:ring-1 focus:ring-gray-500 dark:border-gray-700 dark:bg-gray-900 dark:text-white"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full rounded-lg bg-gray-900 px-4 py-2 text-sm font-medium text-white hover:bg-gray-700 transition-colors dark:bg-white dark:text-gray-900 dark:hover:bg-gray-100"
|
||||
>
|
||||
Sign in
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Sidebar, { MobileDrawer, MobileMenuButton } from "@/components/layout/Sidebar";
|
||||
import Topbar from "@/components/layout/Topbar";
|
||||
import SecondaryNav from "@/components/layout/SecondaryNav";
|
||||
import Breadcrumb from "@/components/layout/Breadcrumb";
|
||||
import CategoryHeader from "@/components/layout/CategoryHeader";
|
||||
|
||||
export default function DashboardLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white flex flex-col">
|
||||
{/* Topbar */}
|
||||
<div className="relative">
|
||||
<div className="lg:hidden absolute left-4 top-1/2 -translate-y-1/2 z-10">
|
||||
<MobileMenuButton onClick={() => setDrawerOpen(true)} />
|
||||
</div>
|
||||
<div className="space-y-6 sm:space-y-8">
|
||||
<div className="mb-2 sm:mb-3">
|
||||
<Topbar />
|
||||
</div>
|
||||
<SecondaryNav />
|
||||
<CategoryHeader />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile drawer */}
|
||||
<MobileDrawer open={drawerOpen} onClose={() => setDrawerOpen(false)} />
|
||||
|
||||
{/* Body */}
|
||||
<div className="flex flex-1 flex-col gap-4 sm:gap-6 sm:p-6">
|
||||
<div className="sm:-mx-6">
|
||||
<Breadcrumb items={[
|
||||
{ label: "Home", href: "/" },
|
||||
{ label: "My Profile", href: "/dashboard" },
|
||||
{ label: "My Orders" },
|
||||
]} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 gap-4 px-4 sm:gap-6 sm:px-0">
|
||||
<Sidebar />
|
||||
<main className="flex-1 min-w-0">{children}</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import PageSectionHeader from "@/components/layout/PageSectionHeader";
|
||||
import SectionHeader from "@/components/layout/SectionHeader";
|
||||
import AddressRow from "@/components/ui/AddressRow";
|
||||
import CartItemsList from "@/components/ui/CartItemsList";
|
||||
import DealBanner from "@/components/ui/DealBanner";
|
||||
import DynamicForm from "@/components/ui/DynamicForm";
|
||||
import FeatureProducts from "@/components/ui/FeatureProducts";
|
||||
import FilterPanel from "@/components/ui/FilterPanel";
|
||||
import HorizontalProductRow from "@/components/ui/HorizontalProductRow";
|
||||
import ItemDetails from "@/components/ui/ItemDetails";
|
||||
import LargePromoBanner from "@/components/ui/LargePromoBanner";
|
||||
import MessageCenter from "@/components/ui/MessageCenter";
|
||||
import OrderComplete from "@/components/ui/OrderComplete";
|
||||
import OrderInvoicePanel from "@/components/ui/OrderInvoicePanel";
|
||||
import OrderSummary from "@/components/ui/OrderSummary";
|
||||
import OrderSuccessReceipt from "@/components/ui/OrderSuccessReceipt";
|
||||
import PaymentMethodRow from "@/components/ui/PaymentMethodRow";
|
||||
import PromoCard from "@/components/ui/PromoCard";
|
||||
import ProductCardWithCartModal from "@/components/ui/ProductCardWithCartModal";
|
||||
import ProductDetailShowcase from "@/components/ui/ProductDetailShowcase";
|
||||
import ProductReviewCard from "@/components/ui/ProductReviewCard";
|
||||
import RoundImageLabel from "@/components/ui/RoundImageLabel";
|
||||
import SellerSummary from "@/components/ui/SellerSummary";
|
||||
import SupportCenter from "@/components/ui/SupportCenter";
|
||||
import SupportRequestForm from "@/components/ui/SupportRequestForm";
|
||||
import Tabs from "@/components/ui/Tabs";
|
||||
|
||||
export default function DashboardPage() {
|
||||
return (
|
||||
<div className="w-full space-y-6">
|
||||
<PageSectionHeader title="My Orders" actionLabel="Clear Order History" />
|
||||
<div className="flex flex-col gap-8 lg:flex-row lg:items-start">
|
||||
<FilterPanel className="lg:sticky lg:top-6 lg:shrink-0" />
|
||||
|
||||
<div className="min-w-0 flex-1 space-y-6">
|
||||
<SupportCenter />
|
||||
<SupportRequestForm />
|
||||
<ProductReviewCard />
|
||||
<DynamicForm />
|
||||
<AddressRow />
|
||||
<PaymentMethodRow />
|
||||
<CartItemsList />
|
||||
<HorizontalProductRow />
|
||||
<MessageCenter />
|
||||
<ItemDetails />
|
||||
<OrderSummary />
|
||||
<OrderSuccessReceipt />
|
||||
<OrderComplete />
|
||||
<OrderInvoicePanel />
|
||||
<SellerSummary />
|
||||
<LargePromoBanner showNavigation />
|
||||
<SectionHeader
|
||||
title="Best Deals"
|
||||
details="Everyday essentials, made with purpose."
|
||||
viewAllHref="/dashboard"
|
||||
/>
|
||||
<PromoCard
|
||||
eyebrow="Special Marketplace Pricing"
|
||||
title="Thoughtfully priced products, available for a limited time."
|
||||
showArrowButton
|
||||
/>
|
||||
<ProductCardWithCartModal
|
||||
discountBadge="-40%"
|
||||
showWishlist
|
||||
name="Artisan Sauce Collection"
|
||||
rating={4.9}
|
||||
soldCount="128 Sold"
|
||||
price="LKR 5,000.00"
|
||||
oldPrice="LKR 5,500.00"
|
||||
showAddToCart
|
||||
/>
|
||||
<DealBanner
|
||||
title="There's a deal for you, too"
|
||||
description="Don't miss a chance to save on item's you've been looking for."
|
||||
showIcon
|
||||
/>
|
||||
<RoundImageLabel name="Leafy" />
|
||||
<ProductDetailShowcase />
|
||||
<Tabs defaultValue="reviews" />
|
||||
<FeatureProducts />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #111111;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import Footer from "@/components/layout/Footer";
|
||||
import CopyrightBar from "@/components/layout/CopyrightBar";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "RDB Buyer",
|
||||
description: "Smart buying, simplified.",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
|
||||
{children}
|
||||
<Footer />
|
||||
<CopyrightBar />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import Topbar from "@/components/layout/Topbar";
|
||||
import SecondaryNav from "@/components/layout/SecondaryNav";
|
||||
import CategoryHeader from "@/components/layout/CategoryHeader";
|
||||
import SectionHeader from "@/components/layout/SectionHeader";
|
||||
import PromoCard from "@/components/ui/PromoCard";
|
||||
import ProductCardWithCartModal from "@/components/ui/ProductCardWithCartModal";
|
||||
import DealBanner from "@/components/ui/DealBanner";
|
||||
import RoundImageLabel from "@/components/ui/RoundImageLabel";
|
||||
|
||||
const productImages = [
|
||||
"/images/artisan-sauce-collection.png",
|
||||
"/images/tote-gallery-1.png",
|
||||
"/images/tote-gallery-2.png",
|
||||
"/images/tote-gallery-3.png",
|
||||
"/images/tote-gallery-4.png",
|
||||
"/images/deal-banner-product.png",
|
||||
"/images/promo-beauty-card.png",
|
||||
"/images/feature-fashion-side.png",
|
||||
"/images/large-sale-banner.png",
|
||||
];
|
||||
|
||||
const heroPromos = [
|
||||
{
|
||||
eyebrow: "Special Marketplace Pricing",
|
||||
title: "Thoughtfully priced products, available for a limited time.",
|
||||
imageSrc: "/images/promo-beauty-card.png",
|
||||
},
|
||||
{
|
||||
eyebrow: "Handcrafted By Local Artisans",
|
||||
title: "Limited collections from women artisans.",
|
||||
imageSrc: "/images/feature-fashion-side.png",
|
||||
},
|
||||
{
|
||||
eyebrow: "Wellness Essentials",
|
||||
title: "Designed to support everyday energy and wellness.",
|
||||
imageSrc: "/images/deal-banner-product.png",
|
||||
},
|
||||
{
|
||||
eyebrow: "Fresh Finds",
|
||||
title: "Curated daily picks from trusted local sellers.",
|
||||
imageSrc: "/images/large-sale-banner.png",
|
||||
},
|
||||
{
|
||||
eyebrow: "Seasonal Picks",
|
||||
title: "New arrivals made for gifting and everyday care.",
|
||||
imageSrc: "/images/tote-gallery-2.png",
|
||||
},
|
||||
];
|
||||
|
||||
const bestDeals = [
|
||||
{ name: "Heritage Mug", rating: 4.8, soldCount: "210 Sold", price: "LKR 1,200.00", oldPrice: "LKR 1,600.00", discountBadge: "-25%" },
|
||||
{ name: "Ceramic Cake", rating: 4.6, soldCount: "98 Sold", price: "LKR 1,800.00" },
|
||||
{ name: "Handcrafted Bracelet", rating: 4.9, soldCount: "340 Sold", price: "LKR 2,400.00", oldPrice: "LKR 3,000.00", discountBadge: "-20%" },
|
||||
{ name: "Dried Beauty Mix", rating: 4.7, soldCount: "76 Sold", price: "LKR 3,950.00" },
|
||||
{ name: "Heritage Candle Trio", rating: 4.5, soldCount: "152 Sold", price: "LKR 1,300.00" },
|
||||
{ name: "Tamarind Chili Snack", rating: 4.8, soldCount: "412 Sold", price: "LKR 950.00", oldPrice: "LKR 1,200.00", discountBadge: "-15%" },
|
||||
{ name: "Forest Coffee Beans", rating: 4.9, soldCount: "289 Sold", price: "LKR 2,650.00" },
|
||||
{ name: "Artisan Sauce Collection", rating: 4.6, soldCount: "64 Sold", price: "LKR 5,000.00", oldPrice: "LKR 5,500.00" },
|
||||
{ name: "Ceylon Tea Selection", rating: 4.7, soldCount: "178 Sold", price: "LKR 1,990.00" },
|
||||
{ name: "Handwoven Cotton Tote", rating: 4.8, soldCount: "133 Sold", price: "LKR 3,300.00", oldPrice: "LKR 3,800.00", discountBadge: "-13%" },
|
||||
];
|
||||
|
||||
const featuredProducts = [
|
||||
{ name: "Ceylon Tea Gift Box", rating: 4.8, soldCount: "245 Sold", price: "LKR 1,690.00" },
|
||||
{ name: "Almond Bar Soap", rating: 4.6, soldCount: "87 Sold", price: "LKR 850.00" },
|
||||
{ name: "Clay Pot Set", rating: 4.7, soldCount: "119 Sold", price: "LKR 2,200.00", oldPrice: "LKR 2,600.00", discountBadge: "-15%" },
|
||||
{ name: "Kids Pouch", rating: 4.9, soldCount: "302 Sold", price: "LKR 1,750.00" },
|
||||
{ name: "Gift Wrap Pack", rating: 4.5, soldCount: "58 Sold", price: "LKR 3,100.00" },
|
||||
{ name: "Cotton Tote Bag", rating: 4.8, soldCount: "201 Sold", price: "LKR 1,450.00" },
|
||||
{ name: "Coconut Bracelet Trio", rating: 4.7, soldCount: "144 Sold", price: "LKR 990.00", oldPrice: "LKR 1,250.00", discountBadge: "-21%" },
|
||||
{ name: "Palm Leaf Basket", rating: 4.6, soldCount: "93 Sold", price: "LKR 2,800.00" },
|
||||
{ name: "Woven Tea Holder", rating: 4.9, soldCount: "267 Sold", price: "LKR 1,150.00" },
|
||||
{ name: "Traditional Support Cloth", rating: 4.7, soldCount: "118 Sold", price: "LKR 1,990.00" },
|
||||
];
|
||||
|
||||
const featuredStores = [
|
||||
{ name: "Leafy", imageSrc: "/images/leafy-round.svg" },
|
||||
{ name: "Cottage", imageSrc: "/images/cozy-round.svg" },
|
||||
{ name: "Coconut", imageSrc: "/images/leafy-round.svg" },
|
||||
{ name: "Cosy", imageSrc: "/images/cozy-round.svg" },
|
||||
{ name: "Home", imageSrc: "/images/leafy-round.svg" },
|
||||
{ name: "Potter Craft", imageSrc: "/images/cozy-round.svg" },
|
||||
{ name: "Bansiri", imageSrc: "/images/leafy-round.svg" },
|
||||
{ name: "Tiki", imageSrc: "/images/cozy-round.svg" },
|
||||
];
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="min-h-screen bg-white text-[#151933]">
|
||||
<div className="min-h-screen w-full overflow-hidden bg-white shadow-[0_0_0_1px_rgba(15,23,42,0.04)]">
|
||||
<Topbar />
|
||||
|
||||
<div className="space-y-8 py-8">
|
||||
<SecondaryNav />
|
||||
<CategoryHeader />
|
||||
</div>
|
||||
|
||||
<main className="space-y-9 px-4 pb-12 pt-4 sm:px-6 lg:px-16">
|
||||
<section className="mx-auto grid w-full max-w-[1680px] grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-5">
|
||||
{heroPromos.map((promo) => (
|
||||
<PromoCard
|
||||
key={promo.title}
|
||||
{...promo}
|
||||
showArrowButton
|
||||
className="max-w-none aspect-[0.78/1] rounded-lg [&_h2]:text-lg [&_p]:text-[11px] sm:[&_h2]:text-xl"
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<section className="-mx-4 space-y-4 bg-[#f3f3f4] px-4 py-8 sm:-mx-6 sm:px-6 lg:-mx-16 lg:px-16">
|
||||
<SectionHeader
|
||||
title="Best Deals"
|
||||
details="Everyday essentials, made with purpose."
|
||||
viewAllHref="/dashboard"
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-8">
|
||||
{bestDeals.map((product, index) => (
|
||||
<ProductCardWithCartModal
|
||||
key={product.name}
|
||||
{...product}
|
||||
imageSrc={productImages[index % productImages.length]}
|
||||
showWishlist
|
||||
showAddToCart
|
||||
size="compact"
|
||||
className="mx-auto"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<DealBanner
|
||||
title="There's a deal for you, too"
|
||||
description="Don't miss a chance to save on items you've been looking for."
|
||||
showIcon
|
||||
className="min-h-[124px] rounded-lg sm:min-h-[148px]"
|
||||
/>
|
||||
|
||||
<section className="-mx-4 space-y-4 bg-[#f3f3f4] px-4 py-8 sm:-mx-6 sm:px-6 lg:-mx-16 lg:px-16">
|
||||
<SectionHeader
|
||||
title="Featured Stores"
|
||||
details="Selected for quality, creativity, and care."
|
||||
viewAllHref="/dashboard"
|
||||
/>
|
||||
<div className="grid grid-cols-4 gap-4 sm:grid-cols-6 lg:grid-cols-8">
|
||||
{featuredStores.map((store) => (
|
||||
<RoundImageLabel
|
||||
key={store.name}
|
||||
{...store}
|
||||
size="sm"
|
||||
className="mx-auto"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
<SectionHeader
|
||||
title="Featured Products"
|
||||
details="Chosen for quality, design, and purpose."
|
||||
viewAllHref="/dashboard"
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-8">
|
||||
{featuredProducts.map((product, index) => (
|
||||
<ProductCardWithCartModal
|
||||
key={product.name}
|
||||
{...product}
|
||||
imageSrc={productImages[(index + 3) % productImages.length]}
|
||||
showWishlist
|
||||
showAddToCart
|
||||
size="compact"
|
||||
className="mx-auto"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
|
||||
interface BreadcrumbItem {
|
||||
label: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
interface BreadcrumbProps {
|
||||
items: BreadcrumbItem[];
|
||||
}
|
||||
|
||||
export default function Breadcrumb({ items }: BreadcrumbProps) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ backgroundColor: "#eceaea" }}
|
||||
className="w-full h-11 flex items-center gap-3 overflow-x-auto px-4 sm:px-5"
|
||||
>
|
||||
{/* Back button */}
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="flex h-7 w-7 items-center justify-center rounded-full hover:bg-gray-200 transition-colors"
|
||||
aria-label="Go back"
|
||||
>
|
||||
<svg className="w-4 h-4 text-gray-700" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Crumbs */}
|
||||
<nav className="flex min-w-0 items-center gap-2 whitespace-nowrap">
|
||||
{items.map((item, index) => {
|
||||
const isLast = index === items.length - 1;
|
||||
return (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
{index > 0 && (
|
||||
<span className="text-gray-400 text-[10px]">•</span>
|
||||
)}
|
||||
{isLast ? (
|
||||
<span className="text-sm font-semibold text-gray-800">
|
||||
{item.label}
|
||||
</span>
|
||||
) : (
|
||||
<Link
|
||||
href={item.href ?? "#"}
|
||||
className="text-sm text-gray-400 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import Button from "@/components/ui/Button";
|
||||
|
||||
const categories = [
|
||||
{ label: "Beauty & Health", accent: "#F8B84E", icon: "bottle" },
|
||||
{ label: "Furniture", accent: "#7A5841", icon: "chair" },
|
||||
{ label: "Electronics", accent: "#4356C5", icon: "device" },
|
||||
{ label: "Mother & Kids", accent: "#F08FA7", icon: "kids" },
|
||||
{ label: "Computer & Office", accent: "#0B66B3", icon: "laptop" },
|
||||
{ label: "Foods & Drinks", accent: "#F1B92B", icon: "drink" },
|
||||
{ label: "Phones & Accs", accent: "#2762A8", icon: "phone" },
|
||||
];
|
||||
|
||||
function CategoryIcon({ accent, icon }: { accent: string; icon: string }) {
|
||||
return (
|
||||
<span
|
||||
className="relative flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-white shadow-sm"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span
|
||||
className="absolute inset-1 rounded-full opacity-15"
|
||||
style={{ backgroundColor: accent }}
|
||||
/>
|
||||
{icon === "bottle" && (
|
||||
<svg className="relative h-5 w-5" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M9 2h6v4l-1.5 2v13h-3V8L9 6V2Z" fill={accent} />
|
||||
<path d="M8 11h8v8a3 3 0 0 1-3 3h-2a3 3 0 0 1-3-3v-8Z" fill="#F6D78B" />
|
||||
</svg>
|
||||
)}
|
||||
{icon === "chair" && (
|
||||
<svg className="relative h-5 w-5" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M6 4h9a3 3 0 0 1 3 3v7H7a3 3 0 0 1-3-3V6a2 2 0 0 1 2-2Z" fill="#8B6A52" />
|
||||
<path d="M7 14v6M17 14v6M7 17h10" stroke={accent} strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
)}
|
||||
{icon === "device" && (
|
||||
<svg className="relative h-5 w-5" viewBox="0 0 24 24" fill="none">
|
||||
<rect x="4" y="5" width="7" height="14" rx="1.5" fill={accent} />
|
||||
<rect x="13" y="4" width="7" height="16" rx="1.5" fill="#27318A" />
|
||||
<path d="M6.5 16h2M15.5 17h2" stroke="white" strokeWidth="1.4" strokeLinecap="round" />
|
||||
</svg>
|
||||
)}
|
||||
{icon === "kids" && (
|
||||
<svg className="relative h-5 w-5" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M7 7a3 3 0 1 1 6 0v9H4V7h3Z" fill="#F8B2BF" />
|
||||
<path d="M14 9a3 3 0 1 1 6 0v7h-8V9h2Z" fill={accent} />
|
||||
<path d="M5 18h14" stroke="#E06B84" strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
)}
|
||||
{icon === "laptop" && (
|
||||
<svg className="relative h-5 w-5" viewBox="0 0 24 24" fill="none">
|
||||
<rect x="5" y="5" width="14" height="10" rx="1.5" fill={accent} />
|
||||
<path d="M3 18h18l-2-3H5l-2 3Z" fill="#1F2937" />
|
||||
<path d="M8 8h8" stroke="#BDEBFF" strokeWidth="1.4" strokeLinecap="round" />
|
||||
</svg>
|
||||
)}
|
||||
{icon === "drink" && (
|
||||
<svg className="relative h-5 w-5" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M7 3h10l-1 18H8L7 3Z" fill="#F8D463" />
|
||||
<path d="M8 8h8" stroke="#EF6F2E" strokeWidth="2" strokeLinecap="round" />
|
||||
<path d="M12 3 17 1" stroke={accent} strokeWidth="1.6" strokeLinecap="round" />
|
||||
</svg>
|
||||
)}
|
||||
{icon === "phone" && (
|
||||
<svg className="relative h-5 w-5" viewBox="0 0 24 24" fill="none">
|
||||
<rect x="7" y="2" width="10" height="20" rx="2" fill={accent} />
|
||||
<path d="M10 5h4M11 19h2" stroke="#D8EEFF" strokeWidth="1.4" strokeLinecap="round" />
|
||||
</svg>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CategoryHeader() {
|
||||
return (
|
||||
<section className="w-full bg-[#f3f3f4] px-4 py-3 sm:px-6 lg:px-16">
|
||||
<div className="flex items-center gap-5 overflow-x-auto">
|
||||
<div className="flex min-w-max items-center gap-5">
|
||||
{categories.map((category) => (
|
||||
<Button
|
||||
key={category.label}
|
||||
variant="pill"
|
||||
className="h-11 border-0 bg-white px-4 text-xs font-medium text-gray-700 shadow-sm hover:bg-white sm:text-sm"
|
||||
icon={<CategoryIcon accent={category.accent} icon={category.icon} />}
|
||||
>
|
||||
{category.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="icon"
|
||||
aria-label="View more categories"
|
||||
className="ml-auto h-11 w-11 shrink-0 border-0 bg-white text-gray-800 shadow-sm hover:bg-white"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="m9 5 7 7-7 7" />
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export default function CopyrightBar() {
|
||||
return (
|
||||
<div className="w-full bg-[#fbfbfc] px-4 py-5 text-center">
|
||||
<p className="text-xs text-gray-500 sm:text-sm">
|
||||
Copyright © 2026 Diriya RDB Bank . All rights reserved
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
const footerSections = [
|
||||
{
|
||||
title: "Company Info",
|
||||
links: ["About Us", "Open Store", "Help & Support", "Blog"],
|
||||
},
|
||||
{
|
||||
title: "Legal",
|
||||
links: ["About Us", "Carrier", "We are hiring", "Blog"],
|
||||
},
|
||||
{
|
||||
title: "Features",
|
||||
links: ["Business Marketing", "User Analytic", "Live Chat", "Unlimited Support"],
|
||||
},
|
||||
{
|
||||
title: "Resources",
|
||||
links: ["IOS & Android", "Watch a Demo", "Customers", "API"],
|
||||
},
|
||||
];
|
||||
|
||||
function FacebookIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" className="h-5 w-5" aria-hidden="true">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M22 12.06C22 6.49 17.52 2 12 2S2 6.49 2 12.06c0 5.02 3.66 9.18 8.44 9.94v-7.03H7.9v-2.91h2.54V9.84c0-2.52 1.49-3.91 3.77-3.91 1.09 0 2.23.2 2.23.2v2.47h-1.26c-1.24 0-1.63.78-1.63 1.57v1.89h2.78l-.44 2.91h-2.34V22c4.78-.76 8.44-4.92 8.44-9.94Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function InstagramIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" className="h-5 w-5" aria-hidden="true">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M7.75 2h8.5A5.76 5.76 0 0 1 22 7.75v8.5A5.76 5.76 0 0 1 16.25 22h-8.5A5.76 5.76 0 0 1 2 16.25v-8.5A5.76 5.76 0 0 1 7.75 2Zm0 2A3.75 3.75 0 0 0 4 7.75v8.5A3.75 3.75 0 0 0 7.75 20h8.5A3.75 3.75 0 0 0 20 16.25v-8.5A3.75 3.75 0 0 0 16.25 4h-8.5Zm8.75 1.5a1.25 1.25 0 1 1 0 2.5 1.25 1.25 0 0 1 0-2.5ZM12 7a5 5 0 1 1 0 10 5 5 0 0 1 0-10Zm0 2a3 3 0 1 0 0 6 3 3 0 0 0 0-6Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function TwitterIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" className="h-5 w-5" aria-hidden="true">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M22 5.92c-.73.32-1.51.54-2.33.64a4.08 4.08 0 0 0 1.79-2.25 8.2 8.2 0 0 1-2.58.99A4.06 4.06 0 0 0 11.96 9c0 .32.04.64.11.94A11.52 11.52 0 0 1 3.7 5.69a4.05 4.05 0 0 0 1.26 5.42 4 4 0 0 1-1.84-.51v.05a4.06 4.06 0 0 0 3.26 3.98c-.34.09-.7.14-1.07.14-.26 0-.52-.03-.77-.08a4.07 4.07 0 0 0 3.79 2.82A8.14 8.14 0 0 1 2 19.29 11.48 11.48 0 0 0 8.23 21c7.48 0 11.57-6.2 11.57-11.58v-.53A8.28 8.28 0 0 0 22 5.92Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="bg-[#f7f7f8] px-4 py-8 text-[#1f2937] sm:px-6 lg:px-8">
|
||||
<div className="mx-auto grid max-w-[1160px] grid-cols-1 gap-8 min-[420px]:grid-cols-2 lg:grid-cols-[1.4fr_repeat(5,1fr)]">
|
||||
<div>
|
||||
<Image
|
||||
src="/images/Diriya-logo.png"
|
||||
alt="Diriya"
|
||||
width={120}
|
||||
height={48}
|
||||
className="h-auto w-[112px] sm:w-[120px]"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
{footerSections.map((section) => (
|
||||
<div key={section.title} className="space-y-3">
|
||||
<h2 className="text-sm font-semibold text-[#111827]">{section.title}</h2>
|
||||
<ul className="space-y-2">
|
||||
{section.links.map((link) => (
|
||||
<li key={link}>
|
||||
<Link href="#" className="text-xs text-gray-500 transition-colors hover:text-[#00AFC1]">
|
||||
{link}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="space-y-3">
|
||||
<h2 className="text-sm font-semibold text-[#111827]">Get In Touch</h2>
|
||||
<div className="flex items-center gap-3 text-[#00AFC1]">
|
||||
<Link href="#" aria-label="Facebook" className="transition-opacity hover:opacity-75">
|
||||
<FacebookIcon />
|
||||
</Link>
|
||||
<Link href="#" aria-label="Instagram" className="transition-opacity hover:opacity-75">
|
||||
<InstagramIcon />
|
||||
</Link>
|
||||
<Link href="#" aria-label="Twitter" className="transition-opacity hover:opacity-75">
|
||||
<TwitterIcon />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="space-y-2 text-xs text-gray-500">
|
||||
<p>(+94) 71555-0129</p>
|
||||
<p>info@diriyardb.com</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
interface PageSectionHeaderProps {
|
||||
title: string;
|
||||
actionLabel?: string;
|
||||
onAction?: () => void;
|
||||
}
|
||||
|
||||
export default function PageSectionHeader({
|
||||
title,
|
||||
actionLabel,
|
||||
onAction,
|
||||
}: PageSectionHeaderProps) {
|
||||
return (
|
||||
<div className="flex w-full items-center justify-between gap-4">
|
||||
<h1 className="text-base font-medium text-gray-900 sm:text-lg">{title}</h1>
|
||||
|
||||
{actionLabel && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onAction}
|
||||
className="shrink-0 text-xs font-medium text-[#00AFC1] transition-colors hover:text-[#008fa0] sm:text-sm"
|
||||
>
|
||||
{actionLabel}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import Button from "@/components/ui/Button";
|
||||
|
||||
export default function SecondaryNav() {
|
||||
return (
|
||||
<div
|
||||
style={{ backgroundColor: "#eceaea" }}
|
||||
className="mx-0 flex h-auto min-h-14 items-center gap-2 overflow-x-auto px-4 py-2 sm:mx-6 sm:rounded-xl sm:px-5 lg:mx-16 lg:px-8"
|
||||
>
|
||||
|
||||
{/* Left nav buttons */}
|
||||
<div className="flex min-w-max items-center gap-2">
|
||||
<Button variant="pill" icon={
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
}>
|
||||
All Categories
|
||||
</Button>
|
||||
|
||||
<Button variant="pill" icon={
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 2C8.134 2 5 5.134 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.866-3.134-7-7-7zm0 9.5a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5z" />
|
||||
</svg>
|
||||
}>
|
||||
Track Order
|
||||
</Button>
|
||||
|
||||
<Button variant="pill" icon={
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3 5a2 2 0 0 1 2-2h3.28a1 1 0 0 1 .95.68l1.1 3.3a1 1 0 0 1-.23 1.02L8.5 9.5a11.05 11.05 0 0 0 6 6l1.5-1.62a1 1 0 0 1 1.02-.23l3.3 1.1a1 1 0 0 1 .68.95V19a2 2 0 0 1-2 2A17 17 0 0 1 3 5z" />
|
||||
</svg>
|
||||
}>
|
||||
Customer Support
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Right side */}
|
||||
<div className="ml-auto flex min-w-max items-center gap-2">
|
||||
<Button variant="teal">Become a Seller</Button>
|
||||
|
||||
<Button variant="icon" aria-label="Wishlist">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
|
||||
</svg>
|
||||
</Button>
|
||||
|
||||
<Button variant="icon" aria-label="Cart">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4zM3 6h18M16 10a4 4 0 0 1-8 0" />
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import Link from "next/link";
|
||||
|
||||
interface SectionHeaderProps {
|
||||
title?: string;
|
||||
details?: string;
|
||||
viewAllHref?: string;
|
||||
viewAllLabel?: string;
|
||||
}
|
||||
|
||||
export default function SectionHeader({
|
||||
title,
|
||||
details,
|
||||
viewAllHref,
|
||||
viewAllLabel = "View all",
|
||||
}: SectionHeaderProps) {
|
||||
const showAction = Boolean(viewAllHref);
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center justify-between gap-4">
|
||||
{(title || details) && (
|
||||
<div className="min-w-0">
|
||||
<h2 className="text-lg font-medium leading-snug text-[#1f2440] sm:text-2xl">
|
||||
{title && <span>{title}</span>}
|
||||
{title && details && <span className="text-gray-500">. </span>}
|
||||
{details && <span className="font-normal text-gray-500">{details}</span>}
|
||||
</h2>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showAction && (
|
||||
<Link
|
||||
href={viewAllHref!}
|
||||
className="ml-auto inline-flex shrink-0 items-center gap-3 text-xs font-medium text-[#00AFC1] transition-colors hover:text-[#008fa0] sm:text-sm"
|
||||
>
|
||||
{viewAllLabel}
|
||||
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="m9 5 7 7-7 7" />
|
||||
</svg>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const nav = [
|
||||
{ label: "Overview", href: "/dashboard" },
|
||||
{ label: "My Orders", href: "/dashboard/orders" },
|
||||
{ label: "Payments", href: "/dashboard/payments" },
|
||||
{ label: "Shipping Address", href: "/dashboard/shipping" },
|
||||
{ label: "Settings", href: "/dashboard/settings" },
|
||||
{ label: "Support Center", href: "/dashboard/support" },
|
||||
{ label: "My Reviews", href: "/dashboard/reviews" },
|
||||
];
|
||||
|
||||
function NavLinks({ onClose }: { onClose?: () => void }) {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<nav className="flex flex-col py-4">
|
||||
{nav.map((item) => {
|
||||
const active = pathname === item.href;
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
onClick={onClose}
|
||||
className={cn(
|
||||
"relative flex items-center px-8 py-5 text-[15px] transition-colors",
|
||||
active
|
||||
? "text-[#00BCD4] font-medium"
|
||||
: "text-gray-800 hover:text-[#00BCD4]"
|
||||
)}
|
||||
>
|
||||
{active && (
|
||||
<span className="absolute left-0 top-0 h-full w-[3.5px] rounded-r-full bg-[#00BCD4]" />
|
||||
)}
|
||||
{item.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
export function MobileMenuButton({ onClick }: { onClick: () => void }) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="lg:hidden p-2 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
aria-label="Open menu"
|
||||
>
|
||||
<svg className="w-5 h-5 text-gray-700" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function MobileDrawer({ open, onClose }: { open: boolean; onClose: () => void }) {
|
||||
return (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
onClick={onClose}
|
||||
className={cn(
|
||||
"fixed inset-0 z-40 bg-black/30 transition-opacity lg:hidden",
|
||||
open ? "opacity-100 pointer-events-auto" : "opacity-0 pointer-events-none"
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Drawer */}
|
||||
<div
|
||||
className={cn(
|
||||
"fixed top-0 left-0 z-50 h-full w-64 bg-white rounded-r-2xl shadow-xl transition-transform duration-300 lg:hidden",
|
||||
open ? "translate-x-0" : "-translate-x-full"
|
||||
)}
|
||||
>
|
||||
{/* Close */}
|
||||
<div className="flex items-center justify-between px-6 py-5 border-b border-gray-100">
|
||||
<span className="text-base font-semibold text-gray-900">Menu</span>
|
||||
<button onClick={onClose} className="p-1 rounded-md hover:bg-gray-100 transition-colors">
|
||||
<svg className="w-4 h-4 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<NavLinks onClose={onClose} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Sidebar() {
|
||||
return (
|
||||
<aside className="hidden lg:flex w-60 shrink-0 flex-col bg-white rounded-2xl self-start sticky top-6">
|
||||
<NavLinks />
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Image from "next/image";
|
||||
import DropdownButton from "@/components/ui/DropdownButton";
|
||||
|
||||
const languages = ["English", "Sinhala", "Tamil"];
|
||||
const currencies = ["LKR", "USD", "EUR", "GBP", "AED"];
|
||||
|
||||
const GlobeIcon = (
|
||||
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8} className="w-4 h-4">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.6 9h16.8M3.6 15h16.8M12 3c-2.4 2.6-3.8 5.7-3.8 9s1.4 6.4 3.8 9M12 3c2.4 2.6 3.8 5.7 3.8 9s-1.4 6.4-3.8 9" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const CurrencyIcon = (
|
||||
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8} className="w-4 h-4">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 7H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2v-3M9 7h6V3H9v4zM9 7l6 6" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default function Topbar() {
|
||||
const [search, setSearch] = useState("");
|
||||
const [language, setLanguage] = useState("English");
|
||||
const [currency, setCurrency] = useState("LKR");
|
||||
|
||||
return (
|
||||
<header className="w-full bg-white border-b border-gray-100 px-4 pl-14 py-3 sm:px-6 lg:px-20 lg:py-0 lg:h-16 flex flex-wrap items-center gap-3 sm:gap-4 lg:gap-6 overflow-visible margin-bottom-20">
|
||||
{/* Logo */}
|
||||
<div className="shrink-0">
|
||||
<Image src="/images/Diriya-logo.png" alt="Diriya" width={120} height={36} className="w-[96px] sm:w-[112px] lg:w-[120px] h-auto" priority />
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="order-3 w-full lg:order-none lg:flex-1 lg:max-w-xl lg:mx-auto">
|
||||
<div className="flex items-center gap-2 bg-gray-100 rounded-full px-4 py-2.5">
|
||||
<svg className="w-4 h-4 text-gray-400 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-4.35-4.35M17 11A6 6 0 1 1 5 11a6 6 0 0 1 12 0z" />
|
||||
</svg>
|
||||
<div className="w-px h-4 bg-gray-300" />
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Search by products or stores"
|
||||
className="flex-1 bg-transparent text-sm text-gray-700 placeholder-gray-400 outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right controls */}
|
||||
<div className="ml-auto flex shrink-0 items-center gap-2 sm:gap-3">
|
||||
<DropdownButton
|
||||
options={languages}
|
||||
selected={language}
|
||||
onSelect={setLanguage}
|
||||
icon={GlobeIcon}
|
||||
/>
|
||||
|
||||
<DropdownButton
|
||||
options={currencies}
|
||||
selected={currency}
|
||||
onSelect={setCurrency}
|
||||
icon={CurrencyIcon}
|
||||
/>
|
||||
|
||||
{/* Avatar */}
|
||||
<div className="w-9 h-9 rounded-full bg-gray-200 flex items-center justify-center text-[12px] font-semibold text-gray-500 tracking-wide cursor-pointer hover:bg-gray-300 transition-colors">
|
||||
AN
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
|
||||
import Button from "@/components/ui/Button";
|
||||
import ProductCard, { type ProductCardProps } from "@/components/ui/ProductCard";
|
||||
|
||||
interface CartProduct {
|
||||
imageSrc?: string;
|
||||
imageAlt?: string;
|
||||
name?: string;
|
||||
size?: string;
|
||||
color?: string;
|
||||
price?: string;
|
||||
oldPrice?: string;
|
||||
discountBadge?: string;
|
||||
sellerName?: string;
|
||||
}
|
||||
|
||||
interface AddToCartModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
product?: CartProduct;
|
||||
recommendations?: ProductCardProps[];
|
||||
}
|
||||
|
||||
const defaultProduct: Required<CartProduct> = {
|
||||
imageSrc: "/images/tote-gallery-1.png",
|
||||
imageAlt: "Calla tote bag",
|
||||
name: "Calla Tote Bag - Espresso Brown",
|
||||
size: "Small - 39.5",
|
||||
color: "Espresso Brown",
|
||||
price: "LKR 5,000.00",
|
||||
oldPrice: "LKR 5,600.00",
|
||||
discountBadge: "-40%",
|
||||
sellerName: "Cozy",
|
||||
};
|
||||
|
||||
const defaultRecommendations: ProductCardProps[] = [
|
||||
{
|
||||
imageSrc: "/images/tote-gallery-4.png",
|
||||
imageAlt: "Black tote bag",
|
||||
discountBadge: "-42%",
|
||||
showWishlist: true,
|
||||
name: "Calla Tote Bag - Espresso Brown",
|
||||
rating: 4.3,
|
||||
soldCount: "128 Sold",
|
||||
price: "LKR 5,000.00",
|
||||
oldPrice: "LKR 5,600.00",
|
||||
showAddToCart: true,
|
||||
},
|
||||
{
|
||||
imageSrc: "/images/tote-gallery-3.png",
|
||||
imageAlt: "Brown heels",
|
||||
showWishlist: true,
|
||||
name: "Calla Tote Bag - Espresso Brown",
|
||||
rating: 4.9,
|
||||
soldCount: "128 Sold",
|
||||
price: "LKR 5,000.00",
|
||||
oldPrice: "LKR 5,600.00",
|
||||
showAddToCart: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default function AddToCartModal({
|
||||
open,
|
||||
onClose,
|
||||
product,
|
||||
recommendations = defaultRecommendations,
|
||||
}: AddToCartModalProps) {
|
||||
if (!open) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const productDetails = { ...defaultProduct, ...product };
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[10000] flex items-center justify-center bg-black/35 px-4 py-6">
|
||||
<div className="relative max-h-[92vh] w-full max-w-[430px] overflow-y-auto rounded-lg bg-white p-6 shadow-2xl">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="absolute right-5 top-5 text-gray-400 transition-colors hover:text-gray-700"
|
||||
aria-label="Close cart modal"
|
||||
>
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<h2 className="text-center text-base font-semibold text-[#1f2440]">
|
||||
Successfully added to cart!
|
||||
</h2>
|
||||
|
||||
<div className="mt-6 flex gap-5">
|
||||
<div className="relative h-32 w-28 shrink-0 overflow-hidden rounded bg-gray-100">
|
||||
<Image
|
||||
src={productDetails.imageSrc}
|
||||
alt={productDetails.imageAlt || productDetails.name}
|
||||
fill
|
||||
sizes="112px"
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1 space-y-2">
|
||||
<h3 className="text-sm font-semibold text-gray-900">{productDetails.name}</h3>
|
||||
<p className="text-xs text-gray-500">
|
||||
Size: {productDetails.size} · Color: {productDetails.color}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-lg font-semibold text-gray-900">{productDetails.price}</span>
|
||||
<span className="text-sm text-red-500 line-through">{productDetails.oldPrice}</span>
|
||||
</div>
|
||||
<span className="inline-flex rounded bg-red-500 px-2 py-1 text-[10px] font-semibold text-white">
|
||||
{productDetails.discountBadge}
|
||||
</span>
|
||||
|
||||
<div className="mt-3 flex items-center gap-3 rounded-md bg-gray-50 px-3 py-2">
|
||||
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-[#b76222] text-[10px] font-semibold text-white">
|
||||
coz
|
||||
</span>
|
||||
<span className="text-xs font-medium text-gray-700">{productDetails.sellerName}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-7 grid grid-cols-2 gap-3">
|
||||
<Button variant="ghost" className="h-11 rounded-full bg-gray-100 text-xs font-semibold text-gray-700 hover:bg-gray-200">
|
||||
Shop More
|
||||
</Button>
|
||||
<Button variant="teal" className="h-11 rounded-full text-xs font-semibold">
|
||||
View cart & checkout
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{recommendations.length > 0 && (
|
||||
<div className="mt-8">
|
||||
<p className="mb-4 text-center text-xs text-gray-700">You may also like this.</p>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{recommendations.map((item, index) => (
|
||||
<ProductCard
|
||||
key={`${item.name ?? "recommendation"}-${index}`}
|
||||
{...item}
|
||||
size="compact"
|
||||
className="shadow-[0_8px_24px_rgba(15,23,42,0.08)]"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface AddressRowProps {
|
||||
name?: string;
|
||||
phone?: string;
|
||||
addressLines?: string[];
|
||||
selected?: boolean;
|
||||
showRadio?: boolean;
|
||||
showDefaultBadge?: boolean;
|
||||
showRemove?: boolean;
|
||||
showEdit?: boolean;
|
||||
showToggle?: boolean;
|
||||
toggleEnabled?: boolean;
|
||||
onSelect?: () => void;
|
||||
onRemove?: () => void;
|
||||
onEdit?: () => void;
|
||||
onToggle?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function AddressRow({
|
||||
name = "Ashan Namal",
|
||||
phone = "+94723452389",
|
||||
addressLines = [
|
||||
"29 Visakha Private Road, Colombo",
|
||||
"Colombo - 05 , Western,SriLanka",
|
||||
],
|
||||
selected = true,
|
||||
showRadio = true,
|
||||
showDefaultBadge = true,
|
||||
showRemove = true,
|
||||
showEdit = true,
|
||||
showToggle = false,
|
||||
toggleEnabled = false,
|
||||
onSelect,
|
||||
onRemove,
|
||||
onEdit,
|
||||
onToggle,
|
||||
className,
|
||||
}: AddressRowProps) {
|
||||
return (
|
||||
<article
|
||||
className={cn(
|
||||
"flex w-full items-center gap-4 rounded-lg border border-gray-200 bg-white px-5 py-4 text-sm text-[#1f2440]",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{showRadio && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSelect}
|
||||
className="flex h-4 w-4 shrink-0 items-center justify-center rounded-full border-2 border-[#00AFC1]"
|
||||
aria-label={`Select address for ${name}`}
|
||||
>
|
||||
{selected && <span className="h-2 w-2 rounded-full bg-[#00AFC1]" />}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="font-semibold text-gray-900">
|
||||
{name} <span className="font-normal text-gray-600">| {phone}</span>
|
||||
</p>
|
||||
<div className="mt-2 space-y-1 text-xs text-gray-500">
|
||||
{addressLines.map((line) => (
|
||||
<p key={line}>{line}</p>
|
||||
))}
|
||||
</div>
|
||||
{showDefaultBadge && (
|
||||
<span className="mt-2 inline-flex rounded bg-cyan-50 px-3 py-1 text-[11px] font-medium text-[#00AFC1]">
|
||||
Default
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 items-center gap-5">
|
||||
{showToggle && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggle}
|
||||
className={cn(
|
||||
"relative h-6 w-11 rounded-full transition-colors",
|
||||
toggleEnabled ? "bg-[#00AFC1]" : "bg-gray-200"
|
||||
)}
|
||||
aria-label="Toggle default address"
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"absolute top-1 h-4 w-4 rounded-full bg-white transition-transform",
|
||||
toggleEnabled ? "translate-x-6" : "translate-x-1"
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{showRemove && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRemove}
|
||||
className="text-xs font-medium text-red-500 transition-colors hover:text-red-600"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
)}
|
||||
{showEdit && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onEdit}
|
||||
className="text-xs font-medium text-[#00AFC1] transition-colors hover:text-[#008fa0]"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ButtonHTMLAttributes } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type Variant = "primary" | "outline" | "ghost" | "pill" | "teal" | "icon";
|
||||
|
||||
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: Variant;
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
const variants: Record<Variant, string> = {
|
||||
primary:
|
||||
"bg-gray-900 text-white hover:bg-gray-700",
|
||||
outline:
|
||||
"border border-gray-300 text-gray-700 hover:bg-gray-50",
|
||||
ghost:
|
||||
"text-gray-600 hover:bg-gray-100",
|
||||
pill:
|
||||
"bg-white border border-gray-200 text-gray-700 hover:bg-gray-100 rounded-full px-4 py-2 text-sm font-medium gap-2",
|
||||
teal:
|
||||
"bg-[#00BCD4] text-white hover:bg-[#00a8be] rounded-full px-5 py-2 text-sm font-medium",
|
||||
icon:
|
||||
"w-9 h-9 rounded-full bg-white border border-gray-200 text-gray-500 hover:bg-gray-100 p-0",
|
||||
};
|
||||
|
||||
export default function Button({
|
||||
variant = "primary",
|
||||
icon,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center transition-colors focus:outline-none disabled:pointer-events-none disabled:opacity-50",
|
||||
variant === "primary" && "rounded-lg px-4 py-2 text-sm font-medium",
|
||||
variants[variant],
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{icon && <span className="flex items-center">{icon}</span>}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
import Image from "next/image";
|
||||
|
||||
import Button from "@/components/ui/Button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface CartItem {
|
||||
id: string;
|
||||
name: string;
|
||||
imageSrc: string;
|
||||
imageAlt?: string;
|
||||
size?: string;
|
||||
color?: string;
|
||||
price: string;
|
||||
oldPrice?: string;
|
||||
discountBadge?: string;
|
||||
checked?: boolean;
|
||||
quantity?: number;
|
||||
}
|
||||
|
||||
interface CartShop {
|
||||
id: string;
|
||||
name: string;
|
||||
logoSrc?: string;
|
||||
checked?: boolean;
|
||||
items: CartItem[];
|
||||
}
|
||||
|
||||
interface CartItemsListProps {
|
||||
shops?: CartShop[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const defaultShops: CartShop[] = [
|
||||
{
|
||||
id: "cozy",
|
||||
name: "Cozy",
|
||||
logoSrc: "/images/cozy-round.svg",
|
||||
checked: true,
|
||||
items: [
|
||||
{
|
||||
id: "calla-tote",
|
||||
name: "Calla Tote Bag - Espresso Brown",
|
||||
imageSrc: "/images/tote-gallery-1.png",
|
||||
size: "Small - 39.5",
|
||||
color: "Espresso Brown",
|
||||
price: "LKR 5,000.00",
|
||||
oldPrice: "LKR 5,600.00",
|
||||
discountBadge: "-40%",
|
||||
checked: true,
|
||||
quantity: 1,
|
||||
},
|
||||
{
|
||||
id: "satin-heel",
|
||||
name: "Satin Teardrop Heel",
|
||||
imageSrc: "/images/tote-gallery-3.png",
|
||||
size: "39",
|
||||
color: "Silver",
|
||||
price: "LKR 8,000.00",
|
||||
checked: true,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "leafy",
|
||||
name: "Leafy",
|
||||
logoSrc: "/images/leafy-round.svg",
|
||||
items: [
|
||||
{
|
||||
id: "lana-perfume",
|
||||
name: "Lana Perfume",
|
||||
imageSrc: "/images/deal-banner-product.png",
|
||||
size: "40ml",
|
||||
color: "Green",
|
||||
price: "LKR 7,000.00",
|
||||
oldPrice: "LKR 9,000.00",
|
||||
discountBadge: "-40%",
|
||||
quantity: 1,
|
||||
},
|
||||
{
|
||||
id: "night-cream",
|
||||
name: "Leafy Special Night Cream",
|
||||
imageSrc: "/images/promo-beauty-card.png",
|
||||
size: "39",
|
||||
color: "Silver",
|
||||
price: "LKR 8,000.00",
|
||||
quantity: 1,
|
||||
},
|
||||
{
|
||||
id: "lana-serum",
|
||||
name: "Lana_Serum",
|
||||
imageSrc: "/images/deal-banner-product.png",
|
||||
size: "39",
|
||||
color: "Silver",
|
||||
price: "LKR 8,000.00",
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
function Checkbox({ checked, label }: { checked?: boolean; label: string }) {
|
||||
return (
|
||||
<input
|
||||
type="checkbox"
|
||||
defaultChecked={checked}
|
||||
aria-label={label}
|
||||
className="h-4 w-4 shrink-0 rounded border-gray-300 accent-[#00AFC1]"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function IconButton({ label, children }: { label: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={label}
|
||||
className="text-gray-500 transition-colors hover:text-gray-900"
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CartItemsList({ shops = defaultShops, className }: CartItemsListProps) {
|
||||
const totalCount = shops.reduce((count, shop) => count + shop.items.length, 0);
|
||||
|
||||
return (
|
||||
<section className={cn("w-full bg-white text-gray-900", className)}>
|
||||
<div className="flex items-center gap-3 border-b border-gray-200 pb-6">
|
||||
<Checkbox label="Select all cart items" />
|
||||
<h2 className="text-sm font-medium text-gray-900">All Items ({totalCount})</h2>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-gray-200">
|
||||
{shops.map((shop) => (
|
||||
<div key={shop.id} className="py-7">
|
||||
<div className="mb-5 flex items-center gap-3 rounded-md bg-gray-50 px-3 py-3">
|
||||
<Checkbox checked={shop.checked} label={`Select ${shop.name} shop`} />
|
||||
{shop.logoSrc && (
|
||||
<div className="relative h-10 w-10 overflow-hidden rounded-full">
|
||||
<Image src={shop.logoSrc} alt={shop.name} fill sizes="40px" className="object-cover" />
|
||||
</div>
|
||||
)}
|
||||
<span className="text-sm font-medium text-gray-900">{shop.name}</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="ml-auto h-9 rounded-full border-[#00AFC1] px-5 text-xs text-[#00AFC1]"
|
||||
>
|
||||
Visit Store
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{shop.items.map((item) => (
|
||||
<div key={item.id} className="grid grid-cols-[auto_72px_1fr] gap-4 sm:grid-cols-[auto_88px_1fr_auto] sm:items-center">
|
||||
<Checkbox checked={item.checked} label={`Select ${item.name}`} />
|
||||
|
||||
<div className="relative h-20 w-[72px] overflow-hidden rounded bg-gray-100 sm:h-24 sm:w-24">
|
||||
<Image
|
||||
src={item.imageSrc}
|
||||
alt={item.imageAlt ?? item.name}
|
||||
fill
|
||||
sizes="96px"
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 space-y-2">
|
||||
<h3 className="text-sm font-medium text-gray-900">{item.name}</h3>
|
||||
{(item.size || item.color) && (
|
||||
<p className="text-xs text-gray-500">
|
||||
{item.size && <>Size: {item.size}</>}
|
||||
{item.size && item.color && <span> · </span>}
|
||||
{item.color && <>Color: {item.color}</>}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<span className="text-base font-medium text-gray-900">{item.price}</span>
|
||||
{item.oldPrice && (
|
||||
<span className="text-sm text-red-500 line-through">{item.oldPrice}</span>
|
||||
)}
|
||||
</div>
|
||||
{item.discountBadge && (
|
||||
<span className="inline-flex rounded bg-red-500 px-2 py-1 text-[10px] font-semibold text-white">
|
||||
{item.discountBadge}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-start-3 flex items-center gap-4 sm:col-start-auto">
|
||||
<select
|
||||
defaultValue={item.quantity ?? 1}
|
||||
aria-label={`Quantity for ${item.name}`}
|
||||
className="h-9 rounded-full bg-gray-100 px-4 text-sm text-gray-900 outline-none"
|
||||
>
|
||||
{[1, 2, 3, 4, 5].map((quantity) => (
|
||||
<option key={quantity} value={quantity}>
|
||||
{quantity}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<IconButton label={`Add ${item.name} to wishlist`}>
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78L12 21.23l8.84-8.84a5.5 5.5 0 0 0 0-7.78Z" />
|
||||
</svg>
|
||||
</IconButton>
|
||||
|
||||
<IconButton label={`Remove ${item.name}`}>
|
||||
<svg className="h-4 w-4 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 7h12M10 11v6M14 11v6M9 7l1-2h4l1 2M8 7v12a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V7" />
|
||||
</svg>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import Image from "next/image";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface DealBannerProps {
|
||||
backgroundColor?: string;
|
||||
imageSrc?: string;
|
||||
imageAlt?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
showIcon?: boolean;
|
||||
onIconClick?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function DealBanner({
|
||||
backgroundColor = "#3f877c",
|
||||
imageSrc = "/images/deal-banner-product.png",
|
||||
imageAlt = "",
|
||||
title,
|
||||
description,
|
||||
showIcon = false,
|
||||
onIconClick,
|
||||
className,
|
||||
}: DealBannerProps) {
|
||||
return (
|
||||
<section
|
||||
style={{ backgroundColor }}
|
||||
className={cn(
|
||||
"relative flex min-h-[150px] w-full overflow-hidden rounded-lg px-5 py-5 text-white sm:min-h-[166px] sm:px-8 lg:items-center lg:px-10",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{imageSrc && (
|
||||
<div className="pointer-events-none absolute -left-2 bottom-0 top-0 w-[150px] sm:w-[200px] lg:w-[240px]">
|
||||
<Image
|
||||
src={imageSrc}
|
||||
alt={imageAlt}
|
||||
fill
|
||||
sizes="(max-width: 640px) 150px, (max-width: 1024px) 200px, 240px"
|
||||
className="object-contain object-left-bottom"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(title || description) && (
|
||||
<div className="relative z-10 ml-[120px] min-w-0 max-w-[560px] py-2 sm:ml-[170px] lg:ml-[225px]">
|
||||
{title && (
|
||||
<h2 className="text-xl font-medium leading-tight tracking-normal sm:text-3xl">
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
{description && (
|
||||
<p className="mt-2 text-xs leading-5 text-white/90 sm:text-sm">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showIcon && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onIconClick}
|
||||
className="absolute right-5 top-1/2 z-10 flex h-12 w-12 -translate-y-1/2 items-center justify-center rounded-full bg-black/25 text-white transition-colors hover:bg-black/35 sm:right-8 sm:h-16 sm:w-16"
|
||||
aria-label="View deal"
|
||||
>
|
||||
<svg className="h-5 w-5 sm:h-7 sm:w-7" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.4}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="m9 5 7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface DropdownButtonProps {
|
||||
options: string[];
|
||||
selected: string;
|
||||
onSelect: (value: string) => void;
|
||||
icon?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function DropdownButton({
|
||||
options,
|
||||
selected,
|
||||
onSelect,
|
||||
icon,
|
||||
className,
|
||||
}: DropdownButtonProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
function handleOutside(e: MouseEvent) {
|
||||
if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) {
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
document.addEventListener("mousedown", handleOutside);
|
||||
return () => document.removeEventListener("mousedown", handleOutside);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={wrapperRef} className={cn("relative", className)}>
|
||||
<button
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
className="flex items-center gap-2 rounded-full bg-gray-100 px-3 py-2 hover:bg-gray-200 transition-colors sm:px-3.5"
|
||||
>
|
||||
{icon && <span className="w-4 h-4 text-gray-500 flex items-center">{icon}</span>}
|
||||
<span className="hidden text-sm font-medium text-gray-700 sm:inline">{selected}</span>
|
||||
<svg
|
||||
className={`w-3 h-3 text-gray-400 transition-transform duration-200 ${open ? "rotate-180" : ""}`}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2.5}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="absolute right-0 top-full mt-2 w-44 bg-white rounded-xl shadow-xl border border-gray-100 py-1.5 z-[9999]">
|
||||
{options.map((opt) => (
|
||||
<button
|
||||
key={opt}
|
||||
onClick={() => { onSelect(opt); setOpen(false); }}
|
||||
className={cn(
|
||||
"w-full text-left px-4 py-2.5 text-sm transition-colors",
|
||||
selected === opt
|
||||
? "text-[#00BCD4] font-medium bg-cyan-50"
|
||||
: "text-gray-700 hover:bg-gray-50"
|
||||
)}
|
||||
>
|
||||
{opt}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import Button from "@/components/ui/Button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type FieldType =
|
||||
| "text"
|
||||
| "email"
|
||||
| "tel"
|
||||
| "number"
|
||||
| "password"
|
||||
| "textarea"
|
||||
| "select"
|
||||
| "checkbox"
|
||||
| "radio"
|
||||
| "otp";
|
||||
|
||||
interface FieldOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface FormField {
|
||||
name: string;
|
||||
label?: string;
|
||||
type: FieldType;
|
||||
placeholder?: string;
|
||||
required?: boolean;
|
||||
options?: FieldOption[];
|
||||
otpLength?: number;
|
||||
helperText?: string;
|
||||
}
|
||||
|
||||
interface DynamicFormProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
fields?: FormField[];
|
||||
submitLabel?: string;
|
||||
resetLabel?: string;
|
||||
showReset?: boolean;
|
||||
onSubmit?: (formData: FormData) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const defaultFields: FormField[] = [
|
||||
{
|
||||
name: "fullName",
|
||||
label: "Full Name",
|
||||
type: "text",
|
||||
placeholder: "Enter full name",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "email",
|
||||
label: "Email",
|
||||
type: "email",
|
||||
placeholder: "Enter email address",
|
||||
},
|
||||
{
|
||||
name: "password",
|
||||
label: "Password",
|
||||
type: "password",
|
||||
placeholder: "Enter password",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "otp",
|
||||
label: "Verification Code",
|
||||
type: "otp",
|
||||
otpLength: 6,
|
||||
helperText: "Enter the code sent to your phone.",
|
||||
},
|
||||
{
|
||||
name: "category",
|
||||
label: "Category",
|
||||
type: "select",
|
||||
placeholder: "Select category",
|
||||
options: [
|
||||
{ label: "Beauty & Health", value: "beauty" },
|
||||
{ label: "Fashion", value: "fashion" },
|
||||
{ label: "Electronics", value: "electronics" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "message",
|
||||
label: "Message",
|
||||
type: "textarea",
|
||||
placeholder: "Write message",
|
||||
},
|
||||
{
|
||||
name: "newsletter",
|
||||
label: "Send me product updates",
|
||||
type: "checkbox",
|
||||
},
|
||||
{
|
||||
name: "contactMethod",
|
||||
label: "Preferred Contact",
|
||||
type: "radio",
|
||||
options: [
|
||||
{ label: "Email", value: "email" },
|
||||
{ label: "Phone", value: "phone" },
|
||||
{ label: "WhatsApp", value: "whatsapp" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
function FieldLabel({ label, required }: { label?: string; required?: boolean }) {
|
||||
if (!label) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<label className="text-sm font-medium text-[#1f2440]">
|
||||
{label}
|
||||
{required && <span className="ml-1 text-red-500">*</span>}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
function PasswordField({ field }: { field: FormField }) {
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<input
|
||||
name={field.name}
|
||||
type={visible ? "text" : "password"}
|
||||
placeholder={field.placeholder}
|
||||
required={field.required}
|
||||
className="h-12 w-full rounded-full bg-gray-100 px-5 pr-12 text-sm outline-none placeholder:text-gray-400"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setVisible((current) => !current)}
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-500"
|
||||
aria-label={visible ? "Hide password" : "Show password"}
|
||||
>
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
|
||||
{visible ? (
|
||||
<>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3 3l18 18" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M10.58 10.58A2 2 0 0 0 13.42 13.42" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9.88 4.24A10.43 10.43 0 0 1 12 4c5 0 8.5 4 10 8a12.2 12.2 0 0 1-2.04 3.47M6.11 6.11A12.26 12.26 0 0 0 2 12c1.5 4 5 8 10 8 1.43 0 2.75-.33 3.93-.9" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12Z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z" />
|
||||
</>
|
||||
)}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function OtpField({ field }: { field: FormField }) {
|
||||
const length = field.otpLength ?? 6;
|
||||
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
{Array.from({ length }).map((_, index) => (
|
||||
<input
|
||||
key={index}
|
||||
name={`${field.name}_${index + 1}`}
|
||||
inputMode="numeric"
|
||||
maxLength={1}
|
||||
className="h-12 w-11 rounded-lg bg-gray-100 text-center text-sm font-semibold outline-none focus:ring-2 focus:ring-[#00AFC1]"
|
||||
aria-label={`${field.label ?? "OTP"} digit ${index + 1}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RenderField({ field }: { field: FormField }) {
|
||||
if (field.type === "checkbox") {
|
||||
return (
|
||||
<label className="flex items-center gap-3 text-sm text-gray-700">
|
||||
<input
|
||||
name={field.name}
|
||||
type="checkbox"
|
||||
required={field.required}
|
||||
className="h-4 w-4 rounded border-gray-300 accent-[#00AFC1]"
|
||||
/>
|
||||
<span>{field.label}</span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
if (field.type === "radio") {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<FieldLabel label={field.label} required={field.required} />
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{field.options?.map((option) => (
|
||||
<label key={option.value} className="flex items-center gap-2 text-sm text-gray-700">
|
||||
<input
|
||||
name={field.name}
|
||||
type="radio"
|
||||
value={option.value}
|
||||
required={field.required}
|
||||
className="h-4 w-4 accent-[#00AFC1]"
|
||||
/>
|
||||
<span>{option.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<FieldLabel label={field.label} required={field.required} />
|
||||
|
||||
{field.type === "password" && <PasswordField field={field} />}
|
||||
|
||||
{field.type === "otp" && <OtpField field={field} />}
|
||||
|
||||
{field.type === "select" && (
|
||||
<select
|
||||
name={field.name}
|
||||
required={field.required}
|
||||
defaultValue=""
|
||||
className="h-12 w-full rounded-full bg-gray-100 px-5 text-sm text-gray-700 outline-none"
|
||||
>
|
||||
<option value="" disabled>
|
||||
{field.placeholder ?? "Select option"}
|
||||
</option>
|
||||
{field.options?.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
|
||||
{field.type === "textarea" && (
|
||||
<textarea
|
||||
name={field.name}
|
||||
placeholder={field.placeholder}
|
||||
required={field.required}
|
||||
rows={4}
|
||||
className="w-full resize-none rounded-2xl bg-gray-100 px-5 py-4 text-sm outline-none placeholder:text-gray-400"
|
||||
/>
|
||||
)}
|
||||
|
||||
{["text", "email", "tel", "number"].includes(field.type) && (
|
||||
<input
|
||||
name={field.name}
|
||||
type={field.type}
|
||||
placeholder={field.placeholder}
|
||||
required={field.required}
|
||||
className="h-12 w-full rounded-full bg-gray-100 px-5 text-sm outline-none placeholder:text-gray-400"
|
||||
/>
|
||||
)}
|
||||
|
||||
{field.helperText && <p className="text-xs text-gray-500">{field.helperText}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DynamicForm({
|
||||
title = "Form Details",
|
||||
description = "Choose the fields you need and reuse this form anywhere.",
|
||||
fields = defaultFields,
|
||||
submitLabel = "Submit",
|
||||
resetLabel = "Reset",
|
||||
showReset = true,
|
||||
onSubmit,
|
||||
className,
|
||||
}: DynamicFormProps) {
|
||||
return (
|
||||
<form
|
||||
className={cn("w-full max-w-[640px] rounded-lg bg-white p-6 shadow-sm", className)}
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
onSubmit?.(new FormData(event.currentTarget));
|
||||
}}
|
||||
>
|
||||
{(title || description) && (
|
||||
<div className="mb-6">
|
||||
{title && <h2 className="text-2xl font-medium text-[#1f2440]">{title}</h2>}
|
||||
{description && <p className="mt-2 text-sm text-gray-500">{description}</p>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-5">
|
||||
{fields.map((field) => (
|
||||
<RenderField key={field.name} field={field} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-7 flex flex-col gap-3 sm:flex-row">
|
||||
{showReset && (
|
||||
<Button
|
||||
type="reset"
|
||||
variant="ghost"
|
||||
className="h-11 flex-1 rounded-full bg-gray-100 text-sm font-semibold text-gray-700 hover:bg-gray-200"
|
||||
>
|
||||
{resetLabel}
|
||||
</Button>
|
||||
)}
|
||||
<Button type="submit" variant="teal" className="h-11 flex-1 rounded-full text-sm font-semibold">
|
||||
{submitLabel}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
"use client";
|
||||
|
||||
import Button from "@/components/ui/Button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface EmptyModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
title?: string;
|
||||
description?: string;
|
||||
primaryLabel?: string;
|
||||
secondaryLabel?: string;
|
||||
onPrimaryClick?: () => void;
|
||||
onSecondaryClick?: () => void;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function EmptyModal({
|
||||
open,
|
||||
onClose,
|
||||
title = "Nothing here yet",
|
||||
description,
|
||||
primaryLabel,
|
||||
secondaryLabel,
|
||||
onPrimaryClick,
|
||||
onSecondaryClick,
|
||||
children,
|
||||
className,
|
||||
}: EmptyModalProps) {
|
||||
if (!open) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[10000] flex items-center justify-center bg-black/35 px-4 py-6">
|
||||
<section
|
||||
className={cn(
|
||||
"relative w-full max-w-[420px] rounded-lg bg-white p-6 text-center shadow-2xl",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="absolute right-5 top-5 text-gray-400 transition-colors hover:text-gray-700"
|
||||
aria-label="Close modal"
|
||||
>
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-gray-100 text-gray-400">
|
||||
<svg className="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M20 13V7a2 2 0 0 0-2-2h-3.5L13 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h5" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M16 19h6M19 16v6" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{title && <h2 className="mt-5 text-lg font-semibold text-[#1f2440]">{title}</h2>}
|
||||
{description && <p className="mt-2 text-sm leading-6 text-gray-500">{description}</p>}
|
||||
|
||||
{children && <div className="mt-6">{children}</div>}
|
||||
|
||||
{(primaryLabel || secondaryLabel) && (
|
||||
<div className="mt-7 grid gap-3 sm:grid-cols-2">
|
||||
{secondaryLabel && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={onSecondaryClick ?? onClose}
|
||||
className="h-11 rounded-full bg-gray-100 text-sm font-semibold text-gray-700 hover:bg-gray-200"
|
||||
>
|
||||
{secondaryLabel}
|
||||
</Button>
|
||||
)}
|
||||
{primaryLabel && (
|
||||
<Button
|
||||
variant="teal"
|
||||
onClick={onPrimaryClick}
|
||||
className="h-11 rounded-full text-sm font-semibold"
|
||||
>
|
||||
{primaryLabel}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import Image from "next/image";
|
||||
|
||||
import SectionHeader from "@/components/layout/SectionHeader";
|
||||
import ProductCard, { type ProductCardProps } from "@/components/ui/ProductCard";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface FeatureProductsProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
products?: ProductCardProps[];
|
||||
imageSrc?: string;
|
||||
imageAlt?: string;
|
||||
imageSide?: "left" | "right";
|
||||
headerAlign?: "left" | "center";
|
||||
productsPosition?: "top" | "bottom";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const defaultProducts: ProductCardProps[] = [
|
||||
{
|
||||
imageSrc: "/images/tote-gallery-1.png",
|
||||
imageAlt: "Espresso brown tote bag",
|
||||
discountBadge: "-42%",
|
||||
showWishlist: true,
|
||||
name: "Calla Tote Bag - Espresso Brown",
|
||||
rating: 4.9,
|
||||
soldCount: "128 Sold",
|
||||
price: "LKR 5,000.00",
|
||||
oldPrice: "LKR 5,600.00",
|
||||
showAddToCart: true,
|
||||
},
|
||||
{
|
||||
imageSrc: "/images/tote-gallery-2.png",
|
||||
imageAlt: "Fashion sandals",
|
||||
showWishlist: true,
|
||||
name: "Calla Tote Bag - Espresso Brown",
|
||||
rating: 4.9,
|
||||
soldCount: "128 Sold",
|
||||
price: "LKR 5,000.00",
|
||||
oldPrice: "LKR 5,600.00",
|
||||
showAddToCart: true,
|
||||
},
|
||||
{
|
||||
imageSrc: "/images/tote-gallery-3.png",
|
||||
imageAlt: "Tan sandals",
|
||||
showWishlist: true,
|
||||
name: "Calla Tote Bag - Espresso Brown",
|
||||
rating: 4.9,
|
||||
soldCount: "128 Sold",
|
||||
price: "LKR 5,000.00",
|
||||
oldPrice: "LKR 5,600.00",
|
||||
showAddToCart: true,
|
||||
},
|
||||
{
|
||||
imageSrc: "/images/tote-gallery-4.png",
|
||||
imageAlt: "Black handbag",
|
||||
discountBadge: "-40%",
|
||||
showWishlist: true,
|
||||
name: "Calla Tote Bag - Espresso Brown",
|
||||
rating: 4.9,
|
||||
soldCount: "128 Sold",
|
||||
price: "LKR 5,000.00",
|
||||
oldPrice: "LKR 5,600.00",
|
||||
showAddToCart: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default function FeatureProducts({
|
||||
title = "Most Popular",
|
||||
description = "Loved by customers, chosen again and again",
|
||||
products = defaultProducts,
|
||||
imageSrc = "/images/feature-fashion-side.png",
|
||||
imageAlt = "Featured fashion editorial",
|
||||
imageSide = "right",
|
||||
headerAlign = "center",
|
||||
productsPosition = "bottom",
|
||||
className,
|
||||
}: FeatureProductsProps) {
|
||||
const hasHeader = Boolean(title || description);
|
||||
const hasProducts = products.length > 0;
|
||||
const hasImage = Boolean(imageSrc);
|
||||
|
||||
const productContent = (
|
||||
<div
|
||||
className={cn(
|
||||
"flex min-h-full flex-col gap-8 bg-white px-4 py-8 sm:px-8 lg:px-10",
|
||||
productsPosition === "bottom" ? "justify-center" : "justify-start"
|
||||
)}
|
||||
>
|
||||
{hasHeader && (
|
||||
<div className={cn(headerAlign === "center" ? "text-center" : "text-left")}>
|
||||
<SectionHeader title={title} details={description} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasProducts && (
|
||||
<div className="grid grid-cols-1 gap-5 min-[480px]:grid-cols-2">
|
||||
{products.map((product, index) => (
|
||||
<ProductCard
|
||||
key={`${product.name ?? "product"}-${index}`}
|
||||
{...product}
|
||||
className={cn("max-w-none rounded-lg shadow-none", product.className)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const imageContent = hasImage ? (
|
||||
<div className="relative min-h-[360px] overflow-hidden bg-gray-100 lg:min-h-[720px]">
|
||||
<Image
|
||||
src={imageSrc}
|
||||
alt={imageAlt}
|
||||
fill
|
||||
sizes="(max-width: 1024px) 100vw, 50vw"
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<section className={cn("grid w-full overflow-hidden rounded-lg bg-white lg:grid-cols-2", className)}>
|
||||
{imageSide === "left" && imageContent}
|
||||
{productContent}
|
||||
{imageSide === "right" && imageContent}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const categories = [
|
||||
{ label: "All" },
|
||||
{ label: "Electronic Devices" },
|
||||
{ label: "Women Accessories", checked: true },
|
||||
{ label: "Mother & Kids" },
|
||||
{ label: "Furniture" },
|
||||
{ label: "Foods & Drinks" },
|
||||
];
|
||||
|
||||
const brands = [
|
||||
{ label: "All" },
|
||||
{ label: "Charles", checked: true },
|
||||
{ label: "Green" },
|
||||
{ label: "Black" },
|
||||
{ label: "Orange" },
|
||||
];
|
||||
|
||||
const availability = [{ label: "In Stock" }, { label: "Out of Stock" }];
|
||||
const discounts = [{ label: "On Sale", hot: true }, { label: "No Discount" }];
|
||||
const sizes = [
|
||||
{ label: "S - 39.5", checked: true },
|
||||
{ label: "M - 40" },
|
||||
{ label: "L - 41" },
|
||||
{ label: "XL - 42" },
|
||||
{ label: "XXL - 43" },
|
||||
];
|
||||
|
||||
const colors = [
|
||||
{ label: "Cyan", value: "#0b8793" },
|
||||
{ label: "Blue", value: "#b9c4ee" },
|
||||
{ label: "Purple", value: "#9c278f" },
|
||||
{ label: "Pink", value: "#e65a9a" },
|
||||
{ label: "Red", value: "#d71919" },
|
||||
{ label: "Orange", value: "#f26a12" },
|
||||
{ label: "Yellow", value: "#ffbf00" },
|
||||
{ label: "Green", value: "#66a74f" },
|
||||
];
|
||||
|
||||
function ChevronIcon() {
|
||||
return (
|
||||
<svg className="h-3 w-3 text-gray-700" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 7.25a.75.75 0 0 1 .53.22l3 3a.75.75 0 1 1-1.06 1.06L10 9.06l-2.47 2.47a.75.75 0 0 1-1.06-1.06l3-3a.75.75 0 0 1 .53-.22Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function FlameIcon() {
|
||||
return (
|
||||
<svg className="h-3 w-3 text-[#f97316]" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path d="M10.8 1.6c.33 2.37-.63 3.95-1.67 5.28-.95 1.22-1.95 2.2-1.3 4.08.24-.63.66-1.4 1.3-2.08.16 1.66 1.16 2.18 1.16 3.55 0 1.02-.78 1.76-1.8 1.76-1.7 0-3.06-1.5-3.06-3.7 0-1.43.72-3.03 1.7-4.28C8.2 4.86 9.62 3.7 10.8 1.6Zm1.38 4.43c.55.72 1.06 1.45 1.47 2.22.5.94.92 2 .92 3.13 0 3.05-2.36 5.02-5.1 5.02-2.77 0-5-2.03-5-5.35 0-2.08 1.03-4 2.2-5.5.55-.7 1.12-1.3 1.63-1.88.5-.57.91-1.1 1.15-1.7.18-.45.76-.6 1.14-.3 1.38 1.1 2.2 2.55 2.2 4.36 0 .02 0 .03-.01.05Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function StarRating({ value }: { value: number }) {
|
||||
return (
|
||||
<div className="flex items-center gap-0.5" aria-label={`${value} stars and up`}>
|
||||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
<svg
|
||||
key={index}
|
||||
className={cn("h-4 w-4", index < value ? "text-[#FFB800]" : "text-gray-300")}
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="m10 1.5 2.55 5.16 5.7.83-4.13 4.02.98 5.68L10 14.51l-5.1 2.68.98-5.68L1.75 7.49l5.7-.83L10 1.5Z" />
|
||||
</svg>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FilterSection({
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<section className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<ChevronIcon />
|
||||
<h3 className="text-xs font-semibold text-gray-800">{title}</h3>
|
||||
</div>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function CheckboxItem({
|
||||
label,
|
||||
checked,
|
||||
children,
|
||||
}: {
|
||||
label: string;
|
||||
checked?: boolean;
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<label className="flex cursor-pointer items-center gap-3 text-xs text-gray-700">
|
||||
<input
|
||||
type="checkbox"
|
||||
defaultChecked={checked}
|
||||
className="h-4 w-4 rounded border-gray-300 accent-[#00AFC1]"
|
||||
/>
|
||||
{children ?? <span>{label}</span>}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
interface FilterPanelProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function FilterPanel({ className }: FilterPanelProps) {
|
||||
return (
|
||||
<aside className={cn("w-full max-w-[240px] bg-white text-gray-900", className)}>
|
||||
<form>
|
||||
<div className="mb-7 flex items-center justify-between">
|
||||
<h2 className="text-sm font-medium text-gray-900">Filters</h2>
|
||||
<button
|
||||
type="reset"
|
||||
className="rounded-full border border-[#00AFC1] px-4 py-1.5 text-[11px] font-medium text-[#00AFC1] transition-colors hover:bg-cyan-50"
|
||||
>
|
||||
Reset All
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-9">
|
||||
<FilterSection title="Category">
|
||||
<div className="space-y-3 pl-5">
|
||||
{categories.map((item) => (
|
||||
<CheckboxItem key={item.label} label={item.label} checked={item.checked} />
|
||||
))}
|
||||
<button type="button" className="text-xs font-medium text-[#00AFC1] hover:text-[#008fa0]">
|
||||
View all Categories
|
||||
</button>
|
||||
</div>
|
||||
</FilterSection>
|
||||
|
||||
<FilterSection title="Brand">
|
||||
<div className="space-y-3 pl-5">
|
||||
{brands.map((item) => (
|
||||
<CheckboxItem key={item.label} label={item.label} checked={item.checked} />
|
||||
))}
|
||||
<button type="button" className="flex items-center gap-2 text-xs font-medium text-[#00AFC1] hover:text-[#008fa0]">
|
||||
<span className="text-sm leading-none">+</span>
|
||||
Show more 45 Brands
|
||||
</button>
|
||||
</div>
|
||||
</FilterSection>
|
||||
|
||||
<FilterSection title="Price">
|
||||
<div className="flex items-center gap-3 pl-5">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="LKR |"
|
||||
className="w-20 rounded-full bg-gray-100 px-4 py-2 text-xs text-gray-500 outline-none placeholder:text-gray-500"
|
||||
/>
|
||||
<span className="text-gray-300">-</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="LKR |"
|
||||
className="w-20 rounded-full bg-gray-100 px-4 py-2 text-xs text-gray-500 outline-none placeholder:text-gray-500"
|
||||
/>
|
||||
</div>
|
||||
</FilterSection>
|
||||
|
||||
<FilterSection title="Availability">
|
||||
<div className="space-y-3 pl-5">
|
||||
{availability.map((item) => (
|
||||
<CheckboxItem key={item.label} label={item.label} />
|
||||
))}
|
||||
</div>
|
||||
</FilterSection>
|
||||
|
||||
<FilterSection title="Discount">
|
||||
<div className="space-y-3 pl-5">
|
||||
{discounts.map((item) => (
|
||||
<CheckboxItem key={item.label} label={item.label}>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{item.label}
|
||||
{item.hot && <FlameIcon />}
|
||||
</span>
|
||||
</CheckboxItem>
|
||||
))}
|
||||
</div>
|
||||
</FilterSection>
|
||||
|
||||
<FilterSection title="Rating">
|
||||
<div className="space-y-2 pl-5">
|
||||
{[5, 4, 3, 2, 1, 0].map((rating) => (
|
||||
<StarRating key={rating} value={rating} />
|
||||
))}
|
||||
</div>
|
||||
</FilterSection>
|
||||
|
||||
<FilterSection title="Size">
|
||||
<div className="space-y-3 pl-5">
|
||||
{sizes.map((item) => (
|
||||
<CheckboxItem key={item.label} label={item.label} checked={item.checked} />
|
||||
))}
|
||||
</div>
|
||||
</FilterSection>
|
||||
|
||||
<FilterSection title="Color">
|
||||
<div className="space-y-3 pl-5">
|
||||
{colors.map((item) => (
|
||||
<CheckboxItem key={item.label} label={item.label}>
|
||||
<span className="flex items-center gap-3">
|
||||
<span className="h-4 w-12 rounded" style={{ backgroundColor: item.value }} />
|
||||
<span>{item.label}</span>
|
||||
</span>
|
||||
</CheckboxItem>
|
||||
))}
|
||||
</div>
|
||||
</FilterSection>
|
||||
</div>
|
||||
</form>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import Image from "next/image";
|
||||
|
||||
import Button from "@/components/ui/Button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface HorizontalProductRowProps {
|
||||
imageSrc?: string;
|
||||
imageAlt?: string;
|
||||
name?: string;
|
||||
size?: string;
|
||||
color?: string;
|
||||
colorValue?: string;
|
||||
quantity?: number;
|
||||
price?: string;
|
||||
oldPrice?: string;
|
||||
discountBadge?: string;
|
||||
showQuantity?: boolean;
|
||||
showAddToCart?: boolean;
|
||||
showBuyNow?: boolean;
|
||||
showIconAction?: boolean;
|
||||
onAddToCart?: () => void;
|
||||
onBuyNow?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function HorizontalProductRow({
|
||||
imageSrc = "/images/tote-gallery-3.png",
|
||||
imageAlt = "",
|
||||
name = "Liberty Formal Step in Men Shoe",
|
||||
size = "41",
|
||||
color = "Black",
|
||||
colorValue = "#111827",
|
||||
quantity = 1,
|
||||
price = "LKR 12,000.00",
|
||||
oldPrice = "LKR 14,000.00",
|
||||
discountBadge = "-25%",
|
||||
showQuantity = true,
|
||||
showAddToCart = true,
|
||||
showBuyNow = true,
|
||||
showIconAction = true,
|
||||
onAddToCart,
|
||||
onBuyNow,
|
||||
className,
|
||||
}: HorizontalProductRowProps) {
|
||||
return (
|
||||
<article
|
||||
className={cn(
|
||||
"flex w-full flex-col gap-4 rounded-md bg-white p-3 shadow-sm sm:flex-row sm:items-center",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{imageSrc && (
|
||||
<div className="relative h-20 w-24 shrink-0 overflow-hidden rounded bg-gray-100">
|
||||
<Image src={imageSrc} alt={imageAlt || name} fill sizes="96px" className="object-cover" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
{name && <h3 className="text-sm font-semibold text-gray-900">{name}</h3>}
|
||||
{(size || color) && (
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
{size && <>Size: {size}</>}
|
||||
{size && color && <span> · </span>}
|
||||
{color && <>Color: {color}</>}
|
||||
{quantity && <span> · Qty: {quantity}</span>}
|
||||
</p>
|
||||
)}
|
||||
{colorValue && (
|
||||
<div className="mt-3 flex items-center gap-2 rounded bg-gray-50 px-3 py-2 text-xs text-gray-700 sm:w-28">
|
||||
<span className="h-4 w-4 rounded-full" style={{ backgroundColor: colorValue }} />
|
||||
<span>{quantity}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="min-w-[130px]">
|
||||
{price && <p className="text-sm font-semibold text-gray-900">{price}</p>}
|
||||
{oldPrice && <p className="mt-1 text-xs text-red-500 line-through">{oldPrice}</p>}
|
||||
{discountBadge && (
|
||||
<span className="mt-1 inline-flex rounded bg-red-500 px-2 py-1 text-[10px] font-semibold text-white">
|
||||
{discountBadge}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3 sm:ml-auto sm:flex-nowrap">
|
||||
{showQuantity && (
|
||||
<select
|
||||
defaultValue={quantity}
|
||||
aria-label={`Quantity for ${name}`}
|
||||
className="h-9 rounded-full bg-gray-100 px-4 text-xs text-gray-900 outline-none"
|
||||
>
|
||||
{[1, 2, 3, 4, 5].map((value) => (
|
||||
<option key={value} value={value}>
|
||||
{value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
|
||||
{showAddToCart && (
|
||||
<Button variant="teal" onClick={onAddToCart} className="h-9 px-6 text-xs">
|
||||
Add to cart
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{showBuyNow && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onBuyNow}
|
||||
className="h-9 rounded-full border-[#00AFC1] px-6 text-xs text-[#00AFC1]"
|
||||
>
|
||||
Buy it now
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{showIconAction && (
|
||||
<Button variant="icon" aria-label="More product actions" className="h-9 w-9 border-0 bg-gray-100">
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6h.01M12 12h.01M12 18h.01" />
|
||||
</svg>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import Image from "next/image";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ItemDetail {
|
||||
id: string;
|
||||
name: string;
|
||||
imageSrc: string;
|
||||
imageAlt?: string;
|
||||
size?: string;
|
||||
color?: string;
|
||||
quantity?: number;
|
||||
price: string;
|
||||
oldPrice?: string;
|
||||
discountBadge?: string;
|
||||
}
|
||||
|
||||
interface ItemDetailsProps {
|
||||
title?: string;
|
||||
shopName?: string;
|
||||
shopLogoSrc?: string;
|
||||
items?: ItemDetail[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const defaultItems: ItemDetail[] = [
|
||||
{
|
||||
id: "calla-tote",
|
||||
name: "Calla Tote Bag - Espresso Brown",
|
||||
imageSrc: "/images/tote-gallery-1.png",
|
||||
size: "Small",
|
||||
color: "Espresso Brown",
|
||||
quantity: 1,
|
||||
price: "LKR 5,000.00",
|
||||
oldPrice: "LKR 5,600.00",
|
||||
discountBadge: "-50%",
|
||||
},
|
||||
{
|
||||
id: "satin-heel",
|
||||
name: "Satin Teardrop Heel",
|
||||
imageSrc: "/images/tote-gallery-3.png",
|
||||
size: "41",
|
||||
color: "Silver",
|
||||
quantity: 1,
|
||||
price: "LKR 8,000.00",
|
||||
},
|
||||
];
|
||||
|
||||
export default function ItemDetails({
|
||||
title,
|
||||
shopName = "Cozy",
|
||||
shopLogoSrc = "/images/cozy-round.svg",
|
||||
items = defaultItems,
|
||||
className,
|
||||
}: ItemDetailsProps) {
|
||||
const heading = title ?? `Item Details (${items.length} Items)`;
|
||||
|
||||
return (
|
||||
<section className={cn("w-full max-w-[620px] rounded-lg bg-[#f7f7f8] p-6 text-[#1f2440]", className)}>
|
||||
<h2 className="text-2xl font-medium">{heading}</h2>
|
||||
|
||||
<div className="mt-8 rounded-lg bg-white px-3 py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
{shopLogoSrc && (
|
||||
<div className="relative h-8 w-8 overflow-hidden rounded-full">
|
||||
<Image src={shopLogoSrc} alt={shopName} fill sizes="32px" className="object-cover" />
|
||||
</div>
|
||||
)}
|
||||
<span className="text-sm font-medium text-gray-900">{shopName}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 space-y-8">
|
||||
{items.map((item) => (
|
||||
<article key={item.id} className="flex gap-8">
|
||||
<div className="relative h-28 w-24 shrink-0 overflow-hidden rounded bg-gray-100">
|
||||
<Image
|
||||
src={item.imageSrc}
|
||||
alt={item.imageAlt ?? item.name}
|
||||
fill
|
||||
sizes="96px"
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 space-y-2">
|
||||
<h3 className="text-lg font-medium text-[#1f2440]">{item.name}</h3>
|
||||
<p className="text-xs text-gray-500">
|
||||
{item.size && <>Size: {item.size}</>}
|
||||
{item.size && item.color && <span> · </span>}
|
||||
{item.color && <>Color: {item.color}</>}
|
||||
{(item.size || item.color) && item.quantity && <span> · </span>}
|
||||
{item.quantity && <>Qty: {item.quantity}</>}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<span className="text-xl font-medium text-[#101936]">{item.price}</span>
|
||||
{item.oldPrice && (
|
||||
<span className="text-base text-red-500 line-through">{item.oldPrice}</span>
|
||||
)}
|
||||
</div>
|
||||
{item.discountBadge && (
|
||||
<span className="inline-flex rounded bg-red-500 px-2 py-1 text-[10px] font-semibold text-white">
|
||||
{item.discountBadge}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface LargePromoBannerProps {
|
||||
imageSrc?: string;
|
||||
imageAlt?: string;
|
||||
eyebrow?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
showNavigation?: boolean;
|
||||
onPrevious?: () => void;
|
||||
onNext?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function ArrowButton({
|
||||
direction,
|
||||
onClick,
|
||||
}: {
|
||||
direction: "left" | "right";
|
||||
onClick?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
"absolute top-1/2 z-20 flex h-11 w-11 -translate-y-1/2 items-center justify-center rounded-full bg-black/35 text-white backdrop-blur-sm transition-colors hover:bg-black/50",
|
||||
direction === "left" ? "left-5" : "right-5"
|
||||
)}
|
||||
aria-label={direction === "left" ? "Previous banner" : "Next banner"}
|
||||
>
|
||||
<svg
|
||||
className={cn("h-5 w-5", direction === "left" && "rotate-180")}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2.4}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="m9 5 7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default function LargePromoBanner({
|
||||
imageSrc = "/images/large-sale-banner.png",
|
||||
imageAlt = "Seasonal sale banner",
|
||||
eyebrow = "Save More This Season",
|
||||
title = "Up to 40% Off",
|
||||
description = "Discover quality products at special prices, available for a limited time.",
|
||||
showNavigation = false,
|
||||
onPrevious,
|
||||
onNext,
|
||||
className,
|
||||
}: LargePromoBannerProps) {
|
||||
const hasText = Boolean(eyebrow || title || description);
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cn(
|
||||
"relative min-h-[180px] w-full overflow-hidden rounded-md bg-gray-900 sm:min-h-[240px] lg:min-h-[300px]",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{imageSrc && (
|
||||
<Image
|
||||
src={imageSrc}
|
||||
alt={imageAlt}
|
||||
fill
|
||||
sizes="100vw"
|
||||
className="object-cover"
|
||||
priority
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-black/75 via-black/25 to-black/20" />
|
||||
|
||||
{showNavigation && (
|
||||
<>
|
||||
<ArrowButton direction="left" onClick={onPrevious} />
|
||||
<ArrowButton direction="right" onClick={onNext} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasText && (
|
||||
<div className="relative z-10 flex min-h-[180px] max-w-[520px] flex-col justify-center px-7 py-8 text-white sm:min-h-[240px] sm:px-16 lg:min-h-[300px] lg:px-24">
|
||||
{eyebrow && (
|
||||
<p className="mb-3 text-xs font-medium text-white/85 sm:text-sm">
|
||||
{eyebrow}
|
||||
</p>
|
||||
)}
|
||||
{title && (
|
||||
<h2 className="text-3xl font-semibold leading-tight tracking-normal sm:text-5xl">
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
{description && (
|
||||
<p className="mt-3 max-w-[440px] text-sm leading-6 text-white/90 sm:text-base">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import Button from "@/components/ui/Button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Conversation {
|
||||
id: string;
|
||||
sender: string;
|
||||
avatarText?: string;
|
||||
avatarColor?: string;
|
||||
preview: string;
|
||||
time: string;
|
||||
}
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
sender: string;
|
||||
text: string;
|
||||
time?: string;
|
||||
}
|
||||
|
||||
interface MessageCenterProps {
|
||||
conversations?: Conversation[];
|
||||
messages?: Message[];
|
||||
initialConversationId?: string;
|
||||
emptyTitle?: string;
|
||||
emptyDescription?: string;
|
||||
showEmptyState?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const defaultConversations: Conversation[] = [
|
||||
{
|
||||
id: "rdb",
|
||||
sender: "RDB Diriya",
|
||||
avatarText: "Diriya",
|
||||
avatarColor: "#ef476f",
|
||||
preview: "Get prepared for some big discounts... It's SuperDeals! Up to 47%",
|
||||
time: "3:24 PM",
|
||||
},
|
||||
{
|
||||
id: "cozy",
|
||||
sender: "Cozy",
|
||||
avatarText: "coz",
|
||||
avatarColor: "#ad5a17",
|
||||
preview: "Get prepared for some big discounts... It's SuperDeals! Up to 47%",
|
||||
time: "3:24 PM",
|
||||
},
|
||||
{
|
||||
id: "tiki",
|
||||
sender: "Tiki",
|
||||
avatarText: "Tiki",
|
||||
avatarColor: "#123d3a",
|
||||
preview: "Get prepared for some big discounts... It's SuperDeals! Up to 47%",
|
||||
time: "3:24 PM",
|
||||
},
|
||||
{
|
||||
id: "caju",
|
||||
sender: "Caju",
|
||||
avatarText: "Caju",
|
||||
avatarColor: "#1f2a12",
|
||||
preview: "Get prepared for some big discounts... It's SuperDeals! Up to 47%",
|
||||
time: "3:24 PM",
|
||||
},
|
||||
];
|
||||
|
||||
const defaultMessages: Message[] = [
|
||||
{
|
||||
id: "1",
|
||||
sender: "Cozy",
|
||||
text: "Get prepared for some big discounts... It's SuperDeals! Up to 47.76% off, jump in and grab a huge saving while stocks last.",
|
||||
time: "3:24 PM",
|
||||
},
|
||||
];
|
||||
|
||||
function InboxIcon() {
|
||||
return (
|
||||
<svg className="h-8 w-8" viewBox="0 0 48 48" fill="none" aria-hidden="true">
|
||||
<path d="M12 32 28 6l8 5-14 25-10-4Z" fill="#3B82F6" />
|
||||
<path d="M9 36h15l-4 7H8l1-7Z" fill="#1E3A8A" />
|
||||
<path d="M29 7l7 4 3-5-7-4-3 5Z" fill="#93C5FD" />
|
||||
<path d="M15 26l10 4" stroke="white" strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyInboxArt() {
|
||||
return (
|
||||
<svg className="h-20 w-20" viewBox="0 0 96 96" fill="none" aria-hidden="true">
|
||||
<circle cx="48" cy="50" r="22" fill="#A7D8FF" />
|
||||
<path d="M32 54c4 12 22 14 31 1 6-9 1-20-10-23-13-4-25 8-21 22Z" fill="#4FA6F7" />
|
||||
<path d="M37 57c7 6 18 5 25-2" stroke="white" strokeWidth="4" strokeLinecap="round" />
|
||||
<circle cx="42" cy="45" r="3" fill="#0F172A" />
|
||||
<circle cx="56" cy="45" r="3" fill="#0F172A" />
|
||||
<path d="M64 40l11-5-7 10" fill="#FACC15" />
|
||||
<path d="M24 27l-4-6M72 25l5-6M22 72l-7 4M74 72l8 4" stroke="#38BDF8" strokeWidth="3" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function Avatar({ text, color }: { text?: string; color?: string }) {
|
||||
return (
|
||||
<div
|
||||
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full text-[10px] font-semibold text-white"
|
||||
style={{ backgroundColor: color ?? "#ad5a17" }}
|
||||
>
|
||||
{text ?? "coz"}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MessageCenter({
|
||||
conversations = defaultConversations,
|
||||
messages = defaultMessages,
|
||||
initialConversationId = "cozy",
|
||||
emptyTitle = "You don't have any messages yet.",
|
||||
emptyDescription = "Start a conversation when you need help.",
|
||||
showEmptyState = false,
|
||||
className,
|
||||
}: MessageCenterProps) {
|
||||
const [activeId, setActiveId] = useState(initialConversationId);
|
||||
const activeConversation = conversations.find((item) => item.id === activeId) ?? conversations[0];
|
||||
|
||||
return (
|
||||
<section className={cn("grid min-h-[520px] overflow-hidden rounded-lg bg-white shadow-sm lg:grid-cols-[310px_1fr]", className)}>
|
||||
<aside className="border-b border-gray-100 bg-white lg:border-b-0 lg:border-r">
|
||||
<div className="flex items-center gap-3 px-6 py-6">
|
||||
<InboxIcon />
|
||||
<h2 className="text-xl font-medium text-[#1f2440]">Inbox</h2>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{conversations.map((conversation) => {
|
||||
const active = conversation.id === activeId;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={conversation.id}
|
||||
type="button"
|
||||
onClick={() => setActiveId(conversation.id)}
|
||||
className={cn(
|
||||
"flex w-full gap-4 px-6 py-4 text-left transition-colors hover:bg-gray-50",
|
||||
active && "bg-gray-50"
|
||||
)}
|
||||
>
|
||||
<Avatar text={conversation.avatarText} color={conversation.avatarColor} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h3 className="truncate text-sm font-medium text-gray-900">{conversation.sender}</h3>
|
||||
<span className="shrink-0 text-[10px] text-gray-400">{conversation.time}</span>
|
||||
</div>
|
||||
<p className="mt-1 truncate text-xs text-gray-500">{conversation.preview}</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div className="flex min-h-[520px] flex-col">
|
||||
{showEmptyState ? (
|
||||
<div className="flex flex-1 flex-col items-center justify-center px-6 text-center">
|
||||
<EmptyInboxArt />
|
||||
<h3 className="mt-4 text-sm font-semibold text-[#1f2440]">{emptyTitle}</h3>
|
||||
<p className="mt-1 text-xs text-gray-500">{emptyDescription}</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center justify-between border-b border-gray-100 px-6 py-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar text={activeConversation?.avatarText} color={activeConversation?.avatarColor} />
|
||||
<h3 className="text-sm font-medium text-gray-900">{activeConversation?.sender}</h3>
|
||||
</div>
|
||||
<button type="button" className="text-gray-400 transition-colors hover:text-gray-700" aria-label="Close chat">
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 space-y-4 px-6 py-6">
|
||||
{messages.map((message) => (
|
||||
<div key={message.id} className="flex gap-3">
|
||||
<Avatar text={activeConversation?.avatarText} color={activeConversation?.avatarColor} />
|
||||
<div className="max-w-[520px]">
|
||||
<p className="text-sm leading-6 text-gray-800">{message.text}</p>
|
||||
{message.time && <p className="mt-2 text-right text-[10px] text-gray-400">{message.time}</p>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-100 px-5 py-4">
|
||||
<div className="mb-4 flex flex-wrap items-center gap-3">
|
||||
<Button variant="ghost" className="h-9 rounded-full bg-gray-50 px-5 text-xs text-gray-700">
|
||||
I have a question about my order
|
||||
</Button>
|
||||
<Button variant="ghost" className="h-9 rounded-full bg-gray-50 px-5 text-xs text-gray-700">
|
||||
I want to request a return or exchange
|
||||
</Button>
|
||||
<Button variant="icon" aria-label="More quick replies" className="h-9 w-9 border-0 bg-gray-50">
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="m9 5 7 7-7 7" />
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 rounded-full border border-gray-100 bg-white px-4 py-3">
|
||||
<button type="button" className="text-gray-500" aria-label="Attach file">
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="m21.44 11.05-8.49 8.49a5 5 0 0 1-7.07-7.07l8.49-8.49a3 3 0 1 1 4.24 4.24l-8.49 8.49a1 1 0 0 1-1.41-1.41l7.78-7.78" />
|
||||
</svg>
|
||||
</button>
|
||||
<input className="min-w-0 flex-1 text-sm outline-none placeholder:text-gray-400" placeholder="Type here" />
|
||||
<button type="button" className="text-gray-500 transition-colors hover:text-[#00AFC1]" aria-label="Send message">
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M22 2 11 13M22 2l-7 20-4-9-9-4 20-7Z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
import Image from "next/image";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface OrderStatus {
|
||||
label: string;
|
||||
location?: string;
|
||||
date?: string;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
interface OrderCompleteProduct {
|
||||
id: string;
|
||||
name: string;
|
||||
imageSrc: string;
|
||||
imageAlt?: string;
|
||||
size?: string;
|
||||
color?: string;
|
||||
quantity?: number;
|
||||
price: string;
|
||||
}
|
||||
|
||||
interface OrderCompleteProps {
|
||||
statuses?: OrderStatus[];
|
||||
orderId?: string;
|
||||
paymentMethod?: string;
|
||||
paymentLastDigits?: string;
|
||||
shippingTo?: string;
|
||||
billingTo?: string;
|
||||
products?: OrderCompleteProduct[];
|
||||
subtotal?: string;
|
||||
deliveryFee?: string;
|
||||
discount?: string;
|
||||
total?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const defaultStatuses: OrderStatus[] = [
|
||||
{
|
||||
label: "Delivered",
|
||||
location: "Gampaha",
|
||||
date: "Saturday | May 31 | 2025, 8:31 pm",
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
label: "Out for Delivery",
|
||||
location: "Gampaha",
|
||||
date: "Saturday | May 31 | 2025, 8:31 pm",
|
||||
},
|
||||
{
|
||||
label: "Have been picked and are waiting to be packed",
|
||||
date: "Monday | April 21 | 2025, 5:18 am",
|
||||
},
|
||||
{
|
||||
label: "Started preparing your order",
|
||||
date: "Sunday | April 20 | 2025, 1:04 pm",
|
||||
},
|
||||
{
|
||||
label: "Order Paid Successfully",
|
||||
date: "Sunday | April 20, 2025, 1:03 pm",
|
||||
},
|
||||
{
|
||||
label: "Order Submitted",
|
||||
date: "Sunday | April 20, 2025, 1:03 pm",
|
||||
},
|
||||
];
|
||||
|
||||
const defaultProducts: OrderCompleteProduct[] = [
|
||||
{
|
||||
id: "calla-tote",
|
||||
name: "Calla Tote Bag - Espresso Brown",
|
||||
imageSrc: "/images/tote-gallery-1.png",
|
||||
size: "Small",
|
||||
color: "Espresso Brown",
|
||||
quantity: 1,
|
||||
price: "LKR 5,600.00",
|
||||
},
|
||||
{
|
||||
id: "satin-heel",
|
||||
name: "Satin Teardrop Heel",
|
||||
imageSrc: "/images/tote-gallery-3.png",
|
||||
size: "41",
|
||||
color: "Silver",
|
||||
quantity: 1,
|
||||
price: "LKR 8,000.00",
|
||||
},
|
||||
];
|
||||
|
||||
function ActiveStatusIcon() {
|
||||
return (
|
||||
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-[#00AFC1] text-white">
|
||||
<svg className="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M16.7 5.3a1 1 0 0 1 0 1.4l-7.5 7.5a1 1 0 0 1-1.4 0L3.3 9.7a1 1 0 0 1 1.4-1.4l3.8 3.79 6.8-6.79a1 1 0 0 1 1.4 0Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function OrderDetailsCard({
|
||||
orderId,
|
||||
paymentMethod,
|
||||
paymentLastDigits,
|
||||
shippingTo,
|
||||
billingTo,
|
||||
products,
|
||||
subtotal,
|
||||
deliveryFee,
|
||||
discount,
|
||||
total,
|
||||
}: Required<Omit<OrderCompleteProps, "statuses" | "className">>) {
|
||||
return (
|
||||
<section className="w-full rounded-xl border border-gray-200 bg-white p-5 sm:p-8">
|
||||
<h2 className="text-center text-sm font-medium text-[#1f2440]">Order Details</h2>
|
||||
|
||||
<div className="mt-5 grid gap-5 rounded-md bg-gray-50 p-4 text-xs sm:grid-cols-2">
|
||||
<div>
|
||||
<p className="text-gray-500">Order ID</p>
|
||||
<p className="mt-2 font-semibold text-gray-900">{orderId}</p>
|
||||
</div>
|
||||
<div className="sm:text-right">
|
||||
<p className="text-gray-500">Payment Method</p>
|
||||
<p className="mt-2 font-semibold text-gray-900">
|
||||
<span className="mr-2 text-[#1434CB]">{paymentMethod}</span>
|
||||
**** {paymentLastDigits}
|
||||
</p>
|
||||
</div>
|
||||
<div className="border-t border-gray-200 pt-4 sm:col-span-2" />
|
||||
<div>
|
||||
<p className="text-gray-500">Shipping To</p>
|
||||
<p className="mt-2 whitespace-pre-line font-medium text-gray-900">{shippingTo}</p>
|
||||
</div>
|
||||
<div className="sm:text-right">
|
||||
<p className="text-gray-500">Billing To</p>
|
||||
<p className="mt-2 whitespace-pre-line font-medium text-gray-900">{billingTo}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="my-7 h-px bg-gray-200" />
|
||||
|
||||
<div className="mb-5 flex items-center justify-between text-sm font-medium">
|
||||
<span>Products</span>
|
||||
<span>Sub Total</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{products.map((product) => (
|
||||
<div key={product.id} className="flex items-center gap-5">
|
||||
<div className="relative h-16 w-16 shrink-0 overflow-hidden rounded bg-gray-100 sm:h-20 sm:w-20">
|
||||
<Image
|
||||
src={product.imageSrc}
|
||||
alt={product.imageAlt ?? product.name}
|
||||
fill
|
||||
sizes="80px"
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="truncate text-sm font-medium text-gray-900">{product.name}</h3>
|
||||
<p className="mt-1 text-[11px] text-gray-500">
|
||||
{product.size && <>Size: {product.size}</>}
|
||||
{product.size && product.color && <span> · </span>}
|
||||
{product.color && <>Color: {product.color}</>}
|
||||
{(product.size || product.color) && product.quantity && <span> · </span>}
|
||||
{product.quantity && <>Qty: {product.quantity}</>}
|
||||
</p>
|
||||
</div>
|
||||
<span className="shrink-0 text-xs font-medium text-gray-900 sm:text-sm">
|
||||
{product.price}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="my-7 h-px bg-gray-200" />
|
||||
|
||||
<div className="space-y-4 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span>Sub Total</span>
|
||||
<span>{subtotal}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Delivery Fee</span>
|
||||
<span>{deliveryFee}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Discount</span>
|
||||
<span className="text-red-500">{discount}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="my-7 h-px bg-gray-200" />
|
||||
|
||||
<div className="flex items-center justify-between text-xl font-medium">
|
||||
<span>Total</span>
|
||||
<span>{total}</span>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function DesktopTimeline({ statuses }: { statuses: OrderStatus[] }) {
|
||||
return (
|
||||
<ol className="hidden w-full max-w-[430px] space-y-0 lg:block">
|
||||
{statuses.map((status, index) => (
|
||||
<li key={`${status.label}-${index}`} className="relative flex gap-5 pb-12 last:pb-0">
|
||||
{index !== statuses.length - 1 && (
|
||||
<span className="absolute left-[9px] top-5 h-full w-px bg-gray-300" />
|
||||
)}
|
||||
<span className="relative z-10 mt-1">
|
||||
{status.active ? (
|
||||
<ActiveStatusIcon />
|
||||
) : (
|
||||
<span className="block h-4 w-4 rounded-full bg-gray-300" />
|
||||
)}
|
||||
</span>
|
||||
<div>
|
||||
<h3 className={cn("text-sm font-medium", status.active ? "text-[#00AFC1]" : "text-gray-900")}>
|
||||
{status.label}
|
||||
</h3>
|
||||
{status.location && <p className="mt-1 text-xs text-gray-500">{status.location}</p>}
|
||||
{status.date && <p className="mt-1 text-xs text-gray-500">{status.date}</p>}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileTimeline({ statuses }: { statuses: OrderStatus[] }) {
|
||||
const mobileStatuses = statuses
|
||||
.filter((status) => status.label !== "Order Paid Successfully")
|
||||
.slice(0, 5);
|
||||
|
||||
return (
|
||||
<ol className="flex gap-0 overflow-x-auto pb-2 lg:hidden">
|
||||
{mobileStatuses.map((status, index, visibleStatuses) => (
|
||||
<li key={`${status.label}-${index}`} className="relative min-w-[170px] flex-1 pt-9">
|
||||
{index !== visibleStatuses.length - 1 && (
|
||||
<span className="absolute left-5 right-[-20px] top-[17px] h-px bg-gray-300" />
|
||||
)}
|
||||
<span className="absolute left-0 top-2 z-10">
|
||||
{status.active ? (
|
||||
<ActiveStatusIcon />
|
||||
) : (
|
||||
<span className="block h-3.5 w-3.5 rounded-full bg-gray-300" />
|
||||
)}
|
||||
</span>
|
||||
<h3 className={cn("text-sm font-medium", status.active ? "text-[#00AFC1]" : "text-gray-900")}>
|
||||
{status.label === "Have been picked and are waiting to be packed"
|
||||
? "Waiting to be packed"
|
||||
: status.label === "Started preparing your order"
|
||||
? "Preparing your order"
|
||||
: status.label}
|
||||
</h3>
|
||||
{status.location && <p className="mt-2 text-xs text-gray-500">{status.location}</p>}
|
||||
{status.date && <p className="mt-1 text-xs text-gray-500">{status.date}</p>}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
|
||||
export default function OrderComplete({
|
||||
statuses = defaultStatuses,
|
||||
orderId = "#SDG123456BFD",
|
||||
paymentMethod = "VISA",
|
||||
paymentLastDigits = "3816",
|
||||
shippingTo = "29 Visakha Private Road,\nColombo, 00500, Sri Lanka",
|
||||
billingTo = "29 Visakha Private Road,\nColombo, 00500, Sri Lanka",
|
||||
products = defaultProducts,
|
||||
subtotal = "LKR 13,000",
|
||||
deliveryFee = "LKR 300",
|
||||
discount = "- LKR 600",
|
||||
total = "LKR 12,700",
|
||||
className,
|
||||
}: OrderCompleteProps) {
|
||||
return (
|
||||
<section className={cn("w-full bg-white px-4 py-8 text-[#1f2440] sm:px-8 lg:px-10", className)}>
|
||||
<MobileTimeline statuses={statuses} />
|
||||
|
||||
<div className="mt-8 grid gap-12 lg:mt-0 lg:grid-cols-[minmax(260px,430px)_minmax(420px,620px)] lg:justify-center">
|
||||
<DesktopTimeline statuses={statuses} />
|
||||
<OrderDetailsCard
|
||||
orderId={orderId}
|
||||
paymentMethod={paymentMethod}
|
||||
paymentLastDigits={paymentLastDigits}
|
||||
shippingTo={shippingTo}
|
||||
billingTo={billingTo}
|
||||
products={products}
|
||||
subtotal={subtotal}
|
||||
deliveryFee={deliveryFee}
|
||||
discount={discount}
|
||||
total={total}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import Image from "next/image";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface InvoiceItem {
|
||||
id: string;
|
||||
name: string;
|
||||
imageSrc: string;
|
||||
imageAlt?: string;
|
||||
size?: string;
|
||||
color?: string;
|
||||
quantity?: number;
|
||||
price: string;
|
||||
}
|
||||
|
||||
interface OrderInvoicePanelProps {
|
||||
logoSrc?: string;
|
||||
onClose?: () => void;
|
||||
orderId?: string;
|
||||
orderTime?: string;
|
||||
subtotal?: string;
|
||||
deliveryFee?: string;
|
||||
discount?: string;
|
||||
total?: string;
|
||||
shippingName?: string;
|
||||
shippingAddress?: string[];
|
||||
shippingEmail?: string;
|
||||
paymentMethod?: string;
|
||||
paymentLastDigits?: string;
|
||||
paidDate?: string;
|
||||
items?: InvoiceItem[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const defaultItems: InvoiceItem[] = [
|
||||
{
|
||||
id: "calla-tote",
|
||||
name: "Calla Tote Bag - Espresso Brown",
|
||||
imageSrc: "/images/tote-gallery-1.png",
|
||||
size: "Small",
|
||||
color: "Espresso Brown",
|
||||
quantity: 1,
|
||||
price: "LKR 5,600.00",
|
||||
},
|
||||
{
|
||||
id: "satin-heel",
|
||||
name: "Satin Teardrop Heel",
|
||||
imageSrc: "/images/tote-gallery-3.png",
|
||||
size: "41",
|
||||
color: "Silver",
|
||||
quantity: 1,
|
||||
price: "LKR 8,000.00",
|
||||
},
|
||||
];
|
||||
|
||||
function InvoiceSection({
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<section className="border-t-8 border-[#eef3f4] px-8 py-7 first:border-t-0">
|
||||
<h2 className="text-base font-semibold text-[#1f2440]">{title}</h2>
|
||||
<div className="mt-5">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default function OrderInvoicePanel({
|
||||
logoSrc = "/images/Diriya-logo.png",
|
||||
onClose,
|
||||
orderId = "11093141645882",
|
||||
orderTime = "Nov 23, 2025",
|
||||
subtotal = "LKR 13,000",
|
||||
deliveryFee = "LKR 300",
|
||||
discount = "-LKR 600",
|
||||
total = "LKR 12,700",
|
||||
shippingName = "Ashan Namal",
|
||||
shippingAddress = [
|
||||
"29 Visakha Private Road, Colombo, 00500, Sri Lanka",
|
||||
"Bambalapitiya",
|
||||
"Colombo",
|
||||
"0786276356 | ashan@gmail.com",
|
||||
],
|
||||
shippingEmail,
|
||||
paymentMethod = "VISA",
|
||||
paymentLastDigits = "3816",
|
||||
paidDate = "Paid on Nov 23,2024",
|
||||
items = defaultItems,
|
||||
className,
|
||||
}: OrderInvoicePanelProps) {
|
||||
return (
|
||||
<article className={cn("w-full max-w-[430px] overflow-hidden rounded-b-xl bg-white text-[#1f2440] shadow-sm", className)}>
|
||||
<header className="flex items-center justify-between border-b-4 border-[#159BFF] px-6 py-4">
|
||||
<Image src={logoSrc} alt="Diriya" width={92} height={36} className="h-auto w-[92px]" />
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="flex h-8 w-8 items-center justify-center text-[#159BFF] transition-colors hover:text-[#0b74c7]"
|
||||
aria-label="Close invoice"
|
||||
>
|
||||
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.7}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<InvoiceSection title="Summary">
|
||||
<dl className="space-y-4 text-sm">
|
||||
<div className="flex justify-between gap-4">
|
||||
<dt>Order ID:</dt>
|
||||
<dd className="text-right text-gray-600">{orderId}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between gap-4">
|
||||
<dt>Order Time:</dt>
|
||||
<dd className="text-right text-gray-600">{orderTime}</dd>
|
||||
</div>
|
||||
<div className="h-px bg-gray-200" />
|
||||
<div className="flex justify-between gap-4">
|
||||
<dt>Sub Total:</dt>
|
||||
<dd>{subtotal}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between gap-4">
|
||||
<dt>Delivery Fee</dt>
|
||||
<dd>{deliveryFee}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between gap-4">
|
||||
<dt>Discount</dt>
|
||||
<dd className="text-red-500">{discount}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between gap-4 font-semibold">
|
||||
<dt>Total</dt>
|
||||
<dd>{total}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</InvoiceSection>
|
||||
|
||||
<InvoiceSection title="Shipping Address">
|
||||
<div className="space-y-2 text-sm leading-6 text-gray-600">
|
||||
<p className="text-base font-semibold text-[#1f2440]">{shippingName}</p>
|
||||
{shippingAddress.map((line) => (
|
||||
<p key={line}>{line}</p>
|
||||
))}
|
||||
{shippingEmail && <p>{shippingEmail}</p>}
|
||||
</div>
|
||||
</InvoiceSection>
|
||||
|
||||
<InvoiceSection title="Payment Method">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<p className="text-lg font-bold text-[#1434CB]">
|
||||
{paymentMethod} <span className="ml-4 text-sm text-[#1f2440]">**** {paymentLastDigits}</span>
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-gray-500">{paidDate}</p>
|
||||
</div>
|
||||
<p className="text-base font-semibold">{total}</p>
|
||||
</div>
|
||||
</InvoiceSection>
|
||||
|
||||
<InvoiceSection title="Item Details">
|
||||
<div className="space-y-6">
|
||||
{items.map((item) => (
|
||||
<article key={item.id} className="flex items-center gap-4">
|
||||
<div className="relative h-16 w-16 shrink-0 overflow-hidden rounded bg-gray-100">
|
||||
<Image
|
||||
src={item.imageSrc}
|
||||
alt={item.imageAlt ?? item.name}
|
||||
fill
|
||||
sizes="64px"
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="truncate text-sm font-semibold">{item.name}</h3>
|
||||
<p className="mt-1 text-[11px] text-gray-500">
|
||||
{item.size && <>Size: {item.size}</>}
|
||||
{item.size && item.color && <span> · </span>}
|
||||
{item.color && <>Color: {item.color}</>}
|
||||
{(item.size || item.color) && item.quantity && <span> · </span>}
|
||||
{item.quantity && <>Qty: {item.quantity}</>}
|
||||
</p>
|
||||
</div>
|
||||
<p className="shrink-0 text-sm font-medium">{item.price}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</InvoiceSection>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
import Image from "next/image";
|
||||
|
||||
import Button from "@/components/ui/Button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ReceiptProduct {
|
||||
id: string;
|
||||
name: string;
|
||||
imageSrc: string;
|
||||
size?: string;
|
||||
color?: string;
|
||||
price: string;
|
||||
}
|
||||
|
||||
interface OrderSuccessReceiptProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
orderId?: string;
|
||||
paymentMethod?: string;
|
||||
paymentLastDigits?: string;
|
||||
shippingTo?: string;
|
||||
billingTo?: string;
|
||||
products?: ReceiptProduct[];
|
||||
subtotal?: string;
|
||||
deliveryFee?: string;
|
||||
discount?: string;
|
||||
total?: string;
|
||||
showPrintReceipt?: boolean;
|
||||
shopMoreLabel?: string;
|
||||
viewOrderLabel?: string;
|
||||
onPrintReceipt?: () => void;
|
||||
onShopMore?: () => void;
|
||||
onViewOrder?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const defaultProducts: ReceiptProduct[] = [
|
||||
{
|
||||
id: "calla-tote",
|
||||
name: "Calla Tote Bag - Espresso Brown",
|
||||
imageSrc: "/images/tote-gallery-1.png",
|
||||
size: "Small",
|
||||
color: "Brown",
|
||||
price: "LKR 5,000.00",
|
||||
},
|
||||
{
|
||||
id: "satin-heel",
|
||||
name: "Satin Teardrop Heel",
|
||||
imageSrc: "/images/tote-gallery-3.png",
|
||||
size: "41",
|
||||
color: "Silver",
|
||||
price: "LKR 8,000.00",
|
||||
},
|
||||
];
|
||||
|
||||
function SuccessIcon() {
|
||||
return (
|
||||
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-[#00AFC1] text-white">
|
||||
<svg className="h-9 w-9" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M10.2 2.6a2.4 2.4 0 0 1 3.6 0l1.05 1.2 1.57-.1a2.4 2.4 0 0 1 2.55 2.55l-.1 1.57 1.2 1.05a2.4 2.4 0 0 1 0 3.6l-1.2 1.05.1 1.57a2.4 2.4 0 0 1-2.55 2.55l-1.57-.1-1.05 1.2a2.4 2.4 0 0 1-3.6 0l-1.05-1.2-1.57.1a2.4 2.4 0 0 1-2.55-2.55l.1-1.57-1.2-1.05a2.4 2.4 0 0 1 0-3.6l1.2-1.05-.1-1.57A2.4 2.4 0 0 1 7.58 3.7l1.57.1 1.05-1.2Zm5.65 7.2a1 1 0 0 0-1.41-1.42l-3.32 3.32-1.56-1.56a1 1 0 1 0-1.41 1.42l2.26 2.26a1 1 0 0 0 1.42 0l4.02-4.02Z" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function OrderSuccessReceipt({
|
||||
title = "Your order is completed!",
|
||||
description = "Thank you. Your order has been successfully placed.",
|
||||
orderId = "#DSG12345abcd",
|
||||
paymentMethod = "VISA",
|
||||
paymentLastDigits = "3816",
|
||||
shippingTo = "29 Vishaka Private Road,\nColombo, 00500, Sri Lanka",
|
||||
billingTo = "29 Vishaka Private Road,\nColombo, 00500, Sri Lanka",
|
||||
products = defaultProducts,
|
||||
subtotal = "LKR 13,000",
|
||||
deliveryFee = "LKR 300",
|
||||
discount = "- LKR 600",
|
||||
total = "LKR 12,700",
|
||||
showPrintReceipt = true,
|
||||
shopMoreLabel = "Shop More",
|
||||
viewOrderLabel = "View Order",
|
||||
onPrintReceipt,
|
||||
onShopMore,
|
||||
onViewOrder,
|
||||
className,
|
||||
}: OrderSuccessReceiptProps) {
|
||||
return (
|
||||
<section className={cn("w-full max-w-[520px] text-[#1f2440]", className)}>
|
||||
<SuccessIcon />
|
||||
|
||||
<div className="mt-5 text-center">
|
||||
<h2 className="text-base font-semibold">{title}</h2>
|
||||
<p className="mt-2 text-xs text-gray-500">{description}</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 rounded-lg border border-gray-200 bg-white p-6">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<h3 className="text-sm font-semibold">Order Details</h3>
|
||||
{showPrintReceipt && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onPrintReceipt}
|
||||
className="h-9 rounded-full border-[#00AFC1] px-5 text-xs text-[#00AFC1]"
|
||||
>
|
||||
Print Receipt
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-5 grid grid-cols-2 gap-6 bg-gray-50 p-4 text-xs">
|
||||
<div>
|
||||
<p className="text-gray-500">Order ID</p>
|
||||
<p className="mt-2 font-semibold text-gray-900">{orderId}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-gray-500">Payment Method</p>
|
||||
<p className="mt-2 font-semibold text-gray-900">
|
||||
<span className="mr-2 text-[#1434CB]">{paymentMethod}</span>
|
||||
**** {paymentLastDigits}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-500">Shipping To</p>
|
||||
<p className="mt-2 whitespace-pre-line font-medium text-gray-900">{shippingTo}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-gray-500">Billing To</p>
|
||||
<p className="mt-2 whitespace-pre-line font-medium text-gray-900">{billingTo}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<div className="mb-4 flex items-center justify-between text-xs font-medium">
|
||||
<span>Products</span>
|
||||
<span>Sub Total</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5">
|
||||
{products.map((product) => (
|
||||
<div key={product.id} className="flex items-center gap-4">
|
||||
<div className="relative h-14 w-14 shrink-0 overflow-hidden rounded bg-gray-100">
|
||||
<Image src={product.imageSrc} alt={product.name} fill sizes="56px" className="object-cover" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h4 className="truncate text-xs font-semibold text-gray-900">{product.name}</h4>
|
||||
<p className="mt-1 text-[11px] text-gray-500">
|
||||
{product.size && <>Size: {product.size}</>}
|
||||
{product.size && product.color && <span> · </span>}
|
||||
{product.color && <>Color: {product.color}</>}
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-xs font-medium text-gray-900">{product.price}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 border-t border-gray-200 pt-5 text-sm">
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<span>Sub Total</span>
|
||||
<span>{subtotal}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Delivery Fee</span>
|
||||
<span>{deliveryFee}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Discount</span>
|
||||
<span className="text-red-500">{discount}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 border-t border-gray-200 pt-5">
|
||||
<div className="flex justify-between text-lg font-semibold">
|
||||
<span>Total</span>
|
||||
<span>{total}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid grid-cols-2 gap-5">
|
||||
<Button variant="ghost" onClick={onShopMore} className="h-11 rounded-full bg-gray-100 text-xs font-semibold text-gray-700 hover:bg-gray-200">
|
||||
{shopMoreLabel}
|
||||
</Button>
|
||||
<Button variant="teal" onClick={onViewOrder} className="h-11 rounded-full text-xs font-semibold">
|
||||
{viewOrderLabel}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import Button from "@/components/ui/Button";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface OrderSummaryProps {
|
||||
title?: string;
|
||||
showPromoCode?: boolean;
|
||||
promoLabel?: string;
|
||||
promoPlaceholder?: string;
|
||||
subtotal?: string;
|
||||
deliveryFee?: string;
|
||||
discountLabel?: string;
|
||||
discount?: string;
|
||||
total?: string;
|
||||
checkoutLabel?: string;
|
||||
onApplyPromo?: () => void;
|
||||
onCheckout?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function OrderSummary({
|
||||
title = "Order Summary",
|
||||
showPromoCode = true,
|
||||
promoLabel = "Promo Code",
|
||||
promoPlaceholder = "Add promo code",
|
||||
subtotal = "LKR 13,000.00",
|
||||
deliveryFee = "200.00",
|
||||
discountLabel = "Discount (-20%)",
|
||||
discount = "LKR 600.00",
|
||||
total = "LKR12,600.00",
|
||||
checkoutLabel = "Check Out",
|
||||
onApplyPromo,
|
||||
onCheckout,
|
||||
className,
|
||||
}: OrderSummaryProps) {
|
||||
return (
|
||||
<section className={cn("w-full max-w-[560px] rounded-lg bg-[#f7f7f8] p-6 text-[#1f2440]", className)}>
|
||||
{title && <h2 className="text-2xl font-medium">{title}</h2>}
|
||||
|
||||
{showPromoCode && (
|
||||
<div className="mt-8">
|
||||
<label className="text-lg font-medium">
|
||||
{promoLabel} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="mt-4 flex gap-5">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={promoPlaceholder}
|
||||
className="h-16 min-w-0 flex-1 rounded-full bg-[#ededf0] px-10 text-base text-gray-700 outline-none placeholder:text-gray-500"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onApplyPromo}
|
||||
className="h-16 min-w-[126px] rounded-full border-[#00AFC1] text-base font-semibold text-[#00AFC1]"
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-9 space-y-4 text-base">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<span className="text-gray-600">Subtotal</span>
|
||||
<span className="font-medium text-[#101936]">{subtotal}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<span className="text-gray-600">Delivery Fee</span>
|
||||
<span className="font-medium text-[#101936]">{deliveryFee}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<span className="text-gray-600">{discountLabel}</span>
|
||||
<span className="font-medium text-red-500 line-through">{discount}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="my-8 h-px bg-gray-200" />
|
||||
|
||||
<div className="flex items-center justify-between gap-4 text-lg">
|
||||
<span>Total</span>
|
||||
<span className="text-2xl font-semibold text-[#101936]">{total}</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="teal"
|
||||
onClick={onCheckout}
|
||||
className="mt-8 h-16 w-full rounded-full text-base font-medium"
|
||||
>
|
||||
{checkoutLabel}
|
||||
</Button>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface PaymentMethodRowProps {
|
||||
cardName?: string;
|
||||
expiry?: string;
|
||||
brand?: "mastercard" | "visa" | "generic";
|
||||
showRemove?: boolean;
|
||||
showEdit?: boolean;
|
||||
onRemove?: () => void;
|
||||
onEdit?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function CardBrandIcon({ brand }: { brand: "mastercard" | "visa" | "generic" }) {
|
||||
if (brand === "visa") {
|
||||
return (
|
||||
<span className="text-sm font-bold italic text-[#1434CB]" aria-hidden="true">
|
||||
VISA
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (brand === "mastercard") {
|
||||
return (
|
||||
<span className="relative block h-6 w-10" aria-hidden="true">
|
||||
<span className="absolute left-0 top-1 h-5 w-5 rounded-full bg-[#EB001B]" />
|
||||
<span className="absolute left-4 top-1 h-5 w-5 rounded-full bg-[#F79E1B] mix-blend-multiply" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return <span className="block h-6 w-10 rounded bg-gray-200" aria-hidden="true" />;
|
||||
}
|
||||
|
||||
export default function PaymentMethodRow({
|
||||
cardName = "MasterCard 6568",
|
||||
expiry = "Exp 12/2024",
|
||||
brand = "mastercard",
|
||||
showRemove = true,
|
||||
showEdit = true,
|
||||
onRemove,
|
||||
onEdit,
|
||||
className,
|
||||
}: PaymentMethodRowProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full items-center gap-4 border border-gray-200 bg-white px-5 py-5",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<CardBrandIcon brand={brand} />
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium text-gray-900">{cardName}</p>
|
||||
<p className="mt-1 text-xs text-gray-500">{expiry}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6">
|
||||
{showRemove && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRemove}
|
||||
className="text-xs font-medium text-red-500 transition-colors hover:text-red-600"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
)}
|
||||
{showEdit && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onEdit}
|
||||
className="text-xs font-medium text-[#00AFC1] transition-colors hover:text-[#008fa0]"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import Image from "next/image";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface ProductCardProps {
|
||||
imageSrc?: string;
|
||||
imageAlt?: string;
|
||||
discountBadge?: string;
|
||||
showWishlist?: boolean;
|
||||
name?: string;
|
||||
rating?: number;
|
||||
soldCount?: string;
|
||||
price?: string;
|
||||
oldPrice?: string;
|
||||
discountText?: string;
|
||||
showAddToCart?: boolean;
|
||||
addToCartLabel?: string;
|
||||
onAddToCart?: () => void;
|
||||
size?: "default" | "compact";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function ProductCard({
|
||||
imageSrc = "/images/artisan-sauce-collection.png",
|
||||
imageAlt = "",
|
||||
discountBadge,
|
||||
showWishlist = false,
|
||||
name,
|
||||
rating,
|
||||
soldCount,
|
||||
price,
|
||||
oldPrice,
|
||||
discountText,
|
||||
showAddToCart = false,
|
||||
addToCartLabel = "Add to cart",
|
||||
onAddToCart,
|
||||
size = "default",
|
||||
className,
|
||||
}: ProductCardProps) {
|
||||
const compact = size === "compact";
|
||||
|
||||
return (
|
||||
<article
|
||||
className={cn(
|
||||
"w-full overflow-hidden bg-white shadow-[0_12px_30px_rgba(15,23,42,0.08)]",
|
||||
compact ? "max-w-[215px] rounded-lg" : "max-w-[235px] rounded-2xl",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className={cn("relative overflow-hidden bg-gray-100", compact ? "aspect-[1.18/1] rounded-t-lg" : "aspect-[1.05/1] rounded-t-2xl")}>
|
||||
<Image
|
||||
src={imageSrc}
|
||||
alt={imageAlt}
|
||||
fill
|
||||
sizes={compact ? "160px" : "200px"}
|
||||
className="object-cover"
|
||||
/>
|
||||
|
||||
{discountBadge && (
|
||||
<span className={cn("absolute rounded-full bg-[#00AFC1] font-semibold text-white", compact ? "left-2 top-2 px-2 py-0.5 text-[8px]" : "left-4 top-3 px-3 py-1 text-[10px]")}>
|
||||
{discountBadge}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{showWishlist && (
|
||||
<button
|
||||
type="button"
|
||||
className={cn("absolute flex items-center justify-center rounded-full bg-black/35 text-white backdrop-blur-sm transition-colors hover:bg-black/50", compact ? "right-2 top-2 h-7 w-7" : "right-4 top-3 h-8 w-8")}
|
||||
aria-label="Add to wishlist"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78L12 21.23l8.84-8.84a5.5 5.5 0 0 0 0-7.78Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={cn(compact ? "space-y-2 p-3" : "space-y-3 p-4")}>
|
||||
{name && (
|
||||
<h3 className={cn("line-clamp-2 font-medium text-gray-800", compact ? "text-xs leading-4" : "text-sm leading-5")}>
|
||||
{name}
|
||||
</h3>
|
||||
)}
|
||||
|
||||
{(rating || soldCount) && (
|
||||
<div className={cn("flex items-center gap-1.5 text-gray-500", compact ? "text-[11px]" : "text-xs")}>
|
||||
{rating && (
|
||||
<>
|
||||
<svg className={cn("text-[#FFB800]", compact ? "h-3.5 w-3.5" : "h-4 w-4")} viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path d="m10 1.5 2.55 5.16 5.7.83-4.13 4.02.98 5.68L10 14.51l-5.1 2.68.98-5.68L1.75 7.49l5.7-.83L10 1.5Z" />
|
||||
</svg>
|
||||
<span className="font-medium text-gray-700">{rating.toFixed(1)}</span>
|
||||
</>
|
||||
)}
|
||||
{rating && soldCount && <span>|</span>}
|
||||
{soldCount && <span>{soldCount}</span>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(price || oldPrice || discountText) && (
|
||||
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-1">
|
||||
{price && <span className={cn("font-semibold text-gray-900", compact ? "text-sm" : "text-sm")}>{price}</span>}
|
||||
{oldPrice && <span className={cn("text-red-500 line-through", compact ? "text-[10px]" : "text-[11px]")}>{oldPrice}</span>}
|
||||
{discountText && <span className="text-[11px] font-medium text-red-500">{discountText}</span>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showAddToCart && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onAddToCart}
|
||||
className={cn("w-full rounded-full bg-[#e4e4e8] font-medium text-gray-700 transition-colors hover:bg-[#d8d8de]", compact ? "h-8 text-xs" : "h-11 text-sm")}
|
||||
>
|
||||
{addToCartLabel}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import AddToCartModal from "@/components/ui/AddToCartModal";
|
||||
import ProductCard, { type ProductCardProps } from "@/components/ui/ProductCard";
|
||||
|
||||
export default function ProductCardWithCartModal(props: ProductCardProps) {
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProductCard
|
||||
{...props}
|
||||
onAddToCart={() => {
|
||||
props.onAddToCart?.();
|
||||
setModalOpen(true);
|
||||
}}
|
||||
/>
|
||||
<AddToCartModal
|
||||
open={modalOpen}
|
||||
onClose={() => setModalOpen(false)}
|
||||
product={{
|
||||
imageSrc: props.imageSrc,
|
||||
imageAlt: props.imageAlt,
|
||||
name: props.name,
|
||||
price: props.price,
|
||||
oldPrice: props.oldPrice,
|
||||
discountBadge: props.discountBadge,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
import Button from "@/components/ui/Button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface GalleryImage {
|
||||
src: string;
|
||||
alt?: string;
|
||||
hasVideo?: boolean;
|
||||
}
|
||||
|
||||
interface StoreInfo {
|
||||
name: string;
|
||||
followers?: string;
|
||||
sold?: string;
|
||||
rating?: number;
|
||||
logoText?: string;
|
||||
}
|
||||
|
||||
interface ColorOption {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface SizeOption {
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface ProductDetailShowcaseProps {
|
||||
galleryImages?: GalleryImage[];
|
||||
store?: StoreInfo;
|
||||
title?: string;
|
||||
rating?: number;
|
||||
price?: string;
|
||||
oldPrice?: string;
|
||||
discountBadge?: string;
|
||||
colors?: ColorOption[];
|
||||
sizes?: SizeOption[];
|
||||
detailsTitle?: string;
|
||||
details?: string;
|
||||
showShare?: boolean;
|
||||
showWishlist?: boolean;
|
||||
showQuantity?: boolean;
|
||||
showBuyNow?: boolean;
|
||||
showAddToCart?: boolean;
|
||||
onBuyNow?: () => void;
|
||||
onAddToCart?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const defaultGalleryImages = [
|
||||
{ src: "/images/tote-gallery-1.png", alt: "Espresso brown tote bag" },
|
||||
{ src: "/images/tote-gallery-2.png", alt: "Model carrying espresso brown tote bag" },
|
||||
{ src: "/images/tote-gallery-3.png", alt: "Model standing with espresso brown tote bag" },
|
||||
{ src: "/images/tote-gallery-4.png", alt: "Espresso brown tote bag video preview", hasVideo: true },
|
||||
];
|
||||
|
||||
const defaultColors = [
|
||||
{ name: "Black", value: "#222222" },
|
||||
{ name: "Espresso", value: "#4b2139" },
|
||||
{ name: "Navy", value: "#162b3d" },
|
||||
];
|
||||
|
||||
const defaultSizes = [
|
||||
{ label: "S - 30x5cm" },
|
||||
{ label: "M - 40cm" },
|
||||
{ label: "XXL - 42cm" },
|
||||
];
|
||||
|
||||
function StarIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path d="m10 1.5 2.55 5.16 5.7.83-4.13 4.02.98 5.68L10 14.51l-5.1 2.68.98-5.68L1.75 7.49l5.7-.83L10 1.5Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function IconButton({
|
||||
label,
|
||||
children,
|
||||
}: {
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
variant="icon"
|
||||
aria-label={label}
|
||||
className="h-9 w-9 border-0 bg-gray-100 text-gray-500 hover:bg-gray-200"
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ProductDetailShowcase({
|
||||
galleryImages = defaultGalleryImages,
|
||||
store = {
|
||||
name: "Cozy",
|
||||
followers: "720 Followers",
|
||||
sold: "98k Sold",
|
||||
rating: 4.7,
|
||||
logoText: "coz",
|
||||
},
|
||||
title = "Calla Tote Bag - Espresso Brown",
|
||||
rating = 4.3,
|
||||
price = "LKR 5,000.00",
|
||||
oldPrice = "LKR 5,600.00",
|
||||
discountBadge = "-41%",
|
||||
colors = defaultColors,
|
||||
sizes = defaultSizes,
|
||||
detailsTitle = "Product: Calta Tote Bag",
|
||||
details = "The Calta Tote Bag in Espresso Brown blends everyday elegance with spacious comfort. Crafted with a smooth premium finish and designed for effortless carrying, it's perfect for work, travel, and daily essentials.",
|
||||
showShare = true,
|
||||
showWishlist = true,
|
||||
showQuantity = true,
|
||||
showBuyNow = true,
|
||||
showAddToCart = true,
|
||||
onBuyNow,
|
||||
onAddToCart,
|
||||
className,
|
||||
}: ProductDetailShowcaseProps) {
|
||||
const [selectedColor, setSelectedColor] = useState(colors[0]?.name);
|
||||
const [selectedSize, setSelectedSize] = useState(sizes[0]?.label);
|
||||
|
||||
return (
|
||||
<section className={cn("grid w-full gap-6 lg:grid-cols-[1.05fr_1fr]", className)}>
|
||||
{galleryImages.length > 0 && (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{galleryImages.map((image, index) => (
|
||||
<div
|
||||
key={`${image.src}-${index}`}
|
||||
className="relative aspect-square overflow-hidden rounded-md bg-gray-100"
|
||||
>
|
||||
<Image
|
||||
src={image.src}
|
||||
alt={image.alt ?? title}
|
||||
fill
|
||||
sizes="(max-width: 1024px) 50vw, 280px"
|
||||
className="object-cover"
|
||||
/>
|
||||
{image.hasVideo && (
|
||||
<span className="absolute left-1/2 top-1/2 flex h-14 w-14 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full bg-black/35 text-white backdrop-blur-sm">
|
||||
<svg className="h-7 w-7 translate-x-0.5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M8 5.5v13l10-6.5-10-6.5Z" />
|
||||
</svg>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-5">
|
||||
{store && (
|
||||
<div className="flex flex-col gap-4 rounded-md bg-gray-50 p-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-full bg-[#b76222] text-xs font-semibold text-white">
|
||||
{store.logoText}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<h3 className="truncate text-sm font-semibold text-gray-900">{store.name}</h3>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-gray-500">
|
||||
{store.followers && <span>{store.followers}</span>}
|
||||
{store.sold && <span>{store.sold}</span>}
|
||||
{store.rating && (
|
||||
<span className="inline-flex items-center gap-1 text-gray-700">
|
||||
<StarIcon className="h-3 w-3 text-[#FFB800]" />
|
||||
{store.rating.toFixed(1)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="teal" className="h-8 px-5 text-xs">
|
||||
Follow
|
||||
</Button>
|
||||
<Button variant="outline" className="h-8 rounded-full border-[#00AFC1] px-5 text-xs text-[#00AFC1]">
|
||||
Go to Store
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="min-w-0 space-y-2">
|
||||
{title && <h1 className="text-lg font-medium text-gray-900">{title}</h1>}
|
||||
{rating && (
|
||||
<div className="flex items-center gap-1 text-sm text-gray-700">
|
||||
<StarIcon className="h-4 w-4 text-[#FFB800]" />
|
||||
{rating.toFixed(1)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
{showShare && (
|
||||
<IconButton label="Share product">
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.5 12.5 15.5 16.5M15.5 7.5 8.5 11.5M18 8.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5ZM6 14.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5ZM18 20.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Z" />
|
||||
</svg>
|
||||
</IconButton>
|
||||
)}
|
||||
{showWishlist && (
|
||||
<IconButton label="Add to wishlist">
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78L12 21.23l8.84-8.84a5.5 5.5 0 0 0 0-7.78Z" />
|
||||
</svg>
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(price || oldPrice || discountBadge) && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-wrap items-baseline gap-2">
|
||||
{price && <span className="text-2xl font-semibold text-gray-900">{price}</span>}
|
||||
{oldPrice && <span className="text-sm text-red-500 line-through">{oldPrice}</span>}
|
||||
</div>
|
||||
{discountBadge && (
|
||||
<span className="inline-flex rounded bg-red-500 px-2 py-1 text-xs font-semibold text-white">
|
||||
{discountBadge}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{colors.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<p className="text-xs font-medium text-gray-500">Available Colors</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{colors.map((color) => (
|
||||
<button
|
||||
key={color.name}
|
||||
type="button"
|
||||
onClick={() => setSelectedColor(color.name)}
|
||||
className={cn(
|
||||
"h-16 w-16 rounded-md ring-offset-2 transition-shadow",
|
||||
selectedColor === color.name && "ring-2 ring-[#00AFC1]"
|
||||
)}
|
||||
style={{ backgroundColor: color.value }}
|
||||
aria-label={color.name}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sizes.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<p className="text-xs font-medium text-gray-500">Available Sizes</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{sizes.map((size) => (
|
||||
<Button
|
||||
key={size.label}
|
||||
type="button"
|
||||
variant="pill"
|
||||
onClick={() => setSelectedSize(size.label)}
|
||||
className={cn(
|
||||
"h-10 border-0 px-5 text-xs",
|
||||
selectedSize === size.label
|
||||
? "bg-[#07132b] text-white hover:bg-[#07132b]"
|
||||
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||
)}
|
||||
>
|
||||
{size.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
||||
{showQuantity && (
|
||||
<select className="h-11 rounded-full bg-gray-100 px-4 text-sm text-gray-700 outline-none sm:w-20">
|
||||
<option>1</option>
|
||||
<option>2</option>
|
||||
<option>3</option>
|
||||
</select>
|
||||
)}
|
||||
{showBuyNow && (
|
||||
<Button variant="teal" onClick={onBuyNow} className="h-11 flex-1 text-sm">
|
||||
Buy now
|
||||
</Button>
|
||||
)}
|
||||
{showAddToCart && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onAddToCart}
|
||||
className="h-11 flex-1 rounded-full border-[#00AFC1] text-sm text-[#00AFC1]"
|
||||
>
|
||||
Add to cart
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(detailsTitle || details) && (
|
||||
<div className="space-y-2 text-sm leading-6 text-gray-600">
|
||||
{detailsTitle && <p className="font-semibold text-gray-900">{detailsTitle}</p>}
|
||||
<p>
|
||||
<span className="font-medium text-gray-800">Shipping Type: Free Shipping</span>
|
||||
</p>
|
||||
{details && <p>{details}</p>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import Image from "next/image";
|
||||
|
||||
import Button from "@/components/ui/Button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ProductReviewCardProps {
|
||||
reviewerName?: string;
|
||||
reviewerAvatarText?: string;
|
||||
reviewedAt?: string;
|
||||
productName?: string;
|
||||
productImageSrc?: string;
|
||||
productImageAlt?: string;
|
||||
size?: string;
|
||||
color?: string;
|
||||
quantity?: number;
|
||||
price?: string;
|
||||
rating?: number;
|
||||
review?: string;
|
||||
showDelete?: boolean;
|
||||
showEdit?: boolean;
|
||||
onDelete?: () => void;
|
||||
onEdit?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function Stars({ rating = 4 }: { rating?: number }) {
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
<svg
|
||||
key={index}
|
||||
className={cn("h-6 w-6", index < rating ? "text-[#FFB800]" : "text-gray-300")}
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="m10 1.5 2.55 5.16 5.7.83-4.13 4.02.98 5.68L10 14.51l-5.1 2.68.98-5.68L1.75 7.49l5.7-.83L10 1.5Z" />
|
||||
</svg>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ProductReviewCard({
|
||||
reviewerName = "Cozy Casual",
|
||||
reviewerAvatarText = "coz",
|
||||
reviewedAt = "On - Apr 5, 2022, 10:07 AM",
|
||||
productName = "Calla Tote Bag - Espresso Brown",
|
||||
productImageSrc = "/images/tote-gallery-1.png",
|
||||
productImageAlt,
|
||||
size = "Small",
|
||||
color = "Espresso Brown",
|
||||
quantity = 1,
|
||||
price = "LKR 5,000.00",
|
||||
rating = 4,
|
||||
review = "I've been using this handbag for a few weeks now, and I'm genuinely impressed by both the quality and the attention to detail. The material feels durable yet soft to the touch, and the stitching is neat and strong, which gives me confidence that it will last a long time.",
|
||||
showDelete = true,
|
||||
showEdit = true,
|
||||
onDelete,
|
||||
onEdit,
|
||||
className,
|
||||
}: ProductReviewCardProps) {
|
||||
return (
|
||||
<article className={cn("w-full rounded-lg border border-gray-200 bg-white p-5 text-[#1f2440]", className)}>
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-[#ad5a17] text-[10px] font-semibold text-white">
|
||||
{reviewerAvatarText}
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-x-5 gap-y-1">
|
||||
<h3 className="text-sm font-semibold text-gray-900">{reviewerName}</h3>
|
||||
<span className="text-xs text-gray-500">Reviewed {reviewedAt}</span>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-3">
|
||||
{showDelete && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onDelete}
|
||||
className="h-8 rounded-full border-red-400 px-5 text-xs text-red-500"
|
||||
>
|
||||
Delete Review
|
||||
</Button>
|
||||
)}
|
||||
{showEdit && (
|
||||
<Button variant="teal" onClick={onEdit} className="h-8 rounded-full px-6 text-xs">
|
||||
Edit
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex items-center gap-5 rounded-md bg-gray-50 p-4">
|
||||
<div className="relative h-16 w-16 shrink-0 overflow-hidden rounded bg-gray-100">
|
||||
<Image
|
||||
src={productImageSrc}
|
||||
alt={productImageAlt ?? productName}
|
||||
fill
|
||||
sizes="64px"
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h4 className="text-sm font-semibold text-gray-900">{productName}</h4>
|
||||
<p className="mt-1 text-[11px] text-gray-500">
|
||||
{size && <>Size: {size}</>}
|
||||
{size && color && <span> · </span>}
|
||||
{color && <>Color: {color}</>}
|
||||
{(size || color) && quantity && <span> · </span>}
|
||||
{quantity && <>Qty: {quantity}</>}
|
||||
</p>
|
||||
</div>
|
||||
{price && <span className="shrink-0 text-sm font-medium text-gray-900">{price}</span>}
|
||||
</div>
|
||||
|
||||
<div className="mt-5">
|
||||
<Stars rating={rating} />
|
||||
{review && <p className="mt-4 text-sm leading-6 text-gray-700">{review}</p>}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface PromoCardProps {
|
||||
eyebrow?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
imageSrc?: string;
|
||||
showArrowButton?: boolean;
|
||||
arrowDirection?: "left" | "right";
|
||||
onArrowClick?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function PromoCard({
|
||||
eyebrow,
|
||||
title,
|
||||
description,
|
||||
imageSrc = "/images/promo-beauty-card.png",
|
||||
showArrowButton = false,
|
||||
arrowDirection = "right",
|
||||
onArrowClick,
|
||||
className,
|
||||
}: PromoCardProps) {
|
||||
const router = useRouter();
|
||||
|
||||
const handleArrowClick = () => {
|
||||
if (onArrowClick) {
|
||||
onArrowClick();
|
||||
return;
|
||||
}
|
||||
|
||||
if (arrowDirection === "left") {
|
||||
router.back();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<article
|
||||
className={cn(
|
||||
"relative aspect-[4/5] w-full max-w-[360px] overflow-hidden rounded-lg bg-emerald-950 text-white",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Image
|
||||
src={imageSrc}
|
||||
alt=""
|
||||
fill
|
||||
sizes="(max-width: 640px) 100vw, 360px"
|
||||
className="object-cover"
|
||||
priority
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-black/40 via-black/5 to-black/0" />
|
||||
|
||||
{(eyebrow || title || description) && (
|
||||
<div className="relative z-10 max-w-[88%] p-6">
|
||||
{eyebrow && (
|
||||
<p className="mb-2 text-xs font-medium text-white/80">{eyebrow}</p>
|
||||
)}
|
||||
{title && (
|
||||
<h2 className="text-xl font-semibold leading-tight tracking-normal sm:text-2xl">
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
{description && (
|
||||
<p className="mt-3 max-w-[230px] text-sm leading-5 text-white/80">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showArrowButton && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleArrowClick}
|
||||
className="absolute bottom-7 right-7 z-10 flex h-14 w-14 items-center justify-center rounded-full bg-black/35 text-white backdrop-blur-sm transition-colors hover:bg-black/50"
|
||||
aria-label={arrowDirection === "left" ? "Go back" : "View next"}
|
||||
>
|
||||
<svg
|
||||
className={cn("h-6 w-6", arrowDirection === "left" && "rotate-180")}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2.2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="m9 5 7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import Image from "next/image";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface RoundImageLabelProps {
|
||||
imageSrc?: string;
|
||||
imageAlt?: string;
|
||||
name?: string;
|
||||
size?: "sm" | "md" | "lg";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const sizes = {
|
||||
sm: {
|
||||
wrapper: "w-20",
|
||||
image: "h-16 w-16",
|
||||
text: "text-xs",
|
||||
},
|
||||
md: {
|
||||
wrapper: "w-28",
|
||||
image: "h-24 w-24",
|
||||
text: "text-sm",
|
||||
},
|
||||
lg: {
|
||||
wrapper: "w-32",
|
||||
image: "h-28 w-28",
|
||||
text: "text-base",
|
||||
},
|
||||
};
|
||||
|
||||
export default function RoundImageLabel({
|
||||
imageSrc = "/images/leafy-round.svg",
|
||||
imageAlt = "",
|
||||
name,
|
||||
size = "md",
|
||||
className,
|
||||
}: RoundImageLabelProps) {
|
||||
const selectedSize = sizes[size];
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col items-center gap-3", selectedSize.wrapper, className)}>
|
||||
{imageSrc && (
|
||||
<div className={cn("relative overflow-hidden rounded-full bg-gray-100", selectedSize.image)}>
|
||||
<Image
|
||||
src={imageSrc}
|
||||
alt={imageAlt || name || ""}
|
||||
fill
|
||||
sizes="112px"
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{name && (
|
||||
<p className={cn("max-w-full truncate text-center font-medium text-gray-600", selectedSize.text)}>
|
||||
{name}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import Button from "@/components/ui/Button";
|
||||
import RoundImageLabel from "@/components/ui/RoundImageLabel";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface SellerSummaryProps {
|
||||
imageSrc?: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
rating?: number;
|
||||
ratesLabel?: string;
|
||||
reviewsLabel?: string;
|
||||
followersLabel?: string;
|
||||
productsLabel?: string;
|
||||
showFollow?: boolean;
|
||||
showContact?: boolean;
|
||||
showWishlist?: boolean;
|
||||
showShare?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function StarIcon() {
|
||||
return (
|
||||
<svg className="h-4 w-4 text-[#FFB800]" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path d="m10 1.5 2.55 5.16 5.7.83-4.13 4.02.98 5.68L10 14.51l-5.1 2.68.98-5.68L1.75 7.49l5.7-.83L10 1.5Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SellerSummary({
|
||||
imageSrc = "/images/cozy-round.svg",
|
||||
name = "Cozy",
|
||||
description = "Thoughtfully handmade. Beautifully carried.",
|
||||
rating = 4.3,
|
||||
ratesLabel = "Rates",
|
||||
reviewsLabel = "112 Reviews",
|
||||
followersLabel = "12,500 Followers",
|
||||
productsLabel = "196 Products",
|
||||
showFollow = true,
|
||||
showContact = true,
|
||||
showWishlist = true,
|
||||
showShare = true,
|
||||
className,
|
||||
}: SellerSummaryProps) {
|
||||
return (
|
||||
<section className={cn("flex flex-col gap-6 sm:flex-row sm:items-center", className)}>
|
||||
<RoundImageLabel imageSrc={imageSrc} imageAlt={name} size="lg" />
|
||||
|
||||
<div className="min-w-0 flex-1 space-y-3">
|
||||
{(name || description) && (
|
||||
<div>
|
||||
{name && <h2 className="text-2xl font-medium text-[#1f2440]">{name}</h2>}
|
||||
{description && <p className="mt-1 text-base text-gray-500">{description}</p>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-gray-600">
|
||||
{rating && (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<StarIcon />
|
||||
<span className="font-medium text-gray-900">{rating.toFixed(1)}</span>
|
||||
<span>{ratesLabel}</span>
|
||||
</span>
|
||||
)}
|
||||
{reviewsLabel && <span>{reviewsLabel}</span>}
|
||||
{followersLabel && <span>{followersLabel}</span>}
|
||||
{productsLabel && <span>{productsLabel}</span>}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{showFollow && (
|
||||
<Button variant="teal" className="h-9 min-w-32 text-xs">
|
||||
Follow
|
||||
</Button>
|
||||
)}
|
||||
{showContact && (
|
||||
<Button variant="outline" className="h-9 min-w-32 rounded-full border-[#00AFC1] text-xs text-[#00AFC1]">
|
||||
Contact Seller
|
||||
</Button>
|
||||
)}
|
||||
{showWishlist && (
|
||||
<Button variant="icon" aria-label="Add seller to wishlist" className="h-9 w-9 border-0 bg-gray-100">
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78L12 21.23l8.84-8.84a5.5 5.5 0 0 0 0-7.78Z" />
|
||||
</svg>
|
||||
</Button>
|
||||
)}
|
||||
{showShare && (
|
||||
<Button variant="icon" aria-label="Share seller" className="h-9 w-9 border-0 bg-gray-100">
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.5 12.5 15.5 16.5M15.5 7.5 8.5 11.5M18 8.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5ZM6 14.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5ZM18 20.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Z" />
|
||||
</svg>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import Button from "@/components/ui/Button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface SupportAction {
|
||||
label: string;
|
||||
icon?: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
interface FaqQuestion {
|
||||
question: string;
|
||||
answer?: string;
|
||||
}
|
||||
|
||||
interface FaqCategory {
|
||||
title: string;
|
||||
questions?: FaqQuestion[];
|
||||
defaultOpen?: boolean;
|
||||
}
|
||||
|
||||
interface SupportCenterProps {
|
||||
title?: string;
|
||||
actions?: SupportAction[];
|
||||
faqTitle?: string;
|
||||
categories?: FaqCategory[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const defaultActions: SupportAction[] = [
|
||||
{
|
||||
label: "Contact Us",
|
||||
icon: (
|
||||
<svg className="h-7 w-7 text-[#3B82F6]" viewBox="0 0 48 48" fill="none" aria-hidden="true">
|
||||
<path d="M12 32 28 6l8 5-14 25-10-4Z" fill="currentColor" />
|
||||
<path d="M9 36h15l-4 7H8l1-7Z" fill="#1E3A8A" />
|
||||
<path d="M15 26l10 4" stroke="white" strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Messages",
|
||||
icon: (
|
||||
<svg className="h-7 w-7 text-[#3B82F6]" viewBox="0 0 48 48" fill="none" aria-hidden="true">
|
||||
<path d="M14 30 29 5l8 5-13 24-10-4Z" fill="currentColor" />
|
||||
<path d="M10 37h18l-5 7H9l1-7Z" fill="#1E3A8A" />
|
||||
<circle cx="34" cy="33" r="7" fill="#00AFC1" />
|
||||
<path d="M31 33h6M34 30v6" stroke="white" strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const defaultCategories: FaqCategory[] = [
|
||||
{
|
||||
title: "Ordering and Payments",
|
||||
defaultOpen: true,
|
||||
questions: [
|
||||
{
|
||||
question: "How do I save and manage my shipping address before payment?",
|
||||
answer: "Go to Shipping Address, add or edit your address, then choose the default address before checkout.",
|
||||
},
|
||||
{
|
||||
question: "I can't find my order in my account",
|
||||
answer: "Check that you are signed in with the same account used at checkout. If it still does not appear, contact support with your order ID.",
|
||||
},
|
||||
{
|
||||
question: "I can't find my order in my account",
|
||||
answer: "Some orders can take a few minutes to sync after payment. Refresh the page or check again later.",
|
||||
},
|
||||
{
|
||||
question: "How can I change my order details, such as shipping address, color, size, etc.?",
|
||||
answer: "You can request changes before the seller starts processing the order. Open the order details page and contact the seller or support.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Shipping and Delivery",
|
||||
questions: [
|
||||
{
|
||||
question: "How can I track my delivery?",
|
||||
answer: "Open your order details and check the delivery timeline. Tracking updates appear after the seller ships the item.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Returns",
|
||||
questions: [
|
||||
{
|
||||
question: "How do I request a return?",
|
||||
answer: "Open the order, choose request return, select the reason, and submit your request with photos if required.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Account Management",
|
||||
questions: [
|
||||
{
|
||||
question: "How do I update my profile information?",
|
||||
answer: "Go to Settings and update your profile, phone number, email, or saved addresses.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Refunds",
|
||||
questions: [
|
||||
{
|
||||
question: "When will I receive my refund?",
|
||||
answer: "Refunds are processed after the return is approved. Bank processing times may vary.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Rules and Policies",
|
||||
questions: [
|
||||
{
|
||||
question: "Where can I read marketplace policies?",
|
||||
answer: "You can find marketplace policies in the Rules and Policies section of the Support Center.",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default function SupportCenter({
|
||||
title = "Support Center",
|
||||
actions = defaultActions,
|
||||
faqTitle = "FAQ",
|
||||
categories = defaultCategories,
|
||||
className,
|
||||
}: SupportCenterProps) {
|
||||
const [openCategories, setOpenCategories] = useState(
|
||||
() => new Set(categories.filter((category) => category.defaultOpen).map((category) => category.title))
|
||||
);
|
||||
const [openQuestions, setOpenQuestions] = useState(new Set<string>());
|
||||
|
||||
const toggleCategory = (title: string) => {
|
||||
setOpenCategories((current) => {
|
||||
const next = new Set(current);
|
||||
if (next.has(title)) {
|
||||
next.delete(title);
|
||||
} else {
|
||||
next.add(title);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const toggleQuestion = (key: string) => {
|
||||
setOpenQuestions((current) => {
|
||||
const next = new Set(current);
|
||||
if (next.has(key)) {
|
||||
next.delete(key);
|
||||
} else {
|
||||
next.add(key);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<section className={cn("w-full bg-white text-[#1f2440]", className)}>
|
||||
<h2 className="text-lg font-medium">{title}</h2>
|
||||
|
||||
<div className="mt-5 grid gap-5 sm:grid-cols-2">
|
||||
{actions.map((action) => (
|
||||
<Button
|
||||
key={action.label}
|
||||
variant="ghost"
|
||||
onClick={action.onClick}
|
||||
className="h-16 rounded-md bg-gray-50 text-sm font-medium text-gray-900 hover:bg-gray-100"
|
||||
icon={action.icon}
|
||||
>
|
||||
{action.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h3 className="mt-8 text-base font-medium">{faqTitle}</h3>
|
||||
|
||||
<div className="mt-4 space-y-4">
|
||||
{categories.map((category) => {
|
||||
const categoryOpen = openCategories.has(category.title);
|
||||
|
||||
return (
|
||||
<section key={category.title}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleCategory(category.title)}
|
||||
className="flex h-14 w-full items-center gap-3 rounded-sm bg-gray-50 px-6 text-left text-sm font-medium text-gray-700"
|
||||
>
|
||||
<svg
|
||||
className={cn("h-4 w-4 transition-transform", categoryOpen && "rotate-90")}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="m9 5 7 7-7 7" />
|
||||
</svg>
|
||||
{category.title}
|
||||
</button>
|
||||
|
||||
{categoryOpen && category.questions && (
|
||||
<div className="bg-white px-10 py-2">
|
||||
{category.questions.map((item, index) => {
|
||||
const questionKey = `${category.title}-${index}`;
|
||||
const questionOpen = openQuestions.has(questionKey);
|
||||
|
||||
return (
|
||||
<div key={questionKey} className="border-b border-gray-100 py-5 last:border-b-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleQuestion(questionKey)}
|
||||
className="flex w-full items-center justify-between gap-4 text-left text-sm text-gray-900"
|
||||
>
|
||||
<span>{item.question}</span>
|
||||
<span className="text-lg text-gray-500">{questionOpen ? "-" : "+"}</span>
|
||||
</button>
|
||||
{questionOpen && item.answer && (
|
||||
<p className="mt-3 max-w-3xl text-sm leading-6 text-gray-500">{item.answer}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
"use client";
|
||||
|
||||
import Button from "@/components/ui/Button";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface SupportRequestFormProps {
|
||||
subjectLabel?: string;
|
||||
subjectPlaceholder?: string;
|
||||
descriptionLabel?: string;
|
||||
descriptionPlaceholder?: string;
|
||||
attachmentsLabel?: string;
|
||||
cancelLabel?: string;
|
||||
submitLabel?: string;
|
||||
showSubject?: boolean;
|
||||
showDescription?: boolean;
|
||||
showAttachments?: boolean;
|
||||
showCancel?: boolean;
|
||||
onCancel?: () => void;
|
||||
onSubmit?: (formData: FormData) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function RequiredLabel({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<label className="text-sm font-medium text-[#1f2440]">
|
||||
{children} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SupportRequestForm({
|
||||
subjectLabel = "Subject",
|
||||
subjectPlaceholder = "Enter Subject Here",
|
||||
descriptionLabel = "Description",
|
||||
descriptionPlaceholder = "Please Provide detailed information about your issue ....",
|
||||
attachmentsLabel = "Attachments",
|
||||
cancelLabel = "Cancel",
|
||||
submitLabel = "Submit",
|
||||
showSubject = true,
|
||||
showDescription = true,
|
||||
showAttachments = true,
|
||||
showCancel = true,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
className,
|
||||
}: SupportRequestFormProps) {
|
||||
return (
|
||||
<form
|
||||
className={cn("w-full rounded-lg bg-white text-[#1f2440]", className)}
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
onSubmit?.(new FormData(event.currentTarget));
|
||||
}}
|
||||
>
|
||||
<div className="space-y-5">
|
||||
{showSubject && (
|
||||
<div className="space-y-2">
|
||||
<RequiredLabel>{subjectLabel}</RequiredLabel>
|
||||
<input
|
||||
name="subject"
|
||||
type="text"
|
||||
required
|
||||
placeholder={subjectPlaceholder}
|
||||
className="h-12 w-full rounded-full bg-gray-100 px-6 text-sm outline-none placeholder:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showDescription && (
|
||||
<div className="space-y-2">
|
||||
<RequiredLabel>{descriptionLabel}</RequiredLabel>
|
||||
<textarea
|
||||
name="description"
|
||||
required
|
||||
rows={3}
|
||||
placeholder={descriptionPlaceholder}
|
||||
className="w-full resize-none rounded-2xl bg-gray-100 px-6 py-4 text-sm outline-none placeholder:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showAttachments && (
|
||||
<div className="space-y-2">
|
||||
<RequiredLabel>{attachmentsLabel}</RequiredLabel>
|
||||
<label className="flex min-h-[150px] cursor-pointer flex-col items-center justify-center rounded-md bg-gray-100 px-6 py-8 text-center transition-colors hover:bg-gray-200">
|
||||
<input name="attachments" type="file" multiple className="sr-only" />
|
||||
<span className="flex h-9 w-9 items-center justify-center rounded-full bg-white text-gray-500 shadow-sm">
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 16V4M8 8l4-4 4 4M20 16.5V19a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-2.5" />
|
||||
</svg>
|
||||
</span>
|
||||
<p className="mt-4 text-sm font-semibold text-gray-900">
|
||||
Drag & Drop files here or <span className="text-[#00AFC1] underline">Choose File</span>
|
||||
</p>
|
||||
<p className="mt-2 text-xs text-gray-500">
|
||||
Files supported: PDF, JPG, PNG | Maximum size : 5MB
|
||||
</p>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex justify-end gap-4">
|
||||
{showCancel && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={onCancel}
|
||||
className="h-11 min-w-40 rounded-full bg-gray-100 text-sm font-semibold text-gray-700 hover:bg-gray-200"
|
||||
>
|
||||
{cancelLabel}
|
||||
</Button>
|
||||
)}
|
||||
<Button type="submit" variant="teal" className="h-11 min-w-40 rounded-full text-sm font-semibold">
|
||||
{submitLabel}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface TabItem {
|
||||
label: string;
|
||||
value: string;
|
||||
content?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface TabsProps {
|
||||
tabs?: TabItem[];
|
||||
defaultValue?: string;
|
||||
onChange?: (value: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ratingBreakdown = [
|
||||
{ stars: 5, count: 50 },
|
||||
{ stars: 4, count: 4 },
|
||||
{ stars: 3, count: 8 },
|
||||
{ stars: 2, count: 8 },
|
||||
{ stars: 1, count: 0 },
|
||||
];
|
||||
|
||||
const reviews = [
|
||||
{
|
||||
name: "Shehani Randima N.",
|
||||
date: "30 Dec 2025",
|
||||
text: "I've been using this handbag for a few weeks now, and I'm genuinely impressed by both the quality and the attention to detail. The material feels durable yet soft to the touch, and the stitching is neat and strong, which gives me confidence that it will last a long time.",
|
||||
},
|
||||
{
|
||||
name: "Michelle Perera",
|
||||
date: "30 Dec 2025",
|
||||
text: "I'm genuinely impressed by both the quality and the attention to detail.",
|
||||
},
|
||||
{
|
||||
name: "Michelle Perera",
|
||||
date: "30 Dec 2025",
|
||||
text: "The material feels durable yet soft to the touch, and the stitching is neat and strong, which gives me confidence that it will last a long time even with regular use.",
|
||||
},
|
||||
{
|
||||
name: "Michelle Perera",
|
||||
date: "30 Dec 2025",
|
||||
text: "Impressed by both the quality and the attention to detail.",
|
||||
},
|
||||
{
|
||||
name: "Michelle Perera",
|
||||
date: "30 Dec 2025",
|
||||
text: "Impressed by both the quality and the attention to detail.",
|
||||
},
|
||||
];
|
||||
|
||||
function StarRow({ value = 5, size = "sm" }: { value?: number; size?: "xs" | "sm" }) {
|
||||
return (
|
||||
<div className="flex items-center gap-0.5">
|
||||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
<svg
|
||||
key={index}
|
||||
className={cn(
|
||||
size === "xs" ? "h-3 w-3" : "h-4 w-4",
|
||||
index < value ? "text-[#FFB800]" : "text-gray-300"
|
||||
)}
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="m10 1.5 2.55 5.16 5.7.83-4.13 4.02.98 5.68L10 14.51l-5.1 2.68.98-5.68L1.75 7.49l5.7-.83L10 1.5Z" />
|
||||
</svg>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ReviewsContent() {
|
||||
const maxCount = Math.max(...ratingBreakdown.map((item) => item.count));
|
||||
|
||||
return (
|
||||
<div className="grid gap-8 lg:grid-cols-[260px_1fr]">
|
||||
<div className="flex items-start gap-6">
|
||||
<div>
|
||||
<div className="text-5xl font-medium text-[#101936]">4.5</div>
|
||||
<p className="mt-3 text-xs text-gray-500">50 New Reviews</p>
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1 space-y-3">
|
||||
{ratingBreakdown.map((item) => (
|
||||
<div key={item.stars} className="flex items-center gap-2">
|
||||
<span className="w-5 text-xs font-medium text-red-500">{item.stars}</span>
|
||||
<svg className="h-3 w-3 shrink-0 text-red-500" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path d="m10 1.5 2.55 5.16 5.7.83-4.13 4.02.98 5.68L10 14.51l-5.1 2.68.98-5.68L1.75 7.49l5.7-.83L10 1.5Z" />
|
||||
</svg>
|
||||
<div className="h-1.5 flex-1 rounded-full bg-gray-100">
|
||||
<div
|
||||
className="h-full rounded-full bg-[#101936]"
|
||||
style={{ width: `${maxCount ? (item.count / maxCount) * 100 : 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-end">
|
||||
<select className="h-8 rounded-full border border-[#00AFC1] bg-white px-4 text-xs font-medium text-[#00AFC1] outline-none">
|
||||
<option>Sort</option>
|
||||
<option>Most Favourable</option>
|
||||
<option>Most Helpful</option>
|
||||
<option>Most Critical</option>
|
||||
<option>Most Recent</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{reviews.map((review, index) => (
|
||||
<article key={`${review.name}-${index}`} className="rounded-md border border-gray-100 bg-white p-4 shadow-sm">
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1">
|
||||
<h3 className="text-xs font-semibold text-gray-900">{review.name}</h3>
|
||||
<StarRow value={5} size="xs" />
|
||||
<span className="text-[11px] text-gray-500">{review.date}</span>
|
||||
</div>
|
||||
<p className="mt-2 text-xs leading-5 text-gray-700">{review.text}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const defaultTabs = [
|
||||
{
|
||||
label: "Product Description",
|
||||
value: "description",
|
||||
content: (
|
||||
<div className="space-y-4 text-xs leading-5 text-gray-700">
|
||||
<p>
|
||||
I've been using this handbag for Designed for everyday elegance,
|
||||
this structured handbag combines style with practicality-perfect for
|
||||
work, travel, or casual outings.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900">Product Information</h3>
|
||||
<ul className="mt-1 list-disc space-y-0.5 pl-4">
|
||||
<li>Item No: CK2-30781483_EP.BRW_XL</li>
|
||||
<li>Material: Premium Calf PU</li>
|
||||
<li>Closure Type: Magnetic fastening</li>
|
||||
<li>Weight: 775 g</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900">Dimensions</h3>
|
||||
<ul className="mt-1 list-disc space-y-0.5 pl-4">
|
||||
<li>Width: S - 39.5 cm</li>
|
||||
<li>Height: M - 31 cm</li>
|
||||
<li>Depth: XXL - 20 cm</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900">Care Instructions</h3>
|
||||
<p className="mt-1">
|
||||
To keep your CHARLES & KEITH product looking its best, store it
|
||||
in a cool, dry place and avoid prolonged exposure to moisture or
|
||||
direct sunlight.
|
||||
</p>
|
||||
<p className="mt-1">
|
||||
For detailed care guidance, visit our Product Care page. Few weeks
|
||||
now, and I'm genuinely impressed by both the quality and the
|
||||
attention to detail. The material feels durable yet soft to the
|
||||
touch, and the stitching is neat and strong, which gives me
|
||||
confidence that it will last a long time even with regular use.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Ratings & Reviews",
|
||||
value: "reviews",
|
||||
content: <ReviewsContent />,
|
||||
},
|
||||
];
|
||||
|
||||
export default function Tabs({
|
||||
tabs = defaultTabs,
|
||||
defaultValue,
|
||||
onChange,
|
||||
className,
|
||||
}: TabsProps) {
|
||||
const [activeTab, setActiveTab] = useState(defaultValue ?? tabs[0]?.value);
|
||||
const activeContent = tabs.find((tab) => tab.value === activeTab)?.content;
|
||||
|
||||
const handleTabClick = (value: string) => {
|
||||
setActiveTab(value);
|
||||
onChange?.(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("w-full", className)}>
|
||||
<div className="flex items-end gap-8 overflow-x-auto border-b border-gray-200">
|
||||
{tabs.map((tab) => {
|
||||
const active = activeTab === tab.value;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={tab.value}
|
||||
type="button"
|
||||
onClick={() => handleTabClick(tab.value)}
|
||||
className={cn(
|
||||
"relative h-10 shrink-0 text-sm font-medium transition-colors",
|
||||
active ? "text-[#00AFC1]" : "text-gray-500 hover:text-gray-800"
|
||||
)}
|
||||
>
|
||||
{tab.label}
|
||||
{active && (
|
||||
<span className="absolute bottom-0 left-0 h-0.5 w-full rounded-full bg-[#00AFC1]" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{activeContent && (
|
||||
<div className="pt-6">
|
||||
{activeContent}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
export interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
role: "buyer" | "admin";
|
||||
}
|
||||
|
||||
export interface Product {
|
||||
id: string;
|
||||
name: string;
|
||||
sku: string;
|
||||
price: number;
|
||||
supplier: string;
|
||||
stock: number;
|
||||
}
|
||||
|
||||
export interface Order {
|
||||
id: string;
|
||||
buyerId: string;
|
||||
items: OrderItem[];
|
||||
status: "pending" | "approved" | "shipped" | "delivered" | "cancelled";
|
||||
createdAt: string;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface OrderItem {
|
||||
productId: string;
|
||||
quantity: number;
|
||||
unitPrice: number;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||