Zlr3-NFJ@NFaJlhyfcw{k^vvtGl`N9xSo**rDW4S}i
zM9{fMPWo%4wYDG~BZ18BD+}h|GQKc-g^{++3MY>}W_uq7jGHx{mwE9fZiPCoxN$+7
zrODGGJrOkcPQUB(FD5aoS4g~7#6NR^ma7-!>mHuJfY5kTe6PpNNKC9GGRiu^L31uG
z$7v`*JknQHsYB!Tm_W{a32TM099djW%5e+j0Ve_ct}IM>XLF1Ap+YvcrLV=|CKo6S
zb+9Nl3_YdKP6%Cxy@6TxZ>;4&nTneadr
z_ES90ydCev)LV!dN=#(*f}|ZORFdvkYBni^aLbUk>BajeWIOcmHP#8S)*2U~QKI%S
zyrLmtPqb&TphJ;>yAxri#;{uyk`JJqODDw%(Z=2`1uc}br^V%>j!gS)D*q*f_-qf8&D;W1dJgQMlaH5er
zN2U<%Smb7==vE}dDI8K7cKz!vs^73o9f>2sgiTzWcwY|BMYHH5%Vn7#kiw&eItCqa
zIkR2~Q}>X=Ar8W|^Ms41Fm8o6IB2_j60eOeBB1Br!boW7JnoeX6Gs)?7rW0^5psc-
zjS16yb>dFn>KPOF;imD}e!enuIniFzv}n$m2#gCCv4jM#ArwlzZ$7@9&XkFxZ4n!V
zj3dyiwW4Ki2QG{@i>yuZXQizw_OkZI^-3otXC{!(lUpJF33gI60ak;Uqitp74|B6I
zgg{b=Iz}WkhCGj1M=hu4#Aw173YxIVbISaoc
z-nLZC*6Tgivd5V`K%GxhBsp@SUU60-rfc$=wb>zdJzXS&-5(NRRodFk;Kxk!S(O(a0e7oY=E(
zAyS;Ow?6Q&XA+cnkCb{28_1N8H#?J!*$MmIwLq^*T_9-z^&UE@A(z9oGYtFy6EZef
LrJugUA?W`A8`#=m
literal 0
HcmV?d00001
diff --git a/src/app/globals.css b/src/app/globals.css
new file mode 100644
index 0000000..8b38607
--- /dev/null
+++ b/src/app/globals.css
@@ -0,0 +1,18 @@
+@import "tailwindcss";
+
+@theme inline {
+ --font-sans: Arial, Helvetica, sans-serif;
+}
+
+body {
+ color: #0f1111;
+ font-family: Arial, Helvetica, sans-serif;
+}
+
+/* Utility for clamping product titles to a fixed number of lines. */
+.line-clamp-2 {
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
new file mode 100644
index 0000000..02e8fc0
--- /dev/null
+++ b/src/app/layout.tsx
@@ -0,0 +1,33 @@
+import type { Metadata } from "next";
+import { Suspense } from "react";
+import "./globals.css";
+import { CartProvider } from "@/lib/cart-context";
+import { Header } from "@/components/Header";
+import { Footer } from "@/components/Footer";
+
+export const metadata: Metadata = {
+ title: "Amazona — Online Shopping for Electronics, Home & More",
+ description:
+ "Amazona demo storefront: shop electronics, home & kitchen, outdoors and more with fast delivery.",
+};
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+
+
+
+
+ }>
+
+
+ {children}
+
+
+
+
+ );
+}
diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx
new file mode 100644
index 0000000..dd18fbf
--- /dev/null
+++ b/src/app/not-found.tsx
@@ -0,0 +1,23 @@
+import Link from "next/link";
+
+export default function NotFound() {
+ return (
+
+
+
+ Looking for something?
+
+
+ We're sorry. The web address you entered is not a functioning
+ page on our site.
+
+
+ Go to the Amazona home page
+
+
+
+ );
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
new file mode 100644
index 0000000..e295352
--- /dev/null
+++ b/src/app/page.tsx
@@ -0,0 +1,134 @@
+import Link from "next/link";
+import { products } from "@/lib/products";
+import { ProductCard } from "@/components/ProductCard";
+
+function ShelfCard({
+ title,
+ ids,
+ cta,
+}: {
+ title: string;
+ ids: string[];
+ cta: string;
+}) {
+ const items = products.filter((p) => ids.includes(p.id)).slice(0, 4);
+ return (
+
+ {title}
+
+ {items.map((p) => (
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+ 
+ {p.title.split(",")[0]}
+
+ ))}
+
+
+ {cta}
+
+
+ );
+}
+
+export default function Home() {
+ return (
+
+ {/* Hero banner */}
+
+
+
+ Great deals, delivered fast
+
+
+ Everything you need, all in one place.
+
+
+ Shop electronics, home essentials, outdoor gear and more — with
+ free Prime delivery on eligible items.
+
+
+ Shop all deals
+
+
+
+
+ {/* Overlapping shelf cards */}
+
+
+
+
+
+
+
+
+ Sign in for the best experience
+
+
+ See personalized picks, track orders, and check out faster.
+
+
+
+
+
+
+
+ {/* Full product grid */}
+
+
+
+ Recommended for you
+
+
+ {products.map((p) => (
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/src/app/product/[id]/page.tsx b/src/app/product/[id]/page.tsx
new file mode 100644
index 0000000..312ed72
--- /dev/null
+++ b/src/app/product/[id]/page.tsx
@@ -0,0 +1,133 @@
+import Image from "next/image";
+import Link from "next/link";
+import { notFound } from "next/navigation";
+import { getProduct, products } from "@/lib/products";
+import { formatPrice } from "@/lib/format";
+import { Stars } from "@/components/Stars";
+import { BuyBox } from "@/components/BuyBox";
+import { ProductCard } from "@/components/ProductCard";
+
+export function generateStaticParams() {
+ return products.map((p) => ({ id: p.id }));
+}
+
+export default async function ProductPage({
+ params,
+}: {
+ params: Promise<{ id: string }>;
+}) {
+ const { id } = await params;
+ const product = getProduct(id);
+ if (!product) notFound();
+
+ const discount =
+ product.listPrice && product.listPrice > product.price
+ ? Math.round((1 - product.price / product.listPrice) * 100)
+ : 0;
+
+ const related = products
+ .filter((p) => p.category === product.category && p.id !== product.id)
+ .slice(0, 4);
+
+ return (
+
+
+
+
+ {/* Image */}
+
+
+
+
+ {/* Info */}
+
+
+ {product.title}
+
+
+ Visit the {product.brand} Store
+
+
+
+ {product.rating}
+
+ {product.reviews.toLocaleString("en-US")} ratings
+
+
+
+
+
+
+ {discount > 0 && (
+
+ -{discount}%
+
+ )}
+
+ {formatPrice(product.price)}
+
+
+ {product.listPrice && (
+
+ List Price:{" "}
+
+ {formatPrice(product.listPrice)}
+
+
+ )}
+
+
+
+ About this item
+
+ {product.bullets.map((b) => (
+ - {b}
+ ))}
+
+
+ {product.description}
+
+
+ {/* Buy box */}
+
+
+
+
+
+ {related.length > 0 && (
+
+
+ Products related to this item
+
+
+ {related.map((p) => (
+
+ ))}
+
+
+ )}
+
+ );
+}
diff --git a/src/app/search/page.tsx b/src/app/search/page.tsx
new file mode 100644
index 0000000..7d06887
--- /dev/null
+++ b/src/app/search/page.tsx
@@ -0,0 +1,12 @@
+import { Suspense } from "react";
+import { SearchResults } from "@/components/SearchResults";
+
+export default function SearchPage() {
+ return (
+ Loading…}
+ >
+
+
+ );
+}
diff --git a/src/components/BuyBox.tsx b/src/components/BuyBox.tsx
new file mode 100644
index 0000000..0fbdc41
--- /dev/null
+++ b/src/components/BuyBox.tsx
@@ -0,0 +1,85 @@
+"use client";
+
+import Link from "next/link";
+import { useRouter } from "next/navigation";
+import { useState } from "react";
+import type { Product } from "@/lib/products";
+import { useCart } from "@/lib/cart-context";
+import { formatPrice } from "@/lib/format";
+
+export function BuyBox({ product }: { product: Product }) {
+ const { addItem } = useCart();
+ const router = useRouter();
+ const [quantity, setQuantity] = useState(1);
+ const [added, setAdded] = useState(false);
+
+ function handleAdd() {
+ addItem(product, quantity);
+ setAdded(true);
+ setTimeout(() => setAdded(false), 2000);
+ }
+
+ function handleBuyNow() {
+ addItem(product, quantity);
+ router.push("/cart");
+ }
+
+ return (
+
+
+ {formatPrice(product.price)}
+
+ {product.prime && (
+
+ prime{" "}
+ FREE delivery
+
+ )}
+
+ {product.inStock ? (
+ In Stock
+ ) : (
+ Currently unavailable
+ )}
+
+
+ {product.inStock && (
+ <>
+
+
+
+
+
+ Go to Cart
+
+ >
+ )}
+
+ );
+}
diff --git a/src/components/CartView.tsx b/src/components/CartView.tsx
new file mode 100644
index 0000000..4fe5db9
--- /dev/null
+++ b/src/components/CartView.tsx
@@ -0,0 +1,138 @@
+"use client";
+
+import Image from "next/image";
+import Link from "next/link";
+import { useCart } from "@/lib/cart-context";
+import { formatPrice } from "@/lib/format";
+
+export function CartView() {
+ const { items, subtotal, count, setQuantity, removeItem } = useCart();
+
+ if (items.length === 0) {
+ return (
+
+
+
+ Your Amazona Cart is empty
+
+
+ Check out our recommendations to get started.
+
+
+ Continue shopping
+
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Line items */}
+
+
+
+ Shopping Cart
+
+ Price
+
+
+ {items.map(({ product, quantity }) => (
+
+
+
+
+
+
+ {product.title}
+
+
+ {product.inStock ? (
+ In Stock
+ ) : (
+ Out of stock
+ )}
+
+ {product.prime && (
+
+
+ prime
+ {" "}
+ FREE delivery
+
+ )}
+
+
+ |
+
+
+
+
+ {formatPrice(product.price * quantity)}
+
+
+ ))}
+
+
+ Subtotal ({count} item{count === 1 ? "" : "s"}):{" "}
+ {formatPrice(subtotal)}
+
+
+
+ {/* Summary */}
+
+
+ Subtotal ({count} item{count === 1 ? "" : "s"}):{" "}
+ {formatPrice(subtotal)}
+
+
+
+ Proceed to checkout
+
+
+
+
+ );
+}
diff --git a/src/components/CheckoutView.tsx b/src/components/CheckoutView.tsx
new file mode 100644
index 0000000..259ed9b
--- /dev/null
+++ b/src/components/CheckoutView.tsx
@@ -0,0 +1,207 @@
+"use client";
+
+import Image from "next/image";
+import Link from "next/link";
+import { useRouter } from "next/navigation";
+import { useState } from "react";
+import { useCart } from "@/lib/cart-context";
+import { formatPrice } from "@/lib/format";
+
+export function CheckoutView() {
+ const { items, subtotal, count, clear } = useCart();
+ const router = useRouter();
+ const [placed, setPlaced] = useState(false);
+ const [orderId, setOrderId] = useState("");
+
+ const shipping = subtotal >= 25 || subtotal === 0 ? 0 : 5.99;
+ const tax = Math.round(subtotal * 0.0875 * 100) / 100;
+ const total = subtotal + shipping + tax;
+
+ function placeOrder(e: React.FormEvent) {
+ e.preventDefault();
+ const id = `AMZ-${Math.random().toString(36).slice(2, 8).toUpperCase()}`;
+ setOrderId(id);
+ setPlaced(true);
+ clear();
+ }
+
+ if (placed) {
+ return (
+
+
+
+
+ Order placed, thank you!
+
+
+ Confirmation will be sent to your email. Your order number is{" "}
+ {orderId}.
+
+
+ Continue shopping
+
+
+
+ );
+ }
+
+ if (items.length === 0) {
+ return (
+
+
+ Your cart is empty
+
+ Continue shopping
+
+
+
+ );
+ }
+
+ return (
+
+
+ Checkout
+
+
+ {/* Form */}
+
+
+ {/* Order summary */}
+
+
+
+ By placing your order you agree to our demo terms.
+
+
+
+ Order Summary
+
+
+
+
+
+
+ - Order total:
+ - {formatPrice(total)}
+
+
+
+
+ {items.map(({ product, quantity }) => (
+ -
+
+
+ {product.title.split(",")[0]}
+
+ ×{quantity}
+
+ ))}
+
+
+
+
+ );
+}
+
+function Field({
+ label,
+ name,
+ required,
+ placeholder,
+}: {
+ label: string;
+ name: string;
+ required?: boolean;
+ placeholder?: string;
+}) {
+ return (
+
+ );
+}
+
+function Row({ label, value }: { label: string; value: string }) {
+ return (
+
+ {label}
+ {value}
+
+ );
+}
diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx
new file mode 100644
index 0000000..483dbc3
--- /dev/null
+++ b/src/components/Footer.tsx
@@ -0,0 +1,78 @@
+import Link from "next/link";
+
+const columns = [
+ {
+ title: "Get to Know Us",
+ links: ["About Us", "Careers", "Press Releases", "Our Community"],
+ },
+ {
+ title: "Make Money with Us",
+ links: [
+ "Sell products",
+ "Become an Affiliate",
+ "Advertise Your Products",
+ "Self-Publish",
+ ],
+ },
+ {
+ title: "Payment Products",
+ links: [
+ "Business Card",
+ "Shop with Points",
+ "Reload Your Balance",
+ "Currency Converter",
+ ],
+ },
+ {
+ title: "Let Us Help You",
+ links: [
+ "Your Account",
+ "Your Orders",
+ "Shipping Rates & Policies",
+ "Help",
+ ],
+ },
+];
+
+export function Footer() {
+ return (
+
+ );
+}
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
new file mode 100644
index 0000000..bc30ba7
--- /dev/null
+++ b/src/components/Header.tsx
@@ -0,0 +1,141 @@
+"use client";
+
+import Link from "next/link";
+import { useRouter, useSearchParams } from "next/navigation";
+import { useState } from "react";
+import { categories } from "@/lib/products";
+import { useCart } from "@/lib/cart-context";
+
+export function Header() {
+ const router = useRouter();
+ const params = useSearchParams();
+ const { count } = useCart();
+ const [query, setQuery] = useState(params.get("q") ?? "");
+ const [category, setCategory] = useState(params.get("category") ?? "All");
+
+ function submitSearch(e: React.FormEvent) {
+ e.preventDefault();
+ const sp = new URLSearchParams();
+ if (query.trim()) sp.set("q", query.trim());
+ if (category !== "All") sp.set("category", category);
+ router.push(`/search${sp.toString() ? `?${sp}` : ""}`);
+ }
+
+ return (
+
+ );
+}
diff --git a/src/components/ProductCard.tsx b/src/components/ProductCard.tsx
new file mode 100644
index 0000000..92a888a
--- /dev/null
+++ b/src/components/ProductCard.tsx
@@ -0,0 +1,78 @@
+"use client";
+
+import Image from "next/image";
+import Link from "next/link";
+import type { Product } from "@/lib/products";
+import { useCart } from "@/lib/cart-context";
+import { formatPrice } from "@/lib/format";
+import { Stars } from "./Stars";
+
+export function ProductCard({ product }: { product: Product }) {
+ const { addItem } = useCart();
+ const discount =
+ product.listPrice && product.listPrice > product.price
+ ? Math.round((1 - product.price / product.listPrice) * 100)
+ : 0;
+
+ return (
+
+
+
+
+
+ {product.title}
+
+
+
+
+
+
+ {formatPrice(product.price)}
+
+ {product.listPrice && (
+
+ {formatPrice(product.listPrice)}
+
+ )}
+ {discount > 0 && (
+
+ -{discount}%
+
+ )}
+
+ {product.prime && (
+
+ prime
+ FREE delivery
+
+ )}
+
+ {product.inStock ? (
+ In Stock
+ ) : (
+ Currently unavailable
+ )}
+
+
+
+ );
+}
diff --git a/src/components/SearchResults.tsx b/src/components/SearchResults.tsx
new file mode 100644
index 0000000..6a4a31a
--- /dev/null
+++ b/src/components/SearchResults.tsx
@@ -0,0 +1,142 @@
+"use client";
+
+import { useMemo, useState } from "react";
+import { useSearchParams } from "next/navigation";
+import { categories, searchProducts } from "@/lib/products";
+import { ProductCard } from "@/components/ProductCard";
+import { Stars } from "@/components/Stars";
+
+type SortKey = "featured" | "priceLow" | "priceHigh" | "rating";
+
+export function SearchResults() {
+ const params = useSearchParams();
+ const q = params.get("q") ?? "";
+ const initialCategory = params.get("category") ?? "All";
+
+ const [category, setCategory] = useState(initialCategory);
+ const [sort, setSort] = useState("featured");
+ const [minRating, setMinRating] = useState(0);
+
+ // Reset filters if the query string changes (new search from the header).
+ const key = `${q}|${initialCategory}`;
+ const [prevKey, setPrevKey] = useState(key);
+ if (key !== prevKey) {
+ setPrevKey(key);
+ setCategory(initialCategory);
+ setSort("featured");
+ setMinRating(0);
+ }
+
+ const results = useMemo(() => {
+ const base = searchProducts(q, category).filter(
+ (p) => p.rating >= minRating,
+ );
+ const sorted = [...base];
+ switch (sort) {
+ case "priceLow":
+ sorted.sort((a, b) => a.price - b.price);
+ break;
+ case "priceHigh":
+ sorted.sort((a, b) => b.price - a.price);
+ break;
+ case "rating":
+ sorted.sort((a, b) => b.rating - a.rating);
+ break;
+ }
+ return sorted;
+ }, [q, category, sort, minRating]);
+
+ return (
+
+
+
+ {results.length} result{results.length === 1 ? "" : "s"}
+ {q && (
+ <>
+ {" "}
+ for "{q}"
+ >
+ )}
+
+
+
+
+
+ {/* Filters sidebar */}
+
+
+ {/* Results grid */}
+
+ {results.length === 0 ? (
+
+ No results found
+
+ Try a different search term or category.
+
+
+ ) : (
+
+ {results.map((p) => (
+
+ ))}
+
+ )}
+
+
+
+ );
+}
diff --git a/src/components/Stars.tsx b/src/components/Stars.tsx
new file mode 100644
index 0000000..de43f4e
--- /dev/null
+++ b/src/components/Stars.tsx
@@ -0,0 +1,60 @@
+export function Stars({
+ rating,
+ reviews,
+ size = "sm",
+}: {
+ rating: number;
+ reviews?: number;
+ size?: "sm" | "md";
+}) {
+ const full = Math.floor(rating);
+ const hasHalf = rating - full >= 0.25 && rating - full < 0.75;
+ const rounded = rating - full >= 0.75 ? full + 1 : full;
+ const dim = size === "md" ? 18 : 14;
+
+ return (
+
+
+ {Array.from({ length: 5 }).map((_, i) => {
+ const isFull = i < (hasHalf ? full : rounded);
+ const isHalf = hasHalf && i === full;
+ return (
+
+ );
+ })}
+
+ {reviews !== undefined && (
+
+ {reviews.toLocaleString("en-US")}
+
+ )}
+
+ );
+}
diff --git a/src/lib/cart-context.tsx b/src/lib/cart-context.tsx
new file mode 100644
index 0000000..f922b62
--- /dev/null
+++ b/src/lib/cart-context.tsx
@@ -0,0 +1,136 @@
+"use client";
+
+import {
+ createContext,
+ useContext,
+ useEffect,
+ useMemo,
+ useReducer,
+} from "react";
+import type { Product } from "@/lib/products";
+
+export type CartItem = {
+ product: Product;
+ quantity: number;
+};
+
+type CartState = {
+ items: CartItem[];
+};
+
+type CartAction =
+ | { type: "add"; product: Product; quantity?: number }
+ | { type: "remove"; id: string }
+ | { type: "setQuantity"; id: string; quantity: number }
+ | { type: "clear" }
+ | { type: "hydrate"; items: CartItem[] };
+
+const STORAGE_KEY = "amazona-cart";
+
+function reducer(state: CartState, action: CartAction): CartState {
+ switch (action.type) {
+ case "hydrate":
+ return { items: action.items };
+ case "add": {
+ const qty = action.quantity ?? 1;
+ const existing = state.items.find(
+ (i) => i.product.id === action.product.id,
+ );
+ if (existing) {
+ return {
+ items: state.items.map((i) =>
+ i.product.id === action.product.id
+ ? { ...i, quantity: i.quantity + qty }
+ : i,
+ ),
+ };
+ }
+ return {
+ items: [...state.items, { product: action.product, quantity: qty }],
+ };
+ }
+ case "remove":
+ return { items: state.items.filter((i) => i.product.id !== action.id) };
+ case "setQuantity": {
+ if (action.quantity <= 0) {
+ return { items: state.items.filter((i) => i.product.id !== action.id) };
+ }
+ return {
+ items: state.items.map((i) =>
+ i.product.id === action.id
+ ? { ...i, quantity: action.quantity }
+ : i,
+ ),
+ };
+ }
+ case "clear":
+ return { items: [] };
+ default:
+ return state;
+ }
+}
+
+type CartContextValue = {
+ items: CartItem[];
+ count: number;
+ subtotal: number;
+ addItem: (product: Product, quantity?: number) => void;
+ removeItem: (id: string) => void;
+ setQuantity: (id: string, quantity: number) => void;
+ clear: () => void;
+};
+
+const CartContext = createContext(null);
+
+export function CartProvider({ children }: { children: React.ReactNode }) {
+ const [state, dispatch] = useReducer(reducer, { items: [] });
+
+ // Hydrate from localStorage on mount.
+ useEffect(() => {
+ try {
+ const raw = localStorage.getItem(STORAGE_KEY);
+ if (raw) {
+ const items = JSON.parse(raw) as CartItem[];
+ if (Array.isArray(items)) dispatch({ type: "hydrate", items });
+ }
+ } catch {
+ // ignore malformed storage
+ }
+ }, []);
+
+ // Persist on change.
+ useEffect(() => {
+ try {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(state.items));
+ } catch {
+ // ignore quota / unavailable storage
+ }
+ }, [state.items]);
+
+ const value = useMemo(() => {
+ const count = state.items.reduce((n, i) => n + i.quantity, 0);
+ const subtotal = state.items.reduce(
+ (sum, i) => sum + i.product.price * i.quantity,
+ 0,
+ );
+ return {
+ items: state.items,
+ count,
+ subtotal,
+ addItem: (product, quantity) =>
+ dispatch({ type: "add", product, quantity }),
+ removeItem: (id) => dispatch({ type: "remove", id }),
+ setQuantity: (id, quantity) =>
+ dispatch({ type: "setQuantity", id, quantity }),
+ clear: () => dispatch({ type: "clear" }),
+ };
+ }, [state.items]);
+
+ return {children};
+}
+
+export function useCart() {
+ const ctx = useContext(CartContext);
+ if (!ctx) throw new Error("useCart must be used within a CartProvider");
+ return ctx;
+}
diff --git a/src/lib/format.ts b/src/lib/format.ts
new file mode 100644
index 0000000..4b7d940
--- /dev/null
+++ b/src/lib/format.ts
@@ -0,0 +1,6 @@
+export function formatPrice(value: number): string {
+ return value.toLocaleString("en-US", {
+ style: "currency",
+ currency: "USD",
+ });
+}
diff --git a/src/lib/products.ts b/src/lib/products.ts
new file mode 100644
index 0000000..e4032bd
--- /dev/null
+++ b/src/lib/products.ts
@@ -0,0 +1,297 @@
+export type Product = {
+ id: string;
+ title: string;
+ brand: string;
+ category: string;
+ price: number;
+ listPrice?: number;
+ rating: number;
+ reviews: number;
+ prime: boolean;
+ image: string;
+ images?: string[];
+ description: string;
+ bullets: string[];
+ inStock: boolean;
+};
+
+// Seed catalog. Images are procedurally generated SVG placeholders (see
+// /public/products) so the app ships with no third-party/copyrighted assets.
+export const products: Product[] = [
+ {
+ id: "echo-mini-speaker",
+ title: "Nova Mini Smart Speaker with Voice Assistant (Charcoal)",
+ brand: "Nova",
+ category: "Electronics",
+ price: 34.99,
+ listPrice: 49.99,
+ rating: 4.6,
+ reviews: 128432,
+ prime: true,
+ image: "/products/speaker.svg",
+ description:
+ "Compact smart speaker with rich, room-filling sound and a built-in voice assistant. Ask questions, play music, set timers, and control compatible smart-home devices hands-free.",
+ bullets: [
+ "Crisp vocals and balanced bass in a compact design",
+ "Hands-free voice assistant built in",
+ "Controls compatible lights, plugs, and thermostats",
+ "Privacy controls with a one-press mic-off button",
+ ],
+ inStock: true,
+ },
+ {
+ id: "aurora-tablet-10",
+ title: 'Aurora Tab 10" HD Tablet, 64GB, Long Battery Life (Slate)',
+ brand: "Aurora",
+ category: "Electronics",
+ price: 89.99,
+ listPrice: 119.99,
+ rating: 4.4,
+ reviews: 54210,
+ prime: true,
+ image: "/products/tablet.svg",
+ description:
+ "A vivid 10-inch HD display, all-day battery, and 64GB of storage (expandable) make the Aurora Tab perfect for streaming, reading, and browsing on the go.",
+ bullets: [
+ '10" 1080p HD display',
+ "Up to 12 hours of battery life",
+ "64GB storage, expandable via microSD",
+ "Fast quad-core processor",
+ ],
+ inStock: true,
+ },
+ {
+ id: "pulse-wireless-earbuds",
+ title: "Pulse Wireless Noise-Cancelling Earbuds with Charging Case",
+ brand: "Pulse",
+ category: "Electronics",
+ price: 59.99,
+ listPrice: 99.99,
+ rating: 4.5,
+ reviews: 98123,
+ prime: true,
+ image: "/products/earbuds.svg",
+ description:
+ "Immersive sound with active noise cancellation, a secure comfortable fit, and up to 30 hours of total playtime with the compact charging case.",
+ bullets: [
+ "Active noise cancellation",
+ "Up to 30 hours total playtime",
+ "Sweat and water resistant (IPX4)",
+ "Bluetooth 5.3 with instant pairing",
+ ],
+ inStock: true,
+ },
+ {
+ id: "everbrew-coffee-maker",
+ title: "EverBrew 12-Cup Programmable Coffee Maker, Stainless Steel",
+ brand: "EverBrew",
+ category: "Home & Kitchen",
+ price: 44.95,
+ listPrice: 64.95,
+ rating: 4.7,
+ reviews: 32211,
+ prime: true,
+ image: "/products/coffee.svg",
+ description:
+ "Wake up to fresh coffee with a 24-hour programmable timer, keep-warm plate, and a 12-cup glass carafe. Brew-strength control lets you dial in the perfect cup.",
+ bullets: [
+ "12-cup glass carafe",
+ "24-hour programmable brew timer",
+ "Adjustable brew strength",
+ "Auto shut-off keep-warm plate",
+ ],
+ inStock: true,
+ },
+ {
+ id: "trailmax-backpack",
+ title: "TrailMax 35L Water-Resistant Hiking Backpack (Forest Green)",
+ brand: "TrailMax",
+ category: "Outdoors",
+ price: 39.99,
+ rating: 4.6,
+ reviews: 18422,
+ prime: true,
+ image: "/products/backpack.svg",
+ description:
+ "A rugged 35L pack with padded straps, a ventilated back panel, and plenty of pockets for day hikes and weekend trips. Water-resistant ripstop fabric keeps gear dry.",
+ bullets: [
+ "35L capacity with multiple compartments",
+ "Water-resistant ripstop fabric",
+ "Padded, adjustable shoulder straps",
+ "Ventilated mesh back panel",
+ ],
+ inStock: true,
+ },
+ {
+ id: "lumen-desk-lamp",
+ title: "Lumen LED Desk Lamp with Wireless Charging & USB Port",
+ brand: "Lumen",
+ category: "Home & Kitchen",
+ price: 27.99,
+ listPrice: 42.99,
+ rating: 4.5,
+ reviews: 9231,
+ prime: true,
+ image: "/products/lamp.svg",
+ description:
+ "A sleek desk lamp with five color temperatures, stepless dimming, a built-in wireless charging pad, and a USB charging port to keep your desk tidy.",
+ bullets: [
+ "5 color modes, stepless dimming",
+ "Built-in 10W wireless charging pad",
+ "Extra USB-A charging port",
+ "Memory function remembers your setting",
+ ],
+ inStock: true,
+ },
+ {
+ id: "corecomfort-office-chair",
+ title: "CoreComfort Ergonomic Mesh Office Chair with Lumbar Support",
+ brand: "CoreComfort",
+ category: "Home & Kitchen",
+ price: 129.0,
+ listPrice: 189.0,
+ rating: 4.3,
+ reviews: 21044,
+ prime: false,
+ image: "/products/chair.svg",
+ description:
+ "Stay comfortable through long workdays with adjustable lumbar support, breathable mesh, a tilt-lock recline, and height-adjustable armrests.",
+ bullets: [
+ "Adjustable lumbar support",
+ "Breathable mesh back",
+ "Tilt-lock recline",
+ "Height-adjustable armrests",
+ ],
+ inStock: true,
+ },
+ {
+ id: "novelist-ereader",
+ title: "PagePro E-Reader, 6.8\" Glare-Free Display, Adjustable Warm Light",
+ brand: "PagePro",
+ category: "Electronics",
+ price: 109.99,
+ listPrice: 139.99,
+ rating: 4.8,
+ reviews: 76540,
+ prime: true,
+ image: "/products/ereader.svg",
+ description:
+ "Read comfortably day or night with a 6.8-inch glare-free display, adjustable warm light, and weeks of battery life on a single charge.",
+ bullets: [
+ '6.8" 300ppi glare-free display',
+ "Adjustable warm light",
+ "Weeks of battery on one charge",
+ "Waterproof (IPX8)",
+ ],
+ inStock: true,
+ },
+ {
+ id: "fitpulse-smartwatch",
+ title: "FitPulse Smartwatch with Heart Rate, GPS & Sleep Tracking",
+ brand: "FitPulse",
+ category: "Electronics",
+ price: 79.99,
+ listPrice: 129.99,
+ rating: 4.2,
+ reviews: 43120,
+ prime: true,
+ image: "/products/watch.svg",
+ description:
+ "Track workouts, heart rate, sleep, and more with built-in GPS and a bright always-on display. Up to 7 days of battery life and 50+ sport modes.",
+ bullets: [
+ "Built-in GPS and heart-rate monitor",
+ "50+ sport modes",
+ "Up to 7-day battery life",
+ "Water resistant to 50m",
+ ],
+ inStock: true,
+ },
+ {
+ id: "breezecool-fan",
+ title: "BreezeCool 3-Speed Oscillating Tower Fan with Remote",
+ brand: "BreezeCool",
+ category: "Home & Kitchen",
+ price: 49.99,
+ rating: 4.4,
+ reviews: 15320,
+ prime: true,
+ image: "/products/fan.svg",
+ description:
+ "Quiet, powerful cooling with three speeds, wide oscillation, a programmable timer, and a handy remote control. Slim tower design fits any room.",
+ bullets: [
+ "3 speeds with wide oscillation",
+ "Programmable timer",
+ "Remote control included",
+ "Quiet sleep mode",
+ ],
+ inStock: false,
+ },
+ {
+ id: "chefsteel-knife-set",
+ title: "ChefSteel 15-Piece Stainless Steel Knife Block Set",
+ brand: "ChefSteel",
+ category: "Home & Kitchen",
+ price: 69.99,
+ listPrice: 99.99,
+ rating: 4.7,
+ reviews: 28765,
+ prime: true,
+ image: "/products/knives.svg",
+ description:
+ "A complete 15-piece set of high-carbon stainless steel knives with a modern wood block, built-in sharpener, and ergonomic handles.",
+ bullets: [
+ "15-piece high-carbon stainless set",
+ "Built-in knife sharpener",
+ "Ergonomic non-slip handles",
+ "Modern hardwood storage block",
+ ],
+ inStock: true,
+ },
+ {
+ id: "playmax-controller",
+ title: "PlayMax Wireless Game Controller, Low-Latency (Midnight Blue)",
+ brand: "PlayMax",
+ category: "Video Games",
+ price: 42.99,
+ listPrice: 59.99,
+ rating: 4.5,
+ reviews: 37211,
+ prime: true,
+ image: "/products/controller.svg",
+ description:
+ "Precision analog sticks, textured triggers, and low-latency wireless connection for console and PC. Rechargeable battery lasts up to 20 hours.",
+ bullets: [
+ "Low-latency 2.4GHz wireless",
+ "Textured triggers and grips",
+ "Up to 20-hour rechargeable battery",
+ "Works with PC and major consoles",
+ ],
+ inStock: true,
+ },
+];
+
+export const categories = [
+ "All",
+ "Electronics",
+ "Home & Kitchen",
+ "Outdoors",
+ "Video Games",
+];
+
+export function getProduct(id: string): Product | undefined {
+ return products.find((p) => p.id === id);
+}
+
+export function searchProducts(query: string, category = "All"): Product[] {
+ const q = query.trim().toLowerCase();
+ return products.filter((p) => {
+ const matchesCategory = category === "All" || p.category === category;
+ const matchesQuery =
+ q === "" ||
+ p.title.toLowerCase().includes(q) ||
+ p.brand.toLowerCase().includes(q) ||
+ p.category.toLowerCase().includes(q) ||
+ p.description.toLowerCase().includes(q);
+ return matchesCategory && matchesQuery;
+ });
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..cf9c65d
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,34 @@
+{
+ "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",
+ "**/*.mts"
+ ],
+ "exclude": ["node_modules"]
+}
From 459d530dca4e537e05fbb04f5b8978fdf154414f Mon Sep 17 00:00:00 2001
From: hellokij <300807055+hellokij@users.noreply.github.com>
Date: Mon, 20 Jul 2026 13:59:28 +0000
Subject: [PATCH 2/4] Apply review + simplify feedback
- Extract discountPercent() and MAX_CART_QTY to lib/format; dedupe
discount computation across ProductCard and product detail
- Add shared QuantitySelect component; dedupe quantity |