diff --git a/src/app/(customer)/(shop)/cart/page.tsx b/src/app/(customer)/(shop)/cart/page.tsx index 31bb900..243db0c 100644 --- a/src/app/(customer)/(shop)/cart/page.tsx +++ b/src/app/(customer)/(shop)/cart/page.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import { getCart } from "@/lib/api/cart"; import { requireSession } from "@/lib/auth/session"; import { CartItemRow } from "@/components/customer/cart-item-row"; +import { ClearCartButton } from "@/components/customer/clear-cart-button"; import { Button } from "@/components/ui/button"; export default async function CartPage() { @@ -22,7 +23,10 @@ export default async function CartPage() { return (
-

سبد خرید

+
+

سبد خرید

+ +
{cart.groups.map((group) => (
diff --git a/src/app/(customer)/(shop)/stores/[uuid]/page.tsx b/src/app/(customer)/(shop)/stores/[uuid]/page.tsx index 9102f9a..ed111ff 100644 --- a/src/app/(customer)/(shop)/stores/[uuid]/page.tsx +++ b/src/app/(customer)/(shop)/stores/[uuid]/page.tsx @@ -25,45 +25,70 @@ export default async function StoreDetailPage({ }); if (!store) notFound(); - const [products, session] = await Promise.all([getProducts({ store: uuid, search, limit: 100 }), getValidSession()]); + const [products, session] = await Promise.all([ + getProducts({ store: uuid, search, limit: 100 }), + getValidSession(), + ]); const cart = session ? await getCart() : null; const storeGroup = cart?.groups.find((g) => g.store.uuid === uuid) ?? null; const cartItemByProduct = new Map(); - storeGroup?.items.forEach((item) => cartItemByProduct.set(item.product.uuid, item)); + storeGroup?.items.forEach((item) => + cartItemByProduct.set(item.product.uuid, item), + ); return (
-
+
{store.cover_image_url && ( // eslint-disable-next-line @next/next/no-img-element - + )}
+
+ +
- - -
-

توضیحات

-

{store.description}

+
+

+ توضیحات +

+

+ {store.description} +

-
+
-
+
{products.map((product) => ( - + ))} {products.length === 0 && ( -

محصولی یافت نشد.

+

+ محصولی یافت نشد. +

)}
- {storeGroup && } + {storeGroup && ( + + )}
); } diff --git a/src/components/auth/role-card.tsx b/src/components/auth/role-card.tsx index f247116..5cdddb1 100644 --- a/src/components/auth/role-card.tsx +++ b/src/components/auth/role-card.tsx @@ -31,7 +31,7 @@ export function RoleCard({ className={cn( "flex h-12 w-full items-center justify-center rounded-full text-body-large font-semibold transition-colors", ctaVariant === "primary" - ? "bg-primary-action text-white hover:bg-primary-action/90" + ? "border-0 bg-primary-action text-white hover:bg-primary-action/90" : "border border-primary-action bg-white text-primary-action hover:bg-primary-action/5", )} > diff --git a/src/components/customer/add-to-cart-button.tsx b/src/components/customer/add-to-cart-button.tsx deleted file mode 100644 index f860672..0000000 --- a/src/components/customer/add-to-cart-button.tsx +++ /dev/null @@ -1,54 +0,0 @@ -"use client"; - -import { useState, useTransition } from "react"; -import { useRouter, usePathname } from "next/navigation"; -import { addToCart } from "@/lib/api/cart"; -import { Button } from "@/components/ui/button"; -import { cn } from "@/lib/utils/cn"; - -export function AddToCartButton({ - productUuid, - quantity = 1, - disabled, - outOfStock, - className, -}: { - productUuid: string; - quantity?: number; - disabled?: boolean; - outOfStock?: boolean; - className?: string; -}) { - const router = useRouter(); - const pathname = usePathname(); - const [isPending, startTransition] = useTransition(); - const [feedback, setFeedback] = useState<"added" | "error" | null>(null); - - function handleClick() { - startTransition(async () => { - const result = await addToCart(productUuid, quantity); - if (result.ok) { - setFeedback("added"); - router.refresh(); - setTimeout(() => setFeedback(null), 1500); - } else if (result.status === 401) { - router.push(`/login?next=${encodeURIComponent(pathname)}`); - } else { - setFeedback("error"); - } - }); - } - - return ( - - ); -} diff --git a/src/components/customer/cart-item-row.tsx b/src/components/customer/cart-item-row.tsx index 923824b..0a92ad9 100644 --- a/src/components/customer/cart-item-row.tsx +++ b/src/components/customer/cart-item-row.tsx @@ -1,59 +1,74 @@ "use client"; -import { useTransition } from "react"; -import { useRouter } from "next/navigation"; +import { useEffect, useState, useTransition } from "react"; +import { useRouter, usePathname } from "next/navigation"; import { updateCartItemQuantity, removeCartItem } from "@/lib/api/cart"; import type { CartItem } from "@/types/api"; export function CartItemRow({ item }: { item: CartItem }) { const router = useRouter(); + const pathname = usePathname(); const [isPending, startTransition] = useTransition(); + const [error, setError] = useState(null); + + useEffect(() => { + if (!error) return; + const timeout = setTimeout(() => setError(null), 3000); + return () => clearTimeout(timeout); + }, [error]); function changeQuantity(nextQuantity: number) { startTransition(async () => { - if (nextQuantity <= 0) { - await removeCartItem(item.uuid); + const result = + nextQuantity <= 0 ? await removeCartItem(item.uuid) : await updateCartItemQuantity(item.uuid, nextQuantity); + + if (result.ok) { + router.refresh(); + } else if (result.status === 401) { + router.push(`/login?next=${encodeURIComponent(pathname)}`); } else { - await updateCartItemQuantity(item.uuid, nextQuantity); + setError(result.message); } - router.refresh(); }); } return ( -
-
- {item.product.image_url && ( - // eslint-disable-next-line @next/next/no-img-element - - )} +
+
+
+ {item.product.image_url && ( + // eslint-disable-next-line @next/next/no-img-element + + )} +
+
+ {item.product.name} + {item.product.price.toLocaleString("fa-IR")} تومان +
+
+ + {item.quantity.toLocaleString("fa-IR")} + +
+ {item.line_total.toLocaleString("fa-IR")}
-
- {item.product.name} - {item.product.price.toLocaleString("fa-IR")} تومان -
-
- - {item.quantity} - -
- {item.line_total.toLocaleString("fa-IR")} + {error &&

{error}

}
); } diff --git a/src/components/customer/clear-cart-button.tsx b/src/components/customer/clear-cart-button.tsx new file mode 100644 index 0000000..708e0e7 --- /dev/null +++ b/src/components/customer/clear-cart-button.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { useEffect, useState, useTransition } from "react"; +import { useRouter, usePathname } from "next/navigation"; +import { Trash2 } from "lucide-react"; +import { clearCart } from "@/lib/api/cart"; + +export function ClearCartButton() { + const router = useRouter(); + const pathname = usePathname(); + const [isPending, startTransition] = useTransition(); + const [error, setError] = useState(null); + + useEffect(() => { + if (!error) return; + const timeout = setTimeout(() => setError(null), 3000); + return () => clearTimeout(timeout); + }, [error]); + + function handleClear() { + if (!window.confirm("سبد خرید شما به‌طور کامل خالی می‌شود. ادامه می‌دهید؟")) return; + startTransition(async () => { + const result = await clearCart(); + if (result.ok) { + router.refresh(); + } else if (result.status === 401) { + router.push(`/login?next=${encodeURIComponent(pathname)}`); + } else { + setError(result.message); + } + }); + } + + return ( +
+ + {error &&

{error}

} +
+ ); +} diff --git a/src/components/store/product-grid-card.tsx b/src/components/store/product-grid-card.tsx index defcdf8..e840bac 100644 --- a/src/components/store/product-grid-card.tsx +++ b/src/components/store/product-grid-card.tsx @@ -4,11 +4,20 @@ import { useEffect, useState, useTransition } from "react"; import Link from "next/link"; import { useRouter, usePathname } from "next/navigation"; import { Minus, Plus } from "lucide-react"; -import { addToCart, updateCartItemQuantity, removeCartItem } from "@/lib/api/cart"; +import { + addToCart, + updateCartItemQuantity, + removeCartItem, +} from "@/lib/api/cart"; import type { CartItem, ProductListItem } from "@/types/api"; -/** designspec.md §2.5 — product-card, 157×254, with the two-state add/stepper control. */ -export function ProductGridCard({ product, cartItem }: { product: ProductListItem; cartItem: CartItem | null }) { +export function ProductGridCard({ + product, + cartItem, +}: { + product: ProductListItem; + cartItem: CartItem | null; +}) { const router = useRouter(); const pathname = usePathname(); const [isPending, startTransition] = useTransition(); @@ -43,12 +52,13 @@ export function ProductGridCard({ product, cartItem }: { product: ProductListIte startTransition(async () => { if (nextQuantity <= 0) { const result = await removeCartItem(localItem.uuid); - // Only clear locally once the backend actually confirms removal — otherwise - // the card would show "add" again while the item still exists server-side. if (result.ok) setLocalItem(null); else setError(result.message); } else { - const result = await updateCartItemQuantity(localItem.uuid, nextQuantity); + const result = await updateCartItemQuantity( + localItem.uuid, + nextQuantity, + ); if (result.ok) setLocalItem(result.data); else setError(result.message); } @@ -57,67 +67,83 @@ export function ProductGridCard({ product, cartItem }: { product: ProductListIte } return ( -
+
{error && (
{error}
)} -
+
{product.image_url && ( // eslint-disable-next-line @next/next/no-img-element - {product.name} + {product.name} )} -
+
-
-
- -

{product.name}

- -

- {product.price.toLocaleString("fa-IR")} تومان -

-
+
+ +

+ {product.name} +

+ +

+ {product.price.toLocaleString("fa-IR")} تومان +

+ {disabled && ( + + ناموجود + + )} +
- {disabled ? ( - ناموجود - ) : quantity === 0 ? ( - - ) : ( -
+ {!disabled && ( +
+
- {quantity.toLocaleString("fa-IR")} - + + {quantity > 0 && ( + <> + + {quantity.toLocaleString("fa-IR")} + + + + + )}
- )} -
+
+ )}
); } diff --git a/src/components/store/store-info-card.tsx b/src/components/store/store-info-card.tsx index 2f21db1..c07dc4c 100644 --- a/src/components/store/store-info-card.tsx +++ b/src/components/store/store-info-card.tsx @@ -6,7 +6,9 @@ import type { StoreDetail } from "@/types/api"; function todaysWorkingHours(store: StoreDetail) { const jsDay = new Date().getDay(); const iranianWeekday = (jsDay + 1) % 7; - return store.working_hours.find((wh) => wh.weekday === iranianWeekday) ?? null; + return ( + store.working_hours.find((wh) => wh.weekday === iranianWeekday) ?? null + ); } function formatTime(value: string | null) { @@ -20,35 +22,52 @@ export function StoreInfoCard({ store }: { store: StoreDetail }) { const mapsUrl = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(`${store.name} ${store.address}`)}`; return ( -
-

{store.name}

-

{store.category.name}

+
+

+ {store.name} +

+

+ {store.category.name} +

- {today && ( - - {today.is_closed ? "تعطیل" : `${formatTime(today.opens_at)} تا ${formatTime(today.closes_at)}`} - - )} - | - + {store.is_open ? "فعال" : "بسته"} + | + {today && ( + + {today.is_closed + ? "تعطیل" + : `${formatTime(today.opens_at)} تا ${formatTime(today.closes_at)}`} + + )}
-
+ @@ -56,7 +75,7 @@ export function StoreInfoCard({ store }: { store: StoreDetail }) { {store.phone_number && ( diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 232d283..500f777 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -8,10 +8,14 @@ const buttonVariants = cva( { variants: { variant: { - primary: "bg-primary text-primary-foreground hover:bg-primary/90", - secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", + // Tailwind's preflight doesn't zero out