added some api

This commit is contained in:
Jelaletdin12
2025-11-13 21:56:30 +05:00
parent fdec9e4b0e
commit 21b9e88c5c
22 changed files with 2268 additions and 930 deletions

View File

@@ -20,12 +20,16 @@ export default function CartPage() {
const t = useTranslations()
const { data: cart, isLoading, isError } = useCart()
// useCart dönen data yapısı: { message: "success", data: [...] }
const { data: cartResponse, isLoading, isError } = useCart()
const { data: regions = [] } = useRegions()
const { data: addresses = [] } = useAddresses()
const { data: paymentTypes = [] } = usePaymentTypes()
const { mutate: createOrder, isPending: isCreatingOrder } = useCreateOrder()
// Cart items'ı doğru şekilde al
const cartItems = cartResponse?.data || []
useEffect(() => {
setIsClient(true)
}, [])
@@ -37,12 +41,10 @@ export default function CartPage() {
const handleCompleteOrder = () => {
if (!selectedRegion || !selectedAddress || !paymentType) {
console.warn("[v0] Missing required fields for order")
console.warn("Missing required fields for order")
return
}
const selectedRegionObj = regions.find((r) => r.code === selectedRegion)
createOrder(
{
customer_address: selectedAddress,
@@ -53,7 +55,6 @@ export default function CartPage() {
},
{
onSuccess: () => {
// Navigate to orders page after successful order creation
router.push(`/orders`)
},
},
@@ -70,7 +71,7 @@ export default function CartPage() {
)
}
if (isError || !cart?.items || cart.items.length === 0) {
if (isError || cartItems.length === 0) {
return (
<div className="container mx-auto px-4 min-h-[90vh] flex items-center justify-center">
<h2 className="text-3xl md:text-4xl lg:text-5xl text-gray-400 font-semibold">
@@ -101,18 +102,30 @@ export default function CartPage() {
map: t("address"),
}
const itemsBySeller = cart.items.reduce(
// Group items by seller (from channel)
const itemsBySeller = cartItems.reduce(
(acc, item) => {
const sellerId = item.seller.id
const sellerId = item.product.channel?.[0]?.id || 0
const sellerName = item.product.channel?.[0]?.name || "Unknown Seller"
if (!acc[sellerId]) {
acc[sellerId] = { seller: item.seller, items: [] }
acc[sellerId] = {
seller: { id: sellerId, name: sellerName },
items: []
}
}
acc[sellerId].items.push(item)
return acc
},
{} as Record<number, { seller: any; items: typeof cart.items }>,
{} as Record<number, { seller: any; items: typeof cartItems }>
)
// Calculate total
const totalAmount = cartItems.reduce((sum, item) => {
const price = parseFloat(item.product.price_amount || "0")
return sum + (price * item.product_quantity)
}, 0)
return (
<div className="container mx-auto px-4 py-8 min-h-screen">
<h1 className="text-3xl font-bold mb-6">{translations.cart}</h1>
@@ -126,9 +139,34 @@ export default function CartPage() {
<div key={sellerId} className="mb-6">
<p className="text-base font-semibold mb-3">{seller.name}</p>
<div className="space-y-4">
{items.map((item) => (
<CartItemCard key={item.id} item={item} translations={translations} />
))}
{items.map((item) => {
const price = parseFloat(item.product.price_amount || "0")
const quantity = item.product_quantity
const total = price * quantity
return (
<CartItemCard
key={item.id}
item={{
...item,
quantity: quantity,
price: price,
total: total,
seller: seller,
price_formatted: `${item.product.price_amount} TMT`,
sub_total_formatted: `${item.product.price_amount} TMT`,
total_formatted: `${total.toFixed(2)} TMT`,
discount_formatted: "0 TMT",
product: {
...item.product,
image: item.product.media?.[0]?.images_800x800 || item.product.media?.[0]?.thumbnail,
images: item.product.media?.map(m => m.images_800x800 || m.thumbnail) || []
}
}}
translations={translations}
/>
)
})}
</div>
{Object.entries(itemsBySeller).length > 1 && <Separator className="mt-4" />}
</div>
@@ -141,10 +179,27 @@ export default function CartPage() {
order={{
id: 1,
seller: { id: 1, name: "Store" },
items: cart.items,
items: cartItems.map(item => ({
...item,
quantity: item.product_quantity,
price: parseFloat(item.product.price_amount || "0"),
total: parseFloat(item.product.price_amount || "0") * item.product_quantity,
seller: {
id: item.product.channel?.[0]?.id || 0,
name: item.product.channel?.[0]?.name || "Unknown"
}
})),
billing: {
body: [{ title: t("goods"), value: `${cart.total_formatted || `${cart.total} TMT`}` }],
footer: { title: t("total"), value: `${cart.total_formatted || `${cart.total} TMT`}` },
body: [
{
title: t("goods"),
value: `${totalAmount.toFixed(2)} TMT`
}
],
footer: {
title: t("total"),
value: `${totalAmount.toFixed(2)} TMT`
},
},
}}
translations={translations}
@@ -168,4 +223,4 @@ export default function CartPage() {
</div>
</div>
)
}
}

View File

@@ -1,29 +1,147 @@
"use client"
import { useState, useEffect, useRef } from "react"
import Image from "next/image"
import { Minus, Plus, Trash2 } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { useUpdateCartItemQuantity, useRemoveFromCart } from "@/lib/hooks"
import {
useUpdateCartItemQuantity,
useRemoveFromCart
} from "@/lib/hooks"
import type { CartItem, CartTranslations } from "./types"
interface CartItemCardProps {
item: CartItem
translations: CartTranslations
onUpdate?: () => void
}
export default function CartItemCard({ item, translations: t }: CartItemCardProps) {
const { mutate: updateQuantity, isPending: isUpdating } = useUpdateCartItemQuantity()
export default function CartItemCard({
item,
translations: t,
onUpdate
}: CartItemCardProps) {
const [localQuantity, setLocalQuantity] = useState(item.quantity)
const [pendingQuantity, setPendingQuantity] = useState(item.quantity)
const [isLoading, setIsLoading] = useState(false)
const updateTimeoutRef = useRef<NodeJS.Timeout>()
const { mutate: updateQuantity } = useUpdateCartItemQuantity()
const { mutate: removeItem, isPending: isRemoving } = useRemoveFromCart()
const handleQuantityChange = (delta: number) => {
const newQuantity = item.quantity + delta
if (newQuantity >= 1) {
updateQuantity({ itemId: item.id, quantity: newQuantity })
// Sync local quantity with server quantity
useEffect(() => {
setLocalQuantity(item.quantity)
setPendingQuantity(item.quantity)
}, [item.quantity])
// Debounced update effect
useEffect(() => {
if (pendingQuantity === item.quantity) {
return
}
// Clear previous timeout
if (updateTimeoutRef.current) {
clearTimeout(updateTimeoutRef.current)
}
// Set new timeout for update
updateTimeoutRef.current = setTimeout(() => {
setIsLoading(true)
if (pendingQuantity <= 0) {
removeItem(item.id, {
onSuccess: () => {
onUpdate?.()
},
onError: (error) => {
console.error("Failed to remove item:", error)
// Revert on error
setLocalQuantity(item.quantity)
setPendingQuantity(item.quantity)
},
onSettled: () => {
setIsLoading(false)
},
})
} else {
updateQuantity(
{ itemId: item.id, quantity: pendingQuantity },
{
onSuccess: () => {
onUpdate?.()
},
onError: (error) => {
console.error("Failed to update quantity:", error)
// Revert on error
setLocalQuantity(item.quantity)
setPendingQuantity(item.quantity)
},
onSettled: () => {
setIsLoading(false)
},
}
)
}
}, 300)
return () => {
if (updateTimeoutRef.current) {
clearTimeout(updateTimeoutRef.current)
}
}
}, [pendingQuantity, item.quantity, item.id, updateQuantity, removeItem, onUpdate])
const handleQuantityIncrease = (e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
if (isLoading) return
const newQuantity = localQuantity + 1
setLocalQuantity(newQuantity)
setPendingQuantity(newQuantity)
}
const handleQuantityDecrease = (e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
if (isLoading) return
const newQuantity = localQuantity - 1
if (newQuantity < 1) {
handleDelete()
return
}
setLocalQuantity(newQuantity)
setPendingQuantity(newQuantity)
}
const handleDelete = () => {
removeItem(item.id)
setIsLoading(true)
removeItem(item.id, {
onSuccess: () => {
onUpdate?.()
},
onError: (error) => {
console.error("Failed to remove item:", error)
},
onSettled: () => {
setIsLoading(false)
},
})
}
const getImageSrc = () => {
if (item.product.image) return item.product.image
if (item.product.images && item.product.images.length > 0) {
return item.product.images[0]
}
return "/placeholder.svg"
}
return (
@@ -33,7 +151,7 @@ export default function CartItemCard({ item, translations: t }: CartItemCardProp
<div className="flex gap-4 flex-1">
<div className="relative w-[88px] h-[117px] rounded-xl border overflow-hidden flex-shrink-0">
<Image
src={item.product.image || item.product.images[0] || "/placeholder.svg"}
src={getImageSrc()}
alt={item.product.name}
fill
className="object-contain"
@@ -46,7 +164,7 @@ export default function CartItemCard({ item, translations: t }: CartItemCardProp
variant="ghost"
size="sm"
onClick={handleDelete}
disabled={isRemoving}
disabled={isRemoving || isLoading}
className="w-fit p-0 h-auto hover:bg-transparent hover:text-red-500"
>
<Trash2 className="h-5 w-5" />
@@ -58,10 +176,14 @@ export default function CartItemCard({ item, translations: t }: CartItemCardProp
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4 justify-between">
<div className="space-y-1">
<p className="text-sm font-semibold">
{t.pricePerUnit} <span className="text-primary">{item.price_formatted || `${item.price} TMT`}</span>
{t.pricePerUnit}{" "}
<span className="text-primary">
{item.price_formatted || `${item.price} TMT`}
</span>
</p>
<p className="text-sm font-semibold">
{t.additionalPrice} {item.sub_total_formatted || `${item.total} TMT`}
{t.additionalPrice}{" "}
{item.sub_total_formatted || `${item.total} TMT`}
</p>
{item.discount_formatted && item.discount_formatted !== "0 TMT" && (
<p className="text-sm font-semibold">
@@ -81,18 +203,20 @@ export default function CartItemCard({ item, translations: t }: CartItemCardProp
<Button
variant="outline"
size="icon"
onClick={() => handleQuantityChange(-1)}
disabled={item.quantity === 1 || isUpdating}
onClick={handleQuantityDecrease}
disabled={isLoading || isRemoving}
className="rounded-xl bg-blue-50"
>
<Minus className="h-4 w-4" />
</Button>
<div className="w-12 text-center font-semibold">{item.quantity}</div>
<div className="w-12 text-center font-semibold">
{localQuantity}
</div>
<Button
variant="outline"
size="icon"
onClick={() => handleQuantityChange(1)}
disabled={isUpdating}
onClick={handleQuantityIncrease}
disabled={isLoading || isRemoving}
className="rounded-xl bg-blue-50"
>
<Plus className="h-4 w-4" />
@@ -102,4 +226,4 @@ export default function CartItemCard({ item, translations: t }: CartItemCardProp
</div>
</Card>
)
}
}

View File

@@ -1,12 +1,12 @@
import React from "react";
import { Truck, Warehouse } from "lucide-react";
import { Card } from "@/components/ui/card";
import { DeliveryType, CartTranslations } from "./types";
"use client"
import { Truck, Warehouse } from "lucide-react"
import { Card } from "@/components/ui/card"
import { DeliveryType, CartTranslations } from "./types"
interface DeliveryTypeSelectorProps {
selectedType: DeliveryType;
onSelect: (type: DeliveryType) => void;
translations: CartTranslations;
selectedType: DeliveryType
onSelect: (type: DeliveryType) => void
translations: CartTranslations
}
export default function DeliveryTypeSelector({
@@ -15,13 +15,13 @@ export default function DeliveryTypeSelector({
translations: t,
}: DeliveryTypeSelectorProps) {
const deliveryOptions: {
type: DeliveryType;
label: string;
icon: typeof Truck;
type: DeliveryType
label: string
icon: typeof Truck
}[] = [
{ type: "SELECTED_DELIVERY", label: t.delivery, icon: Truck },
{ type: "PICK_UP", label: t.pickup, icon: Warehouse },
];
]
return (
<div className="mb-6">
@@ -30,9 +30,9 @@ export default function DeliveryTypeSelector({
{deliveryOptions.map(({ type, label, icon: Icon }) => (
<Card
key={type}
className={`flex-1 cursor-pointer transition-all ${
className={`flex-1 cursor-pointer transition-all hover:shadow-md ${
selectedType === type
? "border-2 border-[#005bff]"
? "border-2 border-[#005bff] bg-blue-50"
: "border-2 border-gray-200"
}`}
onClick={() => onSelect(type)}
@@ -40,14 +40,18 @@ export default function DeliveryTypeSelector({
<div className="flex flex-col items-center justify-center p-4 gap-2">
<Icon
className={`h-8 w-8 ${
selectedType === type ? "text-[#005bff]" : ""
selectedType === type ? "text-[#005bff]" : "text-gray-600"
}`}
/>
<span className="text-xs">{label}</span>
<span className={`text-xs font-medium ${
selectedType === type ? "text-[#005bff]" : "text-gray-700"
}`}>
{label}
</span>
</div>
</Card>
))}
</div>
</div>
);
)
}

View File

@@ -6,9 +6,22 @@ import { Label } from "@/components/ui/label"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Textarea } from "@/components/ui/textarea"
import { Separator } from "@/components/ui/separator"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from "@/components/ui/select"
import DeliveryTypeSelector from "./DeliveryTypeSelector"
import type { Order, Region, Address, DeliveryType, CartTranslations, PaymentTypeOption } from "./types"
import type {
Order,
Region,
Address,
DeliveryType,
CartTranslations,
PaymentTypeOption
} from "./types"
interface OrderSummaryProps {
order: Order
@@ -51,6 +64,7 @@ export default function OrderSummary({
onCompleteOrder,
isLoading,
}: OrderSummaryProps) {
// Filter addresses based on selected region
const filteredAddresses = selectedRegion
? addresses.filter((addr) => {
const region = regions.find((r) => r.code === selectedRegion)
@@ -58,11 +72,12 @@ export default function OrderSummary({
})
: []
// Validate form completion
const isFormValid = selectedRegion && selectedAddress && paymentType
return (
<Card className="w-full md:w-[380px] p-6 rounded-xl h-fit sticky top-20">
{/* Payment Type */}
{/* Payment Type Selection */}
<div className="mb-6">
<h3 className="text-lg font-semibold mb-3">{t.paymentType}</h3>
<div className="flex gap-2">
@@ -70,7 +85,9 @@ export default function OrderSummary({
<Card
key={type.id}
className={`flex-1 cursor-pointer transition-all ${
paymentType?.id === type.id ? "border-2 border-[#005bff]" : "border-2 border-gray-200"
paymentType?.id === type.id
? "border-2 border-[#005bff]"
: "border-2 border-gray-200"
}`}
onClick={() => onPaymentTypeChange(type)}
>
@@ -82,13 +99,23 @@ export default function OrderSummary({
</div>
</div>
{/* Delivery Type */}
<DeliveryTypeSelector selectedType={deliveryType} onSelect={onDeliveryTypeChange} translations={t} />
{/* Delivery Type Selection */}
<DeliveryTypeSelector
selectedType={deliveryType}
onSelect={onDeliveryTypeChange}
translations={t}
/>
{/* Region Selection */}
<div className="mb-6">
<Label className="text-lg font-semibold mb-3 block">{t.selectRegion}</Label>
<RadioGroup value={selectedRegion || ""} onValueChange={onRegionChange} className="flex flex-wrap gap-4">
<Label className="text-lg font-semibold mb-3 block">
{t.selectRegion}
</Label>
<RadioGroup
value={selectedRegion || ""}
onValueChange={onRegionChange}
className="flex flex-wrap gap-4"
>
{regions.map((region) => (
<div key={region.id} className="flex items-center space-x-2">
<RadioGroupItem
@@ -96,7 +123,10 @@ export default function OrderSummary({
id={`region-${region.id}`}
className="border-2 border-gray-400 data-[state=checked]:border-[#005bff] data-[state=checked]:bg-white data-[state=checked]:[&_svg]:fill-[#005bff] data-[state=checked]:[&_svg]:stroke-[#005bff]"
/>
<Label htmlFor={`region-${region.id}`} className="cursor-pointer">
<Label
htmlFor={`region-${region.id}`}
className="cursor-pointer"
>
{region.name}
</Label>
</div>
@@ -104,10 +134,12 @@ export default function OrderSummary({
</RadioGroup>
</div>
{/* Address Selection */}
{filteredAddresses.length > 0 && (
{/* Address Selection (only show when region is selected) */}
{selectedRegion && filteredAddresses.length > 0 && (
<div className="mb-6">
<Label className="text-lg font-semibold mb-3 block">{t.selectAddress}</Label>
<Label className="text-lg font-semibold mb-3 block">
{t.selectAddress}
</Label>
<div className="flex gap-2">
<Select value={selectedAddress} onValueChange={onAddressChange}>
<SelectTrigger className="rounded-xl">
@@ -133,7 +165,7 @@ export default function OrderSummary({
</div>
)}
{/* Note */}
{/* Note Input */}
<div className="mb-6">
<Label className="text-lg font-semibold mb-3 block">{t.note}</Label>
<Textarea
@@ -148,7 +180,10 @@ export default function OrderSummary({
{/* Billing Summary */}
<div className="space-y-2 mb-4">
{order.billing.body.map((item, index) => (
<div key={index} className="flex justify-between text-base font-medium">
<div
key={index}
className="flex justify-between text-base font-medium"
>
<span>{item.title}:</span>
<span>{item.value}</span>
</div>
@@ -157,21 +192,25 @@ export default function OrderSummary({
<Separator className="my-4" />
{/* Total */}
{/* Total Amount */}
<div className="flex justify-between items-center mb-6">
<span className="text-lg font-semibold">{order.billing.footer.title}:</span>
<span className="text-lg font-bold text-green-600">{order.billing.footer.value}</span>
<span className="text-lg font-semibold">
{order.billing.footer.title}:
</span>
<span className="text-lg font-bold text-green-600">
{order.billing.footer.value}
</span>
</div>
{/* Complete Order Button */}
<Button
onClick={onCompleteOrder}
disabled={!isFormValid || isLoading}
className="w-full rounded-xl bg-[#005bff] hover:bg-[#005bff] cursor-pointer h-12 text-lg font-bold disabled:opacity-50"
className="w-full rounded-xl bg-[#005bff] hover:bg-[#004dcc] cursor-pointer h-12 text-lg font-bold disabled:opacity-50 disabled:cursor-not-allowed"
size="lg"
>
{isLoading ? t.placeOrder + "..." : t.placeOrder}
{isLoading ? `${t.placeOrder}...` : t.placeOrder}
</Button>
</Card>
)
}
}

View File

@@ -6,14 +6,18 @@ export interface CartItem {
product: {
id: number
name: string
description?: string
images: (StaticImageData | string)[]
image?: StaticImageData | string
stock?: number
price_amount?: string
}
seller: {
id: number
name: string
}
quantity: number
product_quantity?: number // For compatibility with old API
price: number
total: number
price_formatted?: string
@@ -22,6 +26,15 @@ export interface CartItem {
total_formatted?: string
}
export interface Cart {
message: string
data: CartItem[]
errorDetails?: string
total?: number
total_formatted?: string
items?: CartItem[] // Alternative structure
}
export interface Order {
id: number
seller: {
@@ -85,3 +98,22 @@ export interface CartTranslations {
export type PaymentType = "CASH" | "CARD"
export type DeliveryType = "SELECTED_DELIVERY" | "PICK_UP"
// API Response types
export interface ApiResponse<T> {
message: string
data: T
errorDetails?: string
}
export interface CreateOrderPayload {
customer_name?: string
customer_phone?: string
customer_address: string
shipping_method: string
payment_type_id: number
delivery_time?: string
delivery_at?: string
region: string
note?: string
}