diff --git a/apps/storefront/next-env.d.ts b/apps/storefront/next-env.d.ts index 830fb59..c4b7818 100644 --- a/apps/storefront/next-env.d.ts +++ b/apps/storefront/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -/// +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/storefront/next.config.js b/apps/storefront/next.config.js index c0ee747..30bbef1 100644 --- a/apps/storefront/next.config.js +++ b/apps/storefront/next.config.js @@ -1,6 +1,20 @@ +const path = require("path"); + /** @type {import('next').NextConfig} */ const nextConfig = { transpilePackages: ["@repo/convex", "@repo/types", "@repo/utils"], + turbopack: { + root: path.join(__dirname, "..", ".."), + }, + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "res.cloudinary.com", + pathname: "/**", + }, + ], + }, // PPR: enable when using Next.js canary. Uncomment and add experimental_ppr to PDP page: // experimental: { ppr: "incremental" }, }; diff --git a/apps/storefront/src/app/shop/layout.tsx b/apps/storefront/src/app/shop/layout.tsx new file mode 100644 index 0000000..81c7e01 --- /dev/null +++ b/apps/storefront/src/app/shop/layout.tsx @@ -0,0 +1,12 @@ +import { Suspense } from "react"; +import { ShopProductGridSkeleton } from "@/components/shop/state/ShopProductGridSkeleton"; + +export default function ShopLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + }>{children} + ); +} diff --git a/apps/storefront/src/components/layout/header/HeaderSearchBar.tsx b/apps/storefront/src/components/layout/header/HeaderSearchBar.tsx index 0dcf5a4..1ad7239 100644 --- a/apps/storefront/src/components/layout/header/HeaderSearchBar.tsx +++ b/apps/storefront/src/components/layout/header/HeaderSearchBar.tsx @@ -6,7 +6,6 @@ import { useProductSearch, useClickOutside, SEARCH_CATEGORIES, - MIN_SEARCH_LENGTH, } from "@/lib/search"; import type { SearchCategory } from "@/lib/search"; import { SearchResultsPanel } from "@/components/search/SearchResultsPanel"; @@ -39,15 +38,6 @@ export function HeaderSearchBar({ variant }: HeaderSearchBarProps) { setDropdownOpen(false); } - function handleSearchButtonClick() { - if (search.query.length >= MIN_SEARCH_LENGTH) { - router.push(`/shop?search=${encodeURIComponent(search.query)}`); - search.close(); - } else { - search.open(); - } - } - const isDesktop = variant === "desktop"; return ( @@ -153,8 +143,8 @@ export function HeaderSearchBar({ variant }: HeaderSearchBarProps) { } className={ isDesktop - ? "flex-1 bg-transparent py-3 pl-4 pr-3 text-sm text-[#1a2e2d] outline-none placeholder:text-[#8aa9a8]" - : "flex-1 border-none bg-transparent pl-3 text-sm text-[#1a2e2d] outline-none placeholder:text-[#8aa9a8] focus:ring-0" + ? "flex-1 bg-transparent py-3 pl-4 pr-5 text-sm text-[#1a2e2d] outline-none placeholder:text-[#8aa9a8]" + : "flex-1 border-none bg-transparent pl-3 pr-4 text-sm text-[#1a2e2d] outline-none placeholder:text-[#8aa9a8] focus:ring-0" } role="combobox" aria-expanded={search.isOpen && search.showResults} @@ -168,27 +158,6 @@ export function HeaderSearchBar({ variant }: HeaderSearchBarProps) { autoComplete="off" /> - {/* Search Button */} - - {/* Results Panel */} {(search.showResults || search.showMinCharsHint) && ( } : "skip", + ) ?? []; + const { cancelOrder, isCancelling, requestReturn, isRequestingReturn, reorderItems, isReordering } = useOrderActions(); const { sessionId } = useCartSession(); const [cancelDialogOpen, setCancelDialogOpen] = useState(false); + const [returnDialogOpen, setReturnDialogOpen] = useState(false); const [reorderDialogOpen, setReorderDialogOpen] = useState(false); if (isLoading) return ; @@ -54,6 +64,16 @@ export function OrderDetailPageView({ orderId }: Props) { } }; + const handleConfirmReturn = async () => { + const result = await requestReturn(orderId); + setReturnDialogOpen(false); + if (result.success) { + toast.success("Return requested. We'll be in touch with next steps."); + } else { + toast.danger("Failed to submit return request. Please try again."); + } + }; + const handleConfirmReorder = async () => { const { added, skipped } = await reorderItems(order.items, sessionId); setReorderDialogOpen(false); @@ -128,11 +148,23 @@ export function OrderDetailPageView({ orderId }: Props) { order={order} onCancel={() => setCancelDialogOpen(true)} isCancelling={isCancelling} + onRequestReturn={() => setReturnDialogOpen(true)} + isRequestingReturn={isRequestingReturn} onReorder={() => setReorderDialogOpen(true)} isReordering={isReordering} /> + {/* Timeline */} + + {/* Dialogs */} + setReturnDialogOpen(false)} + onConfirm={handleConfirmReturn} + isRequesting={isRequestingReturn} + orderNumber={order.orderNumber} + /> setCancelDialogOpen(false)} diff --git a/apps/storefront/src/components/orders/actions/RequestReturnDialog.tsx b/apps/storefront/src/components/orders/actions/RequestReturnDialog.tsx new file mode 100644 index 0000000..d4378db --- /dev/null +++ b/apps/storefront/src/components/orders/actions/RequestReturnDialog.tsx @@ -0,0 +1,60 @@ +"use client"; + +import { AlertDialog, Button, Spinner } from "@heroui/react"; + +interface Props { + isOpen: boolean; + onClose: () => void; + onConfirm: () => void; + isRequesting: boolean; + orderNumber: string; +} + +export function RequestReturnDialog({ + isOpen, + onClose, + onConfirm, + isRequesting, + orderNumber, +}: Props) { + return ( + + { if (!open) onClose(); }}> + + + + + Request a Return? + + +

+ You are requesting a return for order{" "} + {orderNumber}. + Our team will review your request and contact you with next steps. +

+
+ + + + +
+
+
+
+ ); +} diff --git a/apps/storefront/src/components/orders/detail/OrderActions.tsx b/apps/storefront/src/components/orders/detail/OrderActions.tsx index e3662da..c9f4012 100644 --- a/apps/storefront/src/components/orders/detail/OrderActions.tsx +++ b/apps/storefront/src/components/orders/detail/OrderActions.tsx @@ -2,13 +2,15 @@ import { Button, Spinner } from "@heroui/react"; import Link from "next/link"; -import { CANCELLABLE_STATUSES, ORDERS_PATH } from "@/lib/orders"; +import { CANCELLABLE_STATUSES, RETURNABLE_STATUSES, ORDERS_PATH } from "@/lib/orders"; import type { OrderDetail } from "@/lib/orders"; interface Props { order: OrderDetail; onCancel: () => void; isCancelling: boolean; + onRequestReturn: () => void; + isRequestingReturn: boolean; onReorder: () => void; isReordering: boolean; } @@ -19,12 +21,18 @@ export function OrderActions({ order, onCancel, isCancelling, + onRequestReturn, + isRequestingReturn, onReorder, isReordering, }: Props) { const canCancel = (CANCELLABLE_STATUSES as readonly string[]).includes( order.status, ); + const canRequestReturn = + (RETURNABLE_STATUSES as readonly string[]).includes(order.status) && + !order.returnRequestedAt && + order.paymentStatus !== "refunded"; const canReorder = (REORDERABLE_STATUSES as readonly string[]).includes( order.status, ); @@ -49,6 +57,30 @@ export function OrderActions({ )} + {canRequestReturn && ( + + )} + + {order.returnRequestedAt && order.status === "delivered" && ( + + Return Requested + + )} + {canReorder && (