-

+
-
>
+
)
}
-const productCategories: ProductCategory[] = [
- {
- id: 'transfer-to-platform',
- name: 'Transfer to Platform',
- items: [
- {
- id: 'transfer-100',
- title: 'Transfer 100',
- subtitle: '1 x Turnover',
- points: '1000 Points',
- ctaLabel: 'Transfer Now',
- imageClassName: 'bg-[linear-gradient(135deg,_#5B1D00_0%,_#FA6A00_55%,_#FFBC6D_100%)]',
- },
- {
- id: 'transfer-300',
- title: 'Transfer 300',
- subtitle: '2 x Turnover',
- points: '2800 Points',
- ctaLabel: 'Transfer Now',
- imageClassName: 'bg-[linear-gradient(135deg,_#1C1A48_0%,_#5050D8_55%,_#8AB6FF_100%)]',
- },
- {
- id: 'transfer-500',
- title: 'Transfer 500',
- subtitle: '3 x Turnover',
- points: '4200 Points',
- ctaLabel: 'Transfer Now',
- imageClassName: 'bg-[linear-gradient(135deg,_#153A35_0%,_#1F9D8B_55%,_#8BF3D8_100%)]',
- },
- {
- id: 'transfer-1000',
- title: 'Transfer 1000',
- subtitle: '5 x Turnover',
- points: '7800 Points',
- ctaLabel: 'Transfer Now',
- imageClassName: 'bg-[linear-gradient(135deg,_#421515_0%,_#C93D3D_52%,_#FF9B9B_100%)]',
- },
- ],
- },
- {
- id: 'game-bonus',
- name: 'Game Bonus',
- items: [
- {
- id: 'bonus-spin',
- title: 'Lucky Spin Pack',
- subtitle: 'Bonus Voucher',
- points: '850 Points',
- ctaLabel: 'Redeem Bonus',
- imageClassName: 'bg-[linear-gradient(135deg,_#28103D_0%,_#8E3DD1_50%,_#F1A8FF_100%)]',
- },
- {
- id: 'bonus-chip',
- title: 'Chip Booster',
- subtitle: 'Casino Special',
- points: '1600 Points',
- ctaLabel: 'Redeem Bonus',
- imageClassName: 'bg-[linear-gradient(135deg,_#2D2407_0%,_#C79200_52%,_#FFE58A_100%)]',
- },
- {
- id: 'bonus-cashback',
- title: 'Cashback Card',
- subtitle: 'Weekly Reward',
- points: '2400 Points',
- ctaLabel: 'Redeem Bonus',
- imageClassName: 'bg-[linear-gradient(135deg,_#062A2F_0%,_#1296A5_52%,_#6DE2F0_100%)]',
- },
- {
- id: 'bonus-vip',
- title: 'VIP Match Bonus',
- subtitle: 'Limited Access',
- points: '5000 Points',
- ctaLabel: 'Redeem Bonus',
- imageClassName: 'bg-[linear-gradient(135deg,_#35120A_0%,_#DD6C2F_52%,_#FFC09A_100%)]',
- },
- ],
- },
- {
- id: 'physical-prizes',
- name: 'Physical Prizes',
- items: [
- {
- id: 'prize-headset',
- title: 'Gaming Headset',
- subtitle: 'Physical Delivery',
- points: '6800 Points',
- ctaLabel: 'Claim Prize',
- imageClassName: 'bg-[linear-gradient(135deg,_#111827_0%,_#374151_50%,_#A3AAB8_100%)]',
- },
- {
- id: 'prize-mouse',
- title: 'Wireless Mouse',
- subtitle: 'Physical Delivery',
- points: '4200 Points',
- ctaLabel: 'Claim Prize',
- imageClassName: 'bg-[linear-gradient(135deg,_#19212F_0%,_#3A5B84_50%,_#98B8E3_100%)]',
- },
- {
- id: 'prize-speaker',
- title: 'Bluetooth Speaker',
- subtitle: 'Physical Delivery',
- points: '7300 Points',
- ctaLabel: 'Claim Prize',
- imageClassName: 'bg-[linear-gradient(135deg,_#32120F_0%,_#A73F2F_50%,_#F0A28D_100%)]',
- },
- {
- id: 'prize-keyboard',
- title: 'Mechanical Keyboard',
- subtitle: 'Physical Delivery',
- points: '9500 Points',
- ctaLabel: 'Claim Prize',
- imageClassName: 'bg-[linear-gradient(135deg,_#142C17_0%,_#2B7A38_52%,_#8DE69B_100%)]',
- },
- ],
- },
-]
+function getProgressPercent(current = 0, total = 0) {
+ if (total <= 0) {
+ return 0
+ }
-const initialAddressOptions: AddressOption[] = [
- {
- id: 'address-shanghai',
- name: 'Jia Jun',
- phone: '+86 138 0000 1288',
- address: 'No. 88 Century Avenue, Pudong New Area, Shanghai',
- postalCode: '200120',
- isDefault: true,
- },
- {
- id: 'address-singapore',
- name: 'Alicia Tan',
- phone: '+65 9123 4567',
- address: '18 Robinson Road, Singapore',
- postalCode: '048547',
- },
- {
- id: 'address-kuala-lumpur',
- name: 'Marcus Lee',
- phone: '+60 12 778 9911',
- address: '27 Jalan Bukit Bintang, Kuala Lumpur',
- postalCode: '55100',
- },
-]
-
-const emptyAddressForm: AddAddressForm = {
- name: '',
- phone: '',
- region: '',
- detailedAddress: '',
- postalCode: '',
- isDefault: false,
-}
-
-function getNumericValue(value: string) {
- const matched = value.match(/\d+/)
- return matched ? matched[0] : value
-}
-
-function getTurnoverRequirement(subtitle: string) {
- const matched = subtitle.match(/\d+/)
- return matched ? `${matched[0]}x` : subtitle
+ return Math.min((current / total) * 100, 100)
}
function HomePage() {
- const [selectedProduct, setSelectedProduct] = useState
(null)
const [claimModalOpen, setClaimModalOpen] = useState(false)
- const [modalMode, setModalMode] = useState('select-address')
- const [addressOptions, setAddressOptions] = useState(initialAddressOptions)
- const [selectedAddressId, setSelectedAddressId] = useState(initialAddressOptions[0]?.id ?? '')
- const [addressForm, setAddressForm] = useState(emptyAddressForm)
+ const navigate = useNavigate()
+ const authInfo = useUserStore(state => state.authInfo)
+ const {productCategories, loading} = useGoodsCatalog()
+ const {invalidateAssets} = useAssetsRefresh()
+ const redeem = useGoodsRedeem()
+ const claimMutation = useMutation({
+ mutationFn: async (claimRequestId: string) => {
+ return await claim({
+ claim_request_id: claimRequestId,
+ session_id: authInfo!.session_id,
+ })
+ },
+ })
+ const syncBalanceMutation = useMutation({
+ mutationFn: invalidateAssets,
+ })
- const handleOpenRedeemModal = (product: ProductItem, categoryId: ProductCategory['id']) => {
- setSelectedProduct({
- product,
- categoryId,
- })
- setSelectedAddressId(addressOptions[0]?.id ?? '')
- setModalMode('select-address')
- setAddressForm(emptyAddressForm)
- }
-
- const handleCloseRedeemModal = () => {
- setSelectedProduct(null)
- setModalMode('select-address')
- setAddressForm(emptyAddressForm)
- }
+ const {assetsInfo} = useAssetsQuery()
+ const claimProgress = getProgressPercent(assetsInfo?.today_claimed, assetsInfo?.today_limit)
+ const previewCategories: ProductCategory[] = productCategories.map((category) => ({
+ ...category,
+ items: category.items.slice(0, 4),
+ }))
const handleOpenClaimModal = () => {
setClaimModalOpen(true)
@@ -260,392 +89,158 @@ function HomePage() {
const handleCloseClaimModal = () => {
setClaimModalOpen(false)
+ claimMutation.reset()
}
- const handleOpenAddAddress = () => {
- setModalMode('add-address')
+ const handleSyncBalance = async () => {
+ try {
+ await syncBalanceMutation.mutateAsync()
+ notifySuccess('Balance synced successfully.')
+ } catch {
+ // request interceptor handles interface error toast
+ }
}
- const handleChangeAddressForm = (field: keyof AddAddressForm, value: string | boolean) => {
- setAddressForm((previous) => ({
- ...previous,
- [field]: value,
- }))
- }
-
- const isAddAddressFormValid = [
- addressForm.name,
- addressForm.phone,
- addressForm.region,
- addressForm.detailedAddress,
- ].every((value) => value.trim())
-
- const handleConfirm = () => {
- if (modalMode === 'add-address') {
- if (!isAddAddressFormValid) {
- return
- }
-
- const newAddress: AddressOption = {
- id: `address-${Date.now()}`,
- name: addressForm.name.trim(),
- phone: addressForm.phone.trim(),
- address: `${addressForm.region.trim()}, ${addressForm.detailedAddress.trim()}`,
- postalCode: addressForm.postalCode.trim() || 'N/A',
- isDefault: addressForm.isDefault,
- }
-
- setAddressOptions((previous) => {
- const normalizedPrevious = addressForm.isDefault
- ? previous.map((item) => ({ ...item, isDefault: false }))
- : previous
-
- return [...normalizedPrevious, newAddress]
- })
- setSelectedAddressId(newAddress.id)
- setAddressForm(emptyAddressForm)
- setModalMode('select-address')
+ const handleConfirmClaim = async () => {
+ const claimValidation = validateClaimSubmission(authInfo)
+ if (!claimValidation.valid) {
+ notifyError(claimValidation.message)
return
}
- handleCloseRedeemModal()
+
+ try {
+ const response = await claimMutation.mutateAsync(`${authInfo!.user_id}${Date.now()}`)
+ await invalidateAssets()
+ notifySuccess(response, 'Claim submitted successfully.')
+ setClaimModalOpen(false)
+ } catch {
+ // request errors are surfaced by the shared request toast
+ }
}
- const selectedCategoryId = selectedProduct?.categoryId
- const selectedProductData = selectedProduct?.product ?? null
- const isPhysicalPrize = selectedCategoryId === 'physical-prizes'
- const isTransferToPlatform = selectedCategoryId === 'transfer-to-platform'
- const isGameBonus = selectedCategoryId === 'game-bonus'
- const modalTitle = modalMode === 'add-address'
- ? 'Add Shipping Address'
- : isTransferToPlatform
- ? 'Confirm Withdrawal'
- : isGameBonus
- ? 'Confirm Bonus Redemption'
- : 'Redeem Product'
- const modalMaxWidthClassName = isTransferToPlatform
- ? 'max-w-[620px]'
- : isGameBonus
- ? 'max-w-[460px]'
- : 'max-w-[720px]'
+ const handleMoreClick = (type: ProductCategory['id']) => {
+ navigate(`/goods?type=${type}`)
+ }
return (
-
-
-
+
+
+
-
-
-
-
Claimable Points
-
2,880
-
Yesterday's losses
- have been converted
- to points.Claim to use.
+
+
+
+
+
+
+
Claimable
+ Points
+
+
{assetsInfo?.locked_points || 0}
+
+
+
+
+
+
+ Yesterday's losses have been converted into points. Claim them to use in rewards.
+
-
-
-
Daily Claim Limit
-
1,500
-
-
+
+
+
+
Daily Claim
+ Limit
+
+
{assetsInfo?.locked_points}
+
+
+
+
+
+
+
Claimed: {assetsInfo?.today_claimed || 0} / {assetsInfo?.today_limit || 0}
-
Claimed: 800 / 1500
-
-
-
Available for Withdrawal (Cash)
-
152 CNY
+
+
+
+
+
Available for
+ Withdrawal
+
+
{assetsInfo?.withdrawable_cash || 0} CNY
+
+
+
+
+
-
-
-
- {
- productCategories.map((category) => (
-
-
{category.name}
-
more
-
-
-
- {
- category.items.map((product) => (
-
-
-
-
{product.title}
-
{product.subtitle}
-
{product.points}
-
handleOpenRedeemModal(product, category.id)}
- >
- {product.ctaLabel}
-
-
-
- ))
- }
-
-
))
- }
-
+
-
- setModalMode('select-address') : handleCloseRedeemModal}
- >
- Cancel
-
-
- Confirm
-
- >
- }
- >
- {selectedProductData ? (
- <>
- {isTransferToPlatform ? (
-
-
-
-
Withdrawal Amount
-
{getNumericValue(selectedProductData.title)}
-
-
-
Points Required
-
{getNumericValue(selectedProductData.points)}
-
-
-
- Turnover Requirement
-
-
{getTurnoverRequirement(selectedProductData.subtitle)}
-
-
-
- Submit withdrawal request?
-
-
- ) : isGameBonus ? (
-
-
-
-
Item
-
{selectedProductData.title}
-
-
-
Points Required
-
{getNumericValue(selectedProductData.points)}
-
-
-
Turnover Requirement
-
{getTurnoverRequirement(selectedProductData.subtitle)}
-
-
-
- ) : modalMode === 'select-address' && isPhysicalPrize ? (
- <>
-
-
-
-
-
{selectedProductData.title}
-
{selectedProductData.subtitle}
-
-
{selectedProductData.points}
-
-
-
-
-
Select Shipping Address
-
- {addressOptions.map((address) => {
- const isSelected = selectedAddressId === address.id
-
- return (
-
setSelectedAddressId(address.id)}
- >
-
-
-
-
{address.name}
-
{address.phone}
- {address.isDefault ? (
-
- Default
-
- ) : null}
-
-
- {address.address}
-
-
- Postal Code: {address.postalCode}
-
-
-
- )
- })}
-
-
-
- +
-
-
-
Add New Address
-
- Create a shipping address for this redemption
-
-
-
-
-
- >
- ) : isPhysicalPrize ? (
-
-
-
Address Info
-
handleChangeAddressForm('isDefault', !addressForm.isDefault)}
- >
-
-
-
- Default Address
-
-
-
-
-
-
- handleChangeAddressForm('name', event.target.value)}
- placeholder="Full Name"
- className="bg-transparent text-[14px] text-white outline-none placeholder:text-white/35"
- />
-
-
-
- handleChangeAddressForm('phone', event.target.value)}
- placeholder="Phone Number"
- className="bg-transparent text-[14px] text-white outline-none placeholder:text-white/35"
- />
-
-
-
- handleChangeAddressForm('region', event.target.value)}
- placeholder="State, City, District"
- className="bg-transparent text-[14px] text-white outline-none placeholder:text-white/35"
- />
-
-
-
- handleChangeAddressForm('detailedAddress', event.target.value)}
- placeholder="Apt, Suite, Bldg, etc"
- className="bg-transparent text-[14px] text-white outline-none placeholder:text-white/35"
- />
-
-
-
- handleChangeAddressForm('postalCode', event.target.value)}
- placeholder="Postal Code"
- className="bg-transparent text-[14px] text-white outline-none placeholder:text-white/35"
- />
-
-
-
- ) : null}
- >
- ) : null}
-
+
-
+
Cancel
-
-
- Confirm
-
+
+
+
+ {claimMutation.isPending ? 'Processing...' : 'Confirm'}
+
>
}
>
-
- Once pending points are transferred to your available balance, they can be redeemed or withdrawn.
- Confirm claim?
+
+ After converting the points to be collected into usable points, they can be redeemed or withdrawn. Are you sure to claim it?
diff --git a/src/views/record/index.tsx b/src/views/record/index.tsx
index 18b6660..0b74d56 100644
--- a/src/views/record/index.tsx
+++ b/src/views/record/index.tsx
@@ -1,131 +1,246 @@
-import { useState } from 'react'
+import {useEffect, useState} from 'react'
+import {useQuery} from '@tanstack/react-query'
import PageLayout from '@/components/layout'
+import {ORDER_STATUS} from '@/constant'
import { cn } from '@/lib'
import Modal from '@/components/modal'
-import type { RecordButtonType } from '@/types'
+import Button from '@/components/button'
+import type { OrderCardProps, OrderRecord, PointsCardProps, RecordButtonType, TabButtonProps } from '@/types'
import { Link } from 'react-router-dom'
+import { ArrowLeft, ChevronRight, Coins, PackageSearch } from 'lucide-react'
+import {orders, pointsLogs} from '@/api/business.ts'
+import {queryKeys} from '@/lib/queryKeys.ts'
+import { useUserStore } from '@/store/user.ts'
+import type {OrderItem, PointsLogItem} from '@/types/business.type.ts'
-type OrderRecord = {
- id: string
- date: string
- time: string
- category: string
- title: string
- trackingNumber?: string
- status: string
- points: string
-}
-
-type PointsRecord = {
- id: string
- title: string
- date: string
- time: string
- amount: string
- tone: 'positive' | 'negative'
-}
-
-const orderRecords: OrderRecord[] = [
- {
- id: 'order-1',
- date: '2025-03-04',
- time: '10:20',
- category: 'Bonus',
- title: 'Daily Rebate 50',
- status: 'Issued',
- points: '-500 points',
- },
- {
- id: 'order-2',
- date: '2025-03-03',
- time: '14:00',
- category: 'Physical',
- title: 'Weekly Bonus 200',
- trackingNumber: 'SF1234567890',
- status: 'Shipped',
- points: '-1200 points',
- },
- {
- id: 'order-3',
- date: '2025-03-02',
- time: '09:15',
- category: 'Withdrawal',
- title: 'Wireless Earbuds',
- status: 'Issued',
- points: '-1000 points',
- },
- {
- id: 'order-4',
- date: '2025-03-01',
- time: '16:30',
- category: 'Bonus',
- title: 'Fitness Tracker',
- status: 'Pending',
- points: '-1800 points',
- },
- {
- id: 'order-5',
- date: '2025-02-28',
- time: '11:00',
- category: 'Physical',
- title: 'Withdraw 100',
- status: 'Rejected',
- points: '-2500 points',
- },
-]
-
-const pointsRecords: PointsRecord[] = [
- {
- id: 'points-1',
- title: 'Bonus Redemption - Daily Rewards 50',
- date: '2025-03-04',
- time: '10:20',
- amount: '-500',
- tone: 'negative',
- },
- {
- id: 'points-2',
- title: "Claim Yesterday's Protection Funds",
- date: '2025-03-04',
- time: '09:20',
- amount: '+800',
- tone: 'positive',
- },
- {
- id: 'points-3',
- title: 'Physical Item Redemption - Bluetooth Headphones',
- date: '2025-03-03',
- time: '14:00',
- amount: '-1200',
- tone: 'negative',
- },
- {
- id: 'points-4',
- title: "Claim Yesterday's Protection Funds",
- date: '2025-03-03',
- time: '09:00',
- amount: '+700',
- tone: 'positive',
- },
- {
- id: 'points-5',
- title: 'Withdraw to Platform - 100',
- date: '2025-03-02',
- time: '09:15',
- amount: '-1000',
- tone: 'negative',
- },
-]
-
-const amountToneClassName: Record
= {
+const pointsRecordToneClassName = {
positive: 'bg-[#9BFFC0] text-[#176640]',
negative: 'bg-[#FF9BA4] text-[#7B2634]',
+} as const
+
+function formatDatePart(value: number) {
+ return String(value).padStart(2, '0')
+}
+
+function getDateTimeParts(value?: number | string) {
+ if (value == null) {
+ return { date: '--', time: '--:--' }
+ }
+
+ const numericValue = typeof value === 'string' && /^\d+$/.test(value) ? Number(value) : value
+ const date = new Date(
+ typeof numericValue === 'number'
+ ? String(numericValue).length === 13
+ ? numericValue
+ : numericValue * 1000
+ : numericValue,
+ )
+
+ if (Number.isNaN(date.getTime())) {
+ return { date: '--', time: '--:--' }
+ }
+
+ return {
+ date: `${date.getFullYear()}-${formatDatePart(date.getMonth() + 1)}-${formatDatePart(date.getDate())}`,
+ time: `${formatDatePart(date.getHours())}:${formatDatePart(date.getMinutes())}`,
+ }
+}
+
+function toTitleCase(value: string) {
+ return value
+ .toLowerCase()
+ .split(/[\s_-]+/)
+ .filter(Boolean)
+ .map((part) => part[0]?.toUpperCase() + part.slice(1))
+ .join(' ')
+}
+
+function getOrderStatus(status?: string | number) {
+ if (typeof status === 'string') {
+ const normalizedStatus = status.trim().toUpperCase()
+ const matchedStatus = ORDER_STATUS.find((item) => item === normalizedStatus)
+ if (matchedStatus) {
+ switch (matchedStatus) {
+ case 'PENDING':
+ return 'Pending'
+ case 'COMPLETED':
+ return 'Completed'
+ case 'SHIPPED':
+ return 'Shipped'
+ case 'REJECTED':
+ return 'Rejected'
+ }
+ }
+ }
+
+ const normalizedStatus = typeof status === 'string' && /^\d+$/.test(status)
+ ? Number(status)
+ : status
+
+ if (typeof normalizedStatus === 'number') {
+ switch (normalizedStatus) {
+ case 0:
+ return 'Pending'
+ case 1:
+ return 'Completed'
+ case 2:
+ return 'Shipped'
+ case 3:
+ return 'Rejected'
+ default:
+ return String(normalizedStatus)
+ }
+ }
+
+ if (!normalizedStatus) {
+ return 'Pending'
+ }
+
+ return toTitleCase(normalizedStatus)
+}
+
+function getOrderCategory(item: OrderItem) {
+ if (item.type?.trim()) {
+ return item.type.trim().toUpperCase()
+ }
+
+ if (item.type_title) {
+ return item.type_title
+ }
+
+ if (item.category_title) {
+ return item.category_title
+ }
+
+ if (item.type) {
+ switch (item.type) {
+ case 'BONUS':
+ return 'Bonus'
+ case 'PHYSICAL':
+ return 'Physical'
+ case 'WITHDRAW':
+ return 'Transfer to Platform'
+ default:
+ return toTitleCase(item.type)
+ }
+ }
+
+ if (item.category) {
+ return toTitleCase(item.category)
+ }
+
+ return 'Order'
+}
+
+function getOrderPoints(item: OrderItem) {
+ const rawValue = item.points_cost
+
+ if (rawValue == null || rawValue === '') {
+ return {display: '--'}
+ }
+
+ const numericValue = typeof rawValue === 'string' ? Number(rawValue) : rawValue
+ if (Number.isNaN(numericValue)) {
+ const textValue = String(rawValue)
+ return {display: `${textValue} points`}
+ }
+
+ return {
+ display: `${numericValue} points`,
+ }
+}
+
+function getTrackingNumber(item: OrderItem) {
+ const shippingNumber = item.shipping_no?.trim()
+ const logisticsNumber = item.logistics_no?.trim()
+ const trackingNumber = item.tracking_no?.trim()
+ const shippingCompany = item.shipping_company?.trim()
+ const resolvedNumber = shippingNumber || logisticsNumber || trackingNumber
+
+ if (!resolvedNumber) {
+ return undefined
+ }
+
+ return shippingCompany ? `${shippingCompany} ${resolvedNumber}` : resolvedNumber
+}
+
+function mapOrderItemToRecord(item: OrderItem): OrderRecord {
+ const { date, time } = getDateTimeParts(item.create_time ?? item.created_at)
+ const orderNumber = item.external_transaction_id?.trim() || item.order_no ? String(item.external_transaction_id?.trim() || item.order_no) : undefined
+ const points = getOrderPoints(item)
+
+ return {
+ id: String(item.id),
+ orderNumber,
+ date,
+ time,
+ category: getOrderCategory(item),
+ title: item.item_title ?? item.title ?? item.mallItem?.title ?? 'Untitled Order',
+ trackingNumber: getTrackingNumber(item),
+ status: getOrderStatus(item.status),
+ points: points.display,
+ }
+}
+
+function getPointsRecordAmount(item: PointsLogItem) {
+ const rawValue = item.points
+
+ if (rawValue == null || rawValue === '') {
+ return {
+ amount: '--',
+ tone: 'negative' as const,
+ }
+ }
+
+ const numericValue = typeof rawValue === 'string' ? Number(rawValue) : rawValue
+ if (Number.isNaN(numericValue)) {
+ const textValue = String(rawValue).trim()
+ return {
+ amount: item.direction === 'IN'
+ ? textValue.startsWith('+') ? textValue : `+${textValue.replace(/^[+-]/, '')}`
+ : item.direction === 'OUT'
+ ? textValue.startsWith('-') ? textValue : `-${textValue.replace(/^[+-]/, '')}`
+ : textValue,
+ tone: item.direction === 'IN' ? 'positive' as const : 'negative' as const,
+ }
+ }
+
+ return {
+ amount: `${item.direction === 'IN' ? '+' : item.direction === 'OUT' ? '-' : ''}${Math.abs(numericValue)}`,
+ tone: item.direction === 'IN' ? 'positive' as const : 'negative' as const,
+ }
+}
+
+function getPointsRecordTitle(item: PointsLogItem) {
+ return [
+ item.item_title,
+ item.title,
+ item.mallItem?.title,
+ item.biz_type,
+ item.type,
+ item.description,
+ item.remark?.split('\n')[0],
+ ].find((value) => typeof value === 'string' && value.trim())?.trim() ?? 'Points Record'
+}
+
+function mapPointsLogItemToRecord(item: PointsLogItem) {
+ const {date, time} = getDateTimeParts(item.ts ?? item.create_time ?? item.created_at ?? item.update_time)
+ const amount = getPointsRecordAmount(item)
+
+ return {
+ id: String(item.id),
+ title: getPointsRecordTitle(item),
+ date,
+ time,
+ amount: amount.amount,
+ tone: amount.tone,
+ }
}
function getOrderStatusClassName(status: string) {
switch (status.toLowerCase()) {
- case 'issued':
+ case 'completed':
return 'bg-[#9BFFC0] text-[#176640]'
case 'shipped':
return 'bg-[#95F0FF] text-[#116A79]'
@@ -138,29 +253,19 @@ function getOrderStatusClassName(status: string) {
}
}
-type TabButtonProps = {
- active: boolean
- label: string
- onClick: () => void
-}
-
-type OrderCardProps = {
- record: OrderRecord
- onOpenDetails: (record: OrderRecord) => void
-}
-
-function TabButton({ active, label, onClick }: TabButtonProps) {
+function TabButton({ active, label, icon: Icon, onClick }: TabButtonProps) {
return (
+
{label}
)
@@ -168,12 +273,12 @@ function TabButton({ active, label, onClick }: TabButtonProps) {
function OrderCard({ record, onOpenDetails }: OrderCardProps) {
return (
-
-
+
+
{record.date} {record.time} • {record.category}
-
-
+
+
{record.title}
{record.trackingNumber ? (
@@ -182,14 +287,15 @@ function OrderCard({ record, onOpenDetails }: OrderCardProps) {
) : null}
onOpenDetails(record)}
>
Check the details
+
-
+
{record.status}
-
{record.points}
+
{record.points.replace(/^-/, '')}
)
}
-function PointsCard({ record }: { record: PointsRecord }) {
+function PointsCard({ record }: PointsCardProps) {
return (
-
-
+
+
{record.title}
-
+
{record.date} {record.time}
{record.amount}
@@ -228,7 +334,92 @@ function PointsCard({ record }: { record: PointsRecord }) {
)
}
+function OrdersTabContent({
+ sessionId,
+ onOpenDetails,
+}: {
+ sessionId: string
+ onOpenDetails: (record: OrderRecord) => void
+}) {
+ const ordersQuery = useQuery({
+ queryKey: queryKeys.orders(sessionId),
+ enabled: Boolean(sessionId),
+ gcTime: 0,
+ queryFn: async () => {
+ const response = await orders({
+ session_id: sessionId,
+ })
+
+ return (response.data.list ?? []).map(mapOrderItemToRecord)
+ },
+ })
+ const orderRecords = ordersQuery.data ?? []
+
+ if (ordersQuery.isPending) {
+ return (
+
+ Loading...
+
+ )
+ }
+
+ if (!orderRecords.length) {
+ return (
+
+ No Data
+
+ )
+ }
+
+ return (
+ <>
+ {orderRecords.map((record) => (
+
+ ))}
+ >
+ )
+}
+
+function PointsTabContent({sessionId}: {sessionId: string}) {
+ const pointsLogsQuery = useQuery({
+ queryKey: queryKeys.pointsLogs(sessionId),
+ enabled: Boolean(sessionId),
+ gcTime: 0,
+ queryFn: async () => {
+ const response = await pointsLogs({
+ session_id: sessionId,
+ })
+
+ return (response.data.list ?? []).map(mapPointsLogItemToRecord)
+ },
+ })
+ const pointsRecords = pointsLogsQuery.data ?? []
+
+ if (pointsLogsQuery.isPending) {
+ return (
+
+ Loading...
+
+ )
+ }
+
+ if (!pointsRecords.length) {
+ return (
+
+ No Data
+
+ )
+ }
+
+ return (
+ <>
+ {pointsRecords.map((record) =>
)}
+ >
+ )
+}
+
function RecordPage() {
+ const sessionId = useUserStore((state) => state.authInfo?.session_id ?? '')
const [tab, setTab] = useState
('order')
const [selectedOrder, setSelectedOrder] = useState(null)
@@ -236,30 +427,50 @@ function RecordPage() {
setSelectedOrder(null)
}
+ useEffect(() => {
+ setSelectedOrder(null)
+ }, [tab])
+
return (
-
+
- <
- Record
+
+ Record
+
-
-
-
setTab('order')} />
- setTab('record')} />
+
+
+
+ setTab('order')}
+ />
+ setTab('record')}
+ />
+
-
+
-
- {tab === 'order'
- ? orderRecords.map((record) => (
-
- ))
- : pointsRecords.map((record) =>
)}
+
+ {tab === 'order' ? (
+
+ ) : (
+
+ )}
@@ -267,22 +478,25 @@ function RecordPage() {
open={Boolean(selectedOrder)}
title="Order Details"
onClose={handleCloseDetails}
- className="max-w-[380px]"
+ className="max-w-[420px]"
bodyClassName="pt-[0px]"
footer={
-
+
Close
-
+
}
>
{selectedOrder ? (
-
+
{[
- { label: 'Order Number', value: `ORD${selectedOrder.date.replaceAll('-', '')}${selectedOrder.time.replace(':', '')}001` },
+ { label: 'Order Number', value: selectedOrder.orderNumber ?? '--' },
{ label: 'Order Time', value: `${selectedOrder.date} ${selectedOrder.time}` },
{ label: 'Order Type', value: selectedOrder.category },
{ label: 'Item Name', value: selectedOrder.title },
- { label: 'Points', value: selectedOrder.points.replace('-', '') },
+ ...(selectedOrder.trackingNumber
+ ? [{ label: 'Tracking Number', value: selectedOrder.trackingNumber }]
+ : []),
+ { label: 'Points', value: selectedOrder.points.replace(/^-/, '') },
{ label: 'Status', value: selectedOrder.status },
].map((item) => (
diff --git a/vite.config.ts b/vite.config.ts
index 543fccf..a4d572f 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,19 +1,58 @@
-import { defineConfig } from 'vite'
-import react, { reactCompilerPreset } from '@vitejs/plugin-react'
+import {defineConfig} from 'vite'
+import react, {reactCompilerPreset} from '@vitejs/plugin-react'
import babel from '@rolldown/plugin-babel'
import tailwindcss from '@tailwindcss/vite'
-import { fileURLToPath, URL } from 'node:url'
+import {fileURLToPath, URL} from 'node:url'
// https://vite.dev/config/
export default defineConfig({
- resolve: {
- alias: {
- '@': fileURLToPath(new URL('./src', import.meta.url)),
+ base: './',
+ build: {
+ rollupOptions: {
+ output: {
+ manualChunks(id) {
+ if (!id.includes('node_modules')) {
+ return
+ }
+
+ if (id.includes('/react-router-dom/')) {
+ return 'router'
+ }
+
+ if (id.includes('/@tanstack/react-query/') || id.includes('/zustand/') || id.includes('/ky/')) {
+ return 'data'
+ }
+
+ if (id.includes('/lucide-react/')) {
+ return 'icons'
+ }
+
+ if (id.includes('/react/') || id.includes('/react-dom/')) {
+ return 'react'
+ }
+
+ return 'vendor'
+ },
+ },
+ },
},
- },
- plugins: [
- tailwindcss(),
- react(),
- babel({ presets: [reactCompilerPreset()] })
- ],
+ server: {
+ proxy: {
+ '/api': {
+ target: 'https://playx-api.cjdhr.top',
+ changeOrigin: true,
+ secure: true,
+ },
+ },
+ },
+ resolve: {
+ alias: {
+ '@': fileURLToPath(new URL('./src', import.meta.url)),
+ },
+ },
+ plugins: [
+ tailwindcss(),
+ react(),
+ babel({presets: [reactCompilerPreset()]})
+ ],
})