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.
This commit is contained in:
2026-07-02 15:32:43 +05:30
commit 3d02889527
71 changed files with 12377 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"extends": ["next/core-web-vitals", "next/typescript"]
}
+35
View File
@@ -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
+8
View File
@@ -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.
+9
View File
@@ -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
+9
View File
@@ -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.
+9
View File
@@ -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
+10
View File
@@ -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;
+6827
View File
File diff suppressed because it is too large Load Diff
+30
View File
@@ -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"
}
}
+7
View File
@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

+6
View File
@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

+17
View File
@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 712 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 682 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

+49
View File
@@ -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>
);
}
+53
View File
@@ -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>
);
}
+85
View File
@@ -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>
);
}
+12
View File
@@ -0,0 +1,12 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #111111;
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}
+36
View File
@@ -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>
);
}
+180
View File
@@ -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>
);
}
+61
View File
@@ -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]">&bull;</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>
);
}
+102
View File
@@ -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>
);
}
+9
View File
@@ -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>
);
}
+107
View File
@@ -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>
);
}
+56
View File
@@ -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>
);
}
+43
View File
@@ -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>
);
}
+102
View File
@@ -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>
);
}
+75
View File
@@ -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>
);
}
+155
View File
@@ -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>
);
}
+116
View File
@@ -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>
);
}
+47
View File
@@ -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>
);
}
+224
View File
@@ -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>
);
}
+76
View File
@@ -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>
);
}
+73
View File
@@ -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>
);
}
+313
View File
@@ -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>
);
}
+91
View File
@@ -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>
);
}
+129
View File
@@ -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>
);
}
+229
View File
@@ -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>
);
}
+127
View File
@@ -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>
);
}
+112
View File
@@ -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>
);
}
+110
View File
@@ -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>
);
}
+229
View File
@@ -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>
);
}
+303
View File
@@ -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> &middot; </span>}
{product.color && <>Color: {product.color}</>}
{(product.size || product.color) && product.quantity && <span> &middot; </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>
);
}
+192
View File
@@ -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>
);
}
+193
View File
@@ -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>
);
}
+93
View File
@@ -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>
);
}
+81
View File
@@ -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>
);
}
+124
View File
@@ -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,
}}
/>
</>
);
}
+312
View File
@@ -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>
);
}
+120
View File
@@ -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>
);
}
+97
View File
@@ -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>
);
}
+61
View File
@@ -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>
);
}
+99
View File
@@ -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>
);
}
+236
View File
@@ -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>
);
}
+121
View File
@@ -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>
);
}
+236
View File
@@ -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&apos;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 &amp; 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&apos;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>
);
}
+6
View File
@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+30
View File
@@ -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;
}
+41
View File
@@ -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"
]
}