feat:新增多语言
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import {useState} from 'react'
|
||||
import {useTranslation} from 'react-i18next'
|
||||
|
||||
import PageLayout from '@/components/layout'
|
||||
import Modal from '@/components/modal'
|
||||
@@ -11,6 +12,7 @@ import {notifySuccess} from '@/features/notifications'
|
||||
import type {AddressListItem} from '@/types/address.type.ts'
|
||||
|
||||
function AccountPage() {
|
||||
const {t} = useTranslation()
|
||||
const addressBook = useAddressBook({autoLoad: true})
|
||||
const [addressModalOpen, setAddressModalOpen] = useState(false)
|
||||
const [editingAddress, setEditingAddress] = useState<AddressListItem | null>(null)
|
||||
@@ -40,7 +42,7 @@ function AccountPage() {
|
||||
const saved = await addressBook.saveAddress(editingAddress)
|
||||
if (saved) {
|
||||
handleCloseAddressModal()
|
||||
notifySuccess(saved.response, editingAddress ? 'Address updated successfully.' : 'Address added successfully.')
|
||||
notifySuccess(saved.response, editingAddress ? t('account.addressUpdatedSuccessfully') : t('goods.addressAddedSuccessfully'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,107 +54,116 @@ function AccountPage() {
|
||||
const deleted = await addressBook.removeAddress(String(deleteTarget.id))
|
||||
if (deleted) {
|
||||
setDeleteTarget(null)
|
||||
notifySuccess(deleted, 'Address deleted successfully.')
|
||||
notifySuccess(deleted, t('account.addressDeletedSuccessfully'))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<PageLayout contentClassName="mx-auto flex min-h-screen w-full max-w-[1120px] flex-col px-4 pb-8 sm:px-6 lg:px-8">
|
||||
<Link
|
||||
to="/"
|
||||
className="mt-[12px] flex h-[44px] items-center justify-between rounded-[12px] bg-[#08070E]/72 px-[14px] text-[#F56E10] transition-colors hover:bg-[#0D0A14]/80 focus-visible:ring-2 focus-visible:ring-[#FE9F00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070E] sm:mt-[16px]"
|
||||
>
|
||||
<div className="flex items-center gap-[8px]">
|
||||
<ArrowLeft className="h-[16px] w-[16px]" aria-hidden="true" />
|
||||
<span className="text-[14px] font-medium text-white/92">Back</span>
|
||||
</div>
|
||||
<div className="text-[15px] font-semibold text-[#F56E10]">Account</div>
|
||||
<div className="w-[52px]"></div>
|
||||
</Link>
|
||||
|
||||
<div className="mx-auto mt-[20px] w-full max-w-[1000px]">
|
||||
<div className="mb-[14px] flex flex-col gap-[12px] sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center gap-[10px]">
|
||||
<div className="flex h-[38px] w-[38px] items-center justify-center rounded-[12px] bg-[#FA6A00]/15 text-[#FE9F00]">
|
||||
<MapPinHouse className="h-[18px] w-[18px]" aria-hidden="true" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[16px] font-semibold text-white">My Shipping Address</div>
|
||||
</div>
|
||||
<PageLayout contentClassName="flex h-[100svh] w-full flex-col overflow-hidden px-4 pb-8 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto w-full max-w-[1120px]">
|
||||
<Link
|
||||
to="/"
|
||||
className="mt-[12px] flex h-[44px] items-center justify-between rounded-[12px] bg-[#08070E]/72 px-[14px] text-[#F56E10] transition-colors hover:bg-[#0D0A14]/80 focus-visible:ring-2 focus-visible:ring-[#FE9F00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070E] sm:mt-[16px]"
|
||||
>
|
||||
<div className="flex items-center gap-[8px]">
|
||||
<ArrowLeft className="h-[16px] w-[16px]" aria-hidden="true" />
|
||||
<span className="text-[14px] font-medium text-white/92">{t('common.back')}</span>
|
||||
</div>
|
||||
<div className="text-[15px] font-semibold text-[#F56E10]">{t('account.title')}</div>
|
||||
<div className="w-[52px]"></div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-0 flex-1 flex-col pt-[20px]">
|
||||
<div className="mx-auto w-full max-w-[1000px]">
|
||||
<div className="mb-[14px] flex flex-col gap-[12px] sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center gap-[10px]">
|
||||
<div className="flex h-[38px] w-[38px] items-center justify-center rounded-[12px] bg-[#FA6A00]/15 text-[#FE9F00]">
|
||||
<MapPinHouse className="h-[18px] w-[18px]" aria-hidden="true" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[16px] font-semibold text-white">{t('account.myShippingAddress')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="liquid-glass-bg inline-flex h-[40px] items-center justify-center gap-[8px] px-[14px] text-sm text-white transition-colors hover:bg-white/28 focus-visible:ring-2 focus-visible:ring-[#FE9F00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070E]"
|
||||
onClick={handleOpenAddAddress}
|
||||
>
|
||||
<Plus className="h-[14px] w-[14px]" aria-hidden="true" />
|
||||
{t('account.addAddress')}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="liquid-glass-bg inline-flex h-[40px] items-center justify-center gap-[8px] px-[14px] text-sm text-white transition-colors hover:bg-white/28 focus-visible:ring-2 focus-visible:ring-[#FE9F00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070E]"
|
||||
onClick={handleOpenAddAddress}
|
||||
>
|
||||
<Plus className="h-[14px] w-[14px]" aria-hidden="true" />
|
||||
Add Address
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{addressBook.loading ? (
|
||||
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
|
||||
Loading address list...
|
||||
</div>
|
||||
) : !addressBook.addresses.length ? (
|
||||
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
|
||||
No shipping address found. Add one.
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-[12px]">
|
||||
{addressBook.addresses.map((address) => {
|
||||
const addressId = String(address.id)
|
||||
const addressText = addressBook.addressOptions.find((option) => option.id === addressId)?.address ?? ''
|
||||
const isDefault = address.default_setting === 1
|
||||
<div className="min-h-0 flex-1 overflow-y-auto pb-[24px]">
|
||||
<div className="mx-auto w-full max-w-[1000px]">
|
||||
{addressBook.loading ? (
|
||||
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
|
||||
{t('account.loadingAddressList')}
|
||||
</div>
|
||||
) : !addressBook.addresses.length ? (
|
||||
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
|
||||
{t('account.noShippingAddressFound')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-[12px]">
|
||||
{addressBook.addresses.map((address) => {
|
||||
const addressId = String(address.id)
|
||||
const addressText = addressBook.addressOptions.find((option) => option.id === addressId)?.address ?? ''
|
||||
const isDefault = address.default_setting === 1
|
||||
|
||||
return (
|
||||
<div key={addressId} className="liquid-glass-bg p-[14px]">
|
||||
<div className="flex items-start justify-between gap-[12px]">
|
||||
<div>
|
||||
<div className="text-[16px] font-semibold text-white">{address.receiver_name}</div>
|
||||
<div className="mt-[4px] text-[13px] text-white/62">{address.phone}</div>
|
||||
</div>
|
||||
<div
|
||||
className={`inline-flex rounded-full px-[10px] py-[5px] text-[12px] ${
|
||||
isDefault
|
||||
? 'bg-[#FA6A00]/14 text-[#FFB36D]'
|
||||
: 'bg-white/6 text-white/62'
|
||||
}`}
|
||||
>
|
||||
{isDefault ? (
|
||||
<span className="inline-flex items-center gap-[5px]">
|
||||
<BadgeCheck className="h-[12px] w-[12px]" aria-hidden="true" />
|
||||
Default
|
||||
</span>
|
||||
) : 'Optional'}
|
||||
</div>
|
||||
return (
|
||||
<div key={addressId} className="liquid-glass-bg p-[14px]">
|
||||
<div className="flex items-start justify-between gap-[12px]">
|
||||
<div>
|
||||
<div className="text-[16px] font-semibold text-white">{address.receiver_name}</div>
|
||||
<div className="mt-[4px] text-[13px] text-white/62">{address.phone}</div>
|
||||
</div>
|
||||
<div
|
||||
className={`inline-flex rounded-full px-[10px] py-[5px] text-[12px] ${
|
||||
isDefault
|
||||
? 'bg-[#FA6A00]/14 text-[#FFB36D]'
|
||||
: 'bg-white/6 text-white/62'
|
||||
}`}
|
||||
>
|
||||
{isDefault ? (
|
||||
<span className="inline-flex items-center gap-[5px]">
|
||||
<BadgeCheck className="h-[12px] w-[12px]" aria-hidden="true" />
|
||||
{t('account.default')}
|
||||
</span>
|
||||
) : t('account.optional')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-[12px] rounded-[10px] bg-black/12 p-[12px]">
|
||||
<div className="text-[12px] uppercase tracking-[0.08em] text-white/44">{t('account.address')}</div>
|
||||
<div className="mt-[6px] text-[13px] leading-[1.6] text-white/78">{addressText}</div>
|
||||
</div>
|
||||
<div className="mt-[12px] flex justify-end gap-[10px]">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-[6px] rounded-full bg-white/6 px-[10px] py-[6px] text-[12px] text-white/82 transition-colors hover:bg-white/10 focus-visible:ring-2 focus-visible:ring-[#FE9F00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070E]"
|
||||
onClick={() => handleOpenEditAddress(address)}
|
||||
>
|
||||
<PencilLine className="h-[12px] w-[12px]" aria-hidden="true" />
|
||||
{t('account.edit')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-[6px] rounded-full bg-[#4B1818]/70 px-[10px] py-[6px] text-[12px] text-[#FFB1B1] transition-colors hover:bg-[#612121]/80 focus-visible:ring-2 focus-visible:ring-[#FE9F00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070E]"
|
||||
onClick={() => setDeleteTarget(address)}
|
||||
>
|
||||
<Trash2 className="h-[12px] w-[12px]" aria-hidden="true" />
|
||||
{t('account.delete')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-[12px] rounded-[10px] bg-black/12 p-[12px]">
|
||||
<div className="text-[12px] uppercase tracking-[0.08em] text-white/44">Address</div>
|
||||
<div className="mt-[6px] text-[13px] leading-[1.6] text-white/78">{addressText}</div>
|
||||
</div>
|
||||
<div className="mt-[12px] flex justify-end gap-[10px]">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-[6px] rounded-full bg-white/6 px-[10px] py-[6px] text-[12px] text-white/82 transition-colors hover:bg-white/10 focus-visible:ring-2 focus-visible:ring-[#FE9F00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070E]"
|
||||
onClick={() => handleOpenEditAddress(address)}
|
||||
>
|
||||
<PencilLine className="h-[12px] w-[12px]" aria-hidden="true" />
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-[6px] rounded-full bg-[#4B1818]/70 px-[10px] py-[6px] text-[12px] text-[#FFB1B1] transition-colors hover:bg-[#612121]/80 focus-visible:ring-2 focus-visible:ring-[#FE9F00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070E]"
|
||||
onClick={() => setDeleteTarget(address)}
|
||||
>
|
||||
<Trash2 className="h-[12px] w-[12px]" aria-hidden="true" />
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)})}
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<GoodsRedeemModal
|
||||
@@ -172,13 +183,13 @@ function AccountPage() {
|
||||
onChangeAddressForm={addressBook.changeAddressForm}
|
||||
forceOpen={addressModalOpen}
|
||||
formOnly
|
||||
titleOverride={editingAddress ? 'Edit Shipping Address' : 'Add Shipping Address'}
|
||||
confirmText={editingAddress ? 'Save Changes' : 'Add Address'}
|
||||
titleOverride={editingAddress ? t('goods.editShippingAddress') : t('goods.addShippingAddress')}
|
||||
confirmText={editingAddress ? t('goods.saveChanges') : t('account.addAddress')}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
open={Boolean(deleteTarget)}
|
||||
title="Delete Address"
|
||||
title={t('account.deleteAddress')}
|
||||
onClose={() => setDeleteTarget(null)}
|
||||
className="max-w-[420px]"
|
||||
bodyClassName="space-y-[18px]"
|
||||
@@ -191,7 +202,7 @@ function AccountPage() {
|
||||
onClick={() => setDeleteTarget(null)}
|
||||
disabled={addressBook.deleteLoading}
|
||||
>
|
||||
Cancel
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -199,13 +210,13 @@ function AccountPage() {
|
||||
onClick={handleConfirmDelete}
|
||||
disabled={addressBook.deleteLoading}
|
||||
>
|
||||
{addressBook.deleteLoading ? 'Deleting...' : 'Delete'}
|
||||
{addressBook.deleteLoading ? t('common.processing') : t('account.delete')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="rounded-[12px] bg-[#1C1818]/82 px-[14px] py-[18px] text-[15px] leading-[1.65] text-white/92 shadow-[0_10px_30px_rgba(0,0,0,0.2)]">
|
||||
{deleteTarget ? `Delete the address for ${deleteTarget.receiver_name}? ` : ''}
|
||||
{deleteTarget ? t('account.deleteAddressFor', {name: deleteTarget.receiver_name}) : ''}
|
||||
</div>
|
||||
</Modal>
|
||||
</PageLayout>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import {ArrowLeft} from 'lucide-react'
|
||||
import {useTranslation} from 'react-i18next'
|
||||
import {Link, useSearchParams} from 'react-router-dom'
|
||||
|
||||
import PageLayout from '@/components/layout'
|
||||
import {HOME_GOOD_TYPE_ORDER} from '@/constant'
|
||||
import {HOME_CATEGORY_META_MAP, HOME_GOOD_TYPE_ORDER} from '@/constant'
|
||||
import {
|
||||
GoodsCategoryList,
|
||||
GoodsRedeemModal,
|
||||
@@ -12,12 +13,14 @@ import {
|
||||
} from '@/features/goods'
|
||||
|
||||
function GoodsPage() {
|
||||
const {t} = useTranslation()
|
||||
const [searchParams] = useSearchParams()
|
||||
const queryType = searchParams.get('type')
|
||||
const selectedType = isGoodsType(queryType) ? queryType : HOME_GOOD_TYPE_ORDER[0]
|
||||
const {productCategories, loading} = useGoodsCatalog({types: [selectedType]})
|
||||
const redeem = useGoodsRedeem()
|
||||
const visibleCategories = productCategories.filter((category) => category.id === selectedType)
|
||||
const pageTitle = t(HOME_CATEGORY_META_MAP[selectedType].nameKey)
|
||||
|
||||
return (
|
||||
<PageLayout contentClassName="mx-auto flex min-h-screen w-full max-w-[1180px] flex-col px-4 pb-8 sm:px-6 lg:px-8">
|
||||
@@ -27,9 +30,9 @@ function GoodsPage() {
|
||||
>
|
||||
<div className="flex items-center gap-[8px]">
|
||||
<ArrowLeft className="h-[16px] w-[16px]" aria-hidden="true"/>
|
||||
<span className="text-[14px] font-medium text-white/92">Back</span>
|
||||
<span className="text-[14px] font-medium text-white/92">{t('common.back')}</span>
|
||||
</div>
|
||||
<div className="text-[15px] font-semibold text-[#F56E10]">{queryType}</div>
|
||||
<div className="text-[15px] font-semibold text-[#F56E10]">{pageTitle}</div>
|
||||
<div className="w-[52px]"></div>
|
||||
</Link>
|
||||
|
||||
@@ -39,7 +42,7 @@ function GoodsPage() {
|
||||
<GoodsCategoryList
|
||||
categories={visibleCategories}
|
||||
loading={loading}
|
||||
emptyText="No goods found for this category."
|
||||
emptyText={t('goods.noGoodsForCategory')}
|
||||
onRedeem={redeem.openRedeemModal}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {useState} from 'react'
|
||||
|
||||
import {useMutation} from '@tanstack/react-query'
|
||||
import {Languages} from 'lucide-react'
|
||||
import {useTranslation} from 'react-i18next'
|
||||
|
||||
import PageLayout from '@/components/layout'
|
||||
import Modal from '@/components/modal'
|
||||
@@ -11,8 +13,8 @@ import {
|
||||
Coins,
|
||||
Gauge,
|
||||
History,
|
||||
UserRound,
|
||||
Wallet,
|
||||
UserRound,
|
||||
} from 'lucide-react'
|
||||
import type {
|
||||
ProductCategory,
|
||||
@@ -30,6 +32,7 @@ import {validateClaimSubmission} from '@/features/home/claimValidation'
|
||||
import {claim} from '@/api/business.ts'
|
||||
import {notifyError, notifySuccess} from '@/features/notifications'
|
||||
import {useUserStore} from "@/store/user.ts";
|
||||
import {normalizeLanguage} from '@/lib/i18n'
|
||||
|
||||
function QuickNavCard({icon: Icon, label, to}: QuickNavCardProps) {
|
||||
return (
|
||||
@@ -58,9 +61,12 @@ function getProgressPercent(current = 0, total = 0) {
|
||||
}
|
||||
|
||||
function HomePage() {
|
||||
const {t} = useTranslation()
|
||||
const [claimModalOpen, setClaimModalOpen] = useState(false)
|
||||
const navigate = useNavigate()
|
||||
const authInfo = useUserStore(state => state.authInfo)
|
||||
const language = useUserStore((state) => state.language)
|
||||
const setLanguage = useUserStore((state) => state.setLanguage)
|
||||
const {productCategories, loading} = useGoodsCatalog()
|
||||
const {invalidateAssets} = useAssetsRefresh()
|
||||
const redeem = useGoodsRedeem()
|
||||
@@ -78,12 +84,18 @@ function HomePage() {
|
||||
|
||||
const {assetsInfo} = useAssetsQuery()
|
||||
const claimProgress = getProgressPercent(assetsInfo?.today_claimed, assetsInfo?.today_limit)
|
||||
const isClaimAvailable = (assetsInfo?.locked_points ?? 0) > 0
|
||||
const previewCategories: ProductCategory[] = productCategories.map((category) => ({
|
||||
...category,
|
||||
items: category.items.slice(0, 4),
|
||||
}))
|
||||
|
||||
const handleOpenClaimModal = () => {
|
||||
if (!isClaimAvailable) {
|
||||
notifyError(t('home.noClaimablePointsAvailable'))
|
||||
return
|
||||
}
|
||||
|
||||
setClaimModalOpen(true)
|
||||
}
|
||||
|
||||
@@ -95,14 +107,14 @@ function HomePage() {
|
||||
const handleSyncBalance = async () => {
|
||||
try {
|
||||
await syncBalanceMutation.mutateAsync()
|
||||
notifySuccess('Balance synced successfully.')
|
||||
notifySuccess(t('home.balanceSyncedSuccessfully'))
|
||||
} catch {
|
||||
// request interceptor handles interface error toast
|
||||
}
|
||||
}
|
||||
|
||||
const handleConfirmClaim = async () => {
|
||||
const claimValidation = validateClaimSubmission(authInfo)
|
||||
const claimValidation = validateClaimSubmission(authInfo, assetsInfo)
|
||||
if (!claimValidation.valid) {
|
||||
notifyError(claimValidation.message)
|
||||
return
|
||||
@@ -111,7 +123,7 @@ function HomePage() {
|
||||
try {
|
||||
const response = await claimMutation.mutateAsync(`${authInfo!.user_id}${Date.now()}`)
|
||||
await invalidateAssets()
|
||||
notifySuccess(response, 'Claim submitted successfully.')
|
||||
notifySuccess(response, t('home.claimSubmittedSuccessfully'))
|
||||
setClaimModalOpen(false)
|
||||
} catch {
|
||||
// request errors are surfaced by the shared request toast
|
||||
@@ -122,47 +134,61 @@ function HomePage() {
|
||||
navigate(`/goods?type=${type}`)
|
||||
}
|
||||
|
||||
const handleToggleLanguage = () => {
|
||||
const currentLanguage = normalizeLanguage(language)
|
||||
setLanguage(currentLanguage === 'zh' ? 'en' : 'zh')
|
||||
}
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div
|
||||
className="grid grid-cols-2 gap-2 py-[14px] sm:ml-auto sm:flex sm:w-auto sm:grid-cols-none sm:justify-end">
|
||||
<QuickNavCard to="/record" icon={History} label="record"/>
|
||||
<QuickNavCard to="/account" icon={UserRound} label="account"/>
|
||||
className="grid grid-cols-3 gap-2 py-[14px] sm:ml-auto sm:flex sm:w-auto sm:grid-cols-none sm:justify-end">
|
||||
<QuickNavCard to="/record" icon={History} label={t('nav.record')}/>
|
||||
<QuickNavCard to="/account" icon={UserRound} label={t('nav.account')}/>
|
||||
<button
|
||||
type="button"
|
||||
className="liquid-glass-bg flex items-center justify-between gap-[12px] rounded-[12px] px-[12px] py-[8px] text-[13px] text-white/88 transition-colors hover:bg-white/28 focus-visible:ring-2 focus-visible:ring-[#FE9F00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070E]"
|
||||
onClick={handleToggleLanguage}
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-[10px]">
|
||||
<div className="flex h-[32px] w-[32px] items-center justify-center rounded-[10px] bg-linear-to-b from-[#FB8001] to-[#FCAA2C] text-white shadow-[0_8px_18px_rgba(250,109,2,0.24)]">
|
||||
<Languages className="h-[16px] w-[16px] shrink-0" aria-hidden="true"/>
|
||||
</div>
|
||||
<div className="truncate font-medium">{normalizeLanguage(language) === 'zh' ? t('nav.switchToEnglish') : t('nav.switchToChinese')}</div>
|
||||
</div>
|
||||
<ChevronRight className="h-[16px] w-[16px] shrink-0 text-white/70" aria-hidden="true"/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-[4px]">
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-stretch">
|
||||
<div className="grid grid-cols-2 gap-3 lg:w-[544px] lg:shrink-0">
|
||||
<div className="liquid-glass-bg flex min-h-[167px] flex-col justify-between p-[14px]">
|
||||
<div className="liquid-glass-bg flex min-h-[167px] flex-col justify-between p-[12px] sm:p-[14px]">
|
||||
<div className="flex items-start justify-between gap-[12px]">
|
||||
<div>
|
||||
<div className="text-[13px] tracking-[0.16em] text-white/58">Claimable
|
||||
Points
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-[12px] tracking-[0.1em] text-white/58 sm:text-[13px] sm:tracking-[0.16em]">{t('home.claimablePoints')}</div>
|
||||
<div
|
||||
className="mt-[10px] text-[34px] font-semibold leading-none text-white">{assetsInfo?.locked_points || 0}</div>
|
||||
className="mt-[10px] min-w-0 break-all text-[clamp(1.25rem,8vw,1.625rem)] font-semibold leading-[1.05] text-white sm:text-[34px]">{assetsInfo?.locked_points || 0}</div>
|
||||
</div>
|
||||
<div
|
||||
className="flex h-[38px] w-[38px] items-center justify-center rounded-[12px] bg-[#FA6A00]/16 text-[#FE9F00]">
|
||||
<Coins className="h-[18px] w-[18px]" aria-hidden="true"/>
|
||||
className="flex h-[34px] w-[34px] shrink-0 items-center justify-center rounded-[10px] bg-[#FA6A00]/16 text-[#FE9F00] sm:h-[38px] sm:w-[38px] sm:rounded-[12px]">
|
||||
<Coins className="h-[16px] w-[16px] sm:h-[18px] sm:w-[18px]" aria-hidden="true"/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-w-[28ch] text-[13px] leading-[1.6] text-white/68">
|
||||
Yesterday's losses have been converted into points. Claim them to use in rewards.
|
||||
<div className="max-w-[20ch] text-[12px] leading-[1.5] text-white/68 sm:max-w-[28ch] sm:text-[13px] sm:leading-[1.6]">
|
||||
{t('home.claimDescription')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="liquid-glass-bg flex min-h-[167px] flex-col justify-around p-[14px]">
|
||||
<div className="liquid-glass-bg flex min-h-[167px] flex-col justify-around p-[12px] sm:p-[14px]">
|
||||
<div className="flex items-start justify-between gap-[12px]">
|
||||
<div>
|
||||
<div className="text-[13px] tracking-[0.16em] text-white/58">Daily Claim
|
||||
Limit
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-[12px] tracking-[0.1em] text-white/58 sm:text-[13px] sm:tracking-[0.16em]">{t('home.dailyClaimLimit')}</div>
|
||||
<div
|
||||
className="mt-[10px] text-[34px] font-semibold leading-none text-white">{assetsInfo?.locked_points}</div>
|
||||
className="mt-[10px] min-w-0 break-all text-[clamp(1.25rem,8vw,1.625rem)] font-semibold leading-[1.05] text-white sm:text-[34px]">{assetsInfo?.today_limit || 0}</div>
|
||||
</div>
|
||||
<div
|
||||
className="flex h-[38px] w-[38px] items-center justify-center rounded-[12px] bg-[#FA6A00]/16 text-[#FE9F00]">
|
||||
<Gauge className="h-[18px] w-[18px]" aria-hidden="true"/>
|
||||
className="flex h-[34px] w-[34px] shrink-0 items-center justify-center rounded-[10px] bg-[#FA6A00]/16 text-[#FE9F00] sm:h-[38px] sm:w-[38px] sm:rounded-[12px]">
|
||||
<Gauge className="h-[16px] w-[16px] sm:h-[18px] sm:w-[18px]" aria-hidden="true"/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -178,7 +204,7 @@ function HomePage() {
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
className="mt-[10px] text-[13px] text-white/68">Claimed: <span
|
||||
className="mt-[10px] text-[12px] leading-[1.5] text-white/68 sm:text-[13px]">{t('home.claimed')}: <span
|
||||
className={'text-[#FE9C00]'}>{assetsInfo?.today_claimed || 0}</span> / {assetsInfo?.today_limit || 0}
|
||||
</div>
|
||||
</div>
|
||||
@@ -189,13 +215,11 @@ function HomePage() {
|
||||
className="liquid-glass-bg flex min-h-[100px] flex-col justify-between p-[14px] sm:p-[16px]">
|
||||
<div className="flex items-center justify-between gap-[12px]">
|
||||
<div>
|
||||
<div className="text-[13px] tracking-[0.16em] text-white/58">Available for
|
||||
Withdrawal (Cash)
|
||||
</div>
|
||||
<div className="text-[13px] tracking-[0.16em] text-white/58">{t('home.availableForWithdrawal')}</div>
|
||||
<div
|
||||
className="mt-[10px] text-[32px] font-semibold leading-none text-white">{assetsInfo?.withdrawable_cash || 0}
|
||||
className="mt-[10px] min-w-0 break-all text-[clamp(1.25rem,8vw,2rem)] font-semibold leading-[1.05] text-white sm:text-[32px]">{assetsInfo?.withdrawable_cash || 0}
|
||||
<span
|
||||
className="text-[13px] tracking-[0.16em] text-white/58 ml-[10px]">CNY</span>
|
||||
className="text-[13px] tracking-[0.16em] text-white/58 ml-[10px]">{t('home.cashUnit')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -205,8 +229,11 @@ function HomePage() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="liquid-glass-bg grid grid-cols-2 gap-[10px] p-[5px]">
|
||||
<Button className="h-[44px] w-full text-[13px]" onClick={handleOpenClaimModal}>
|
||||
Claim Now
|
||||
<Button
|
||||
className="h-[44px] w-full text-[13px]"
|
||||
onClick={handleOpenClaimModal}
|
||||
>
|
||||
{t('home.claimNow')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={'gray'}
|
||||
@@ -214,7 +241,7 @@ function HomePage() {
|
||||
onClick={handleSyncBalance}
|
||||
disabled={syncBalanceMutation.isPending}
|
||||
>
|
||||
{syncBalanceMutation.isPending ? 'Syncing...' : 'Sync Balance'}
|
||||
{syncBalanceMutation.isPending ? t('home.syncing') : t('home.syncBalance')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -224,7 +251,7 @@ function HomePage() {
|
||||
<GoodsCategoryList
|
||||
categories={previewCategories}
|
||||
loading={loading}
|
||||
emptyText="No goods available yet."
|
||||
emptyText={t('goods.noGoodsAvailableYet')}
|
||||
showMore
|
||||
onMoreClick={handleMoreClick}
|
||||
onRedeem={redeem.openRedeemModal}
|
||||
@@ -249,7 +276,7 @@ function HomePage() {
|
||||
|
||||
<Modal
|
||||
open={claimModalOpen}
|
||||
title="Confirm Claim"
|
||||
title={t('home.confirmClaim')}
|
||||
onClose={handleCloseClaimModal}
|
||||
className="max-w-[560px]"
|
||||
bodyClassName="space-y-[18px]"
|
||||
@@ -258,25 +285,24 @@ function HomePage() {
|
||||
<Button type="button" variant={'gray'} className="h-[38px] w-full sm:w-auto sm:min-w-[130px]"
|
||||
onClick={handleCloseClaimModal}
|
||||
disabled={claimMutation.isPending}>
|
||||
Cancel
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
|
||||
<Button type="button" className="h-[38px] w-full sm:w-auto sm:min-w-[130px]"
|
||||
onClick={handleConfirmClaim}
|
||||
disabled={claimMutation.isPending}>
|
||||
{claimMutation.isPending ? 'Processing...' : 'Confirm'}
|
||||
disabled={claimMutation.isPending || !isClaimAvailable}>
|
||||
{claimMutation.isPending ? t('common.processing') : t('common.confirm')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="rounded-[12px] bg-[#1C1818]/82 px-[14px] py-[18px] text-[17px] leading-[1.65] text-white/92 shadow-[0_10px_30px_rgba(0,0,0,0.2)]">
|
||||
After converting the points to be collected into usable points, they can be redeemed or withdrawn.
|
||||
Are you sure to claim it?
|
||||
{t('home.confirmClaimDescription')}
|
||||
</div>
|
||||
</Modal>
|
||||
</PageLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default HomePage;
|
||||
export default HomePage
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import {useState} from 'react'
|
||||
import {useQuery} from '@tanstack/react-query'
|
||||
import {useTranslation} from 'react-i18next'
|
||||
|
||||
import PageLayout from '@/components/layout'
|
||||
import {ORDER_STATUS} from '@/constant'
|
||||
import i18n from '@/lib/i18n'
|
||||
import { cn } from '@/lib'
|
||||
import Modal from '@/components/modal'
|
||||
import Button from '@/components/button'
|
||||
@@ -63,13 +65,13 @@ function getOrderStatus(status?: string | number) {
|
||||
if (matchedStatus) {
|
||||
switch (matchedStatus) {
|
||||
case 'PENDING':
|
||||
return 'Pending'
|
||||
return i18n.t('record.statusLabel.pending')
|
||||
case 'COMPLETED':
|
||||
return 'Completed'
|
||||
return i18n.t('record.statusLabel.completed')
|
||||
case 'SHIPPED':
|
||||
return 'Shipped'
|
||||
return i18n.t('record.statusLabel.shipped')
|
||||
case 'REJECTED':
|
||||
return 'Rejected'
|
||||
return i18n.t('record.statusLabel.rejected')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,20 +83,20 @@ function getOrderStatus(status?: string | number) {
|
||||
if (typeof normalizedStatus === 'number') {
|
||||
switch (normalizedStatus) {
|
||||
case 0:
|
||||
return 'Pending'
|
||||
return i18n.t('record.statusLabel.pending')
|
||||
case 1:
|
||||
return 'Completed'
|
||||
return i18n.t('record.statusLabel.completed')
|
||||
case 2:
|
||||
return 'Shipped'
|
||||
return i18n.t('record.statusLabel.shipped')
|
||||
case 3:
|
||||
return 'Rejected'
|
||||
return i18n.t('record.statusLabel.rejected')
|
||||
default:
|
||||
return String(normalizedStatus)
|
||||
}
|
||||
}
|
||||
|
||||
if (!normalizedStatus) {
|
||||
return 'Pending'
|
||||
return i18n.t('record.statusLabel.pending')
|
||||
}
|
||||
|
||||
return toTitleCase(normalizedStatus)
|
||||
@@ -102,7 +104,17 @@ function getOrderStatus(status?: string | number) {
|
||||
|
||||
function getOrderCategory(item: OrderItem) {
|
||||
if (item.type?.trim()) {
|
||||
return item.type.trim().toUpperCase()
|
||||
const normalizedType = item.type.trim().toUpperCase()
|
||||
switch (normalizedType) {
|
||||
case 'BONUS':
|
||||
return i18n.t('record.categories.bonus')
|
||||
case 'PHYSICAL':
|
||||
return i18n.t('record.categories.physical')
|
||||
case 'WITHDRAW':
|
||||
return i18n.t('record.categories.withdraw')
|
||||
default:
|
||||
return toTitleCase(normalizedType)
|
||||
}
|
||||
}
|
||||
|
||||
if (item.type_title) {
|
||||
@@ -116,11 +128,11 @@ function getOrderCategory(item: OrderItem) {
|
||||
if (item.type) {
|
||||
switch (item.type) {
|
||||
case 'BONUS':
|
||||
return 'Bonus'
|
||||
return i18n.t('record.categories.bonus')
|
||||
case 'PHYSICAL':
|
||||
return 'Physical'
|
||||
return i18n.t('record.categories.physical')
|
||||
case 'WITHDRAW':
|
||||
return 'Transfer to Platform'
|
||||
return i18n.t('record.categories.withdraw')
|
||||
default:
|
||||
return toTitleCase(item.type)
|
||||
}
|
||||
@@ -130,7 +142,7 @@ function getOrderCategory(item: OrderItem) {
|
||||
return toTitleCase(item.category)
|
||||
}
|
||||
|
||||
return 'Order'
|
||||
return i18n.t('record.categories.order')
|
||||
}
|
||||
|
||||
function getOrderPoints(item: OrderItem) {
|
||||
@@ -143,11 +155,11 @@ function getOrderPoints(item: OrderItem) {
|
||||
const numericValue = typeof rawValue === 'string' ? Number(rawValue) : rawValue
|
||||
if (Number.isNaN(numericValue)) {
|
||||
const textValue = String(rawValue)
|
||||
return {display: `${textValue} points`}
|
||||
return {display: `${textValue} ${i18n.t('common.points')}`}
|
||||
}
|
||||
|
||||
return {
|
||||
display: `${numericValue} points`,
|
||||
display: `${numericValue} ${i18n.t('common.points')}`,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +188,7 @@ function mapOrderItemToRecord(item: OrderItem): OrderRecord {
|
||||
date,
|
||||
time,
|
||||
category: getOrderCategory(item),
|
||||
title: item.item_title ?? item.title ?? item.mallItem?.title ?? 'Untitled Order',
|
||||
title: item.item_title ?? item.title ?? item.mallItem?.title ?? i18n.t('record.untitledOrder'),
|
||||
trackingNumber: getTrackingNumber(item),
|
||||
status: getOrderStatus(item.status),
|
||||
points: points.display,
|
||||
@@ -221,7 +233,7 @@ function getPointsRecordTitle(item: PointsLogItem) {
|
||||
item.type,
|
||||
item.description,
|
||||
item.remark?.split('\n')[0],
|
||||
].find((value) => typeof value === 'string' && value.trim())?.trim() ?? 'Points Record'
|
||||
].find((value) => typeof value === 'string' && value.trim())?.trim() ?? i18n.t('record.pointsRecordFallback')
|
||||
}
|
||||
|
||||
function mapPointsLogItemToRecord(item: PointsLogItem) {
|
||||
@@ -239,18 +251,24 @@ function mapPointsLogItemToRecord(item: PointsLogItem) {
|
||||
}
|
||||
|
||||
function getOrderStatusClassName(status: string) {
|
||||
switch (status.toLowerCase()) {
|
||||
case 'completed':
|
||||
return 'bg-[#9BFFC0] text-[#176640]'
|
||||
case 'shipped':
|
||||
return 'bg-[#95F0FF] text-[#116A79]'
|
||||
case 'pending':
|
||||
return 'bg-[#FFF18C] text-[#7F6A0D]'
|
||||
case 'rejected':
|
||||
return 'bg-[#FFB1C0] text-[#7C2941]'
|
||||
default:
|
||||
return 'bg-white/15 text-white/80'
|
||||
const normalizedStatus = status.toLowerCase()
|
||||
if (normalizedStatus === 'completed' || status === i18n.t('record.statusLabel.completed')) {
|
||||
return 'bg-[#9BFFC0] text-[#176640]'
|
||||
}
|
||||
|
||||
if (normalizedStatus === 'shipped' || status === i18n.t('record.statusLabel.shipped')) {
|
||||
return 'bg-[#95F0FF] text-[#116A79]'
|
||||
}
|
||||
|
||||
if (normalizedStatus === 'pending' || status === i18n.t('record.statusLabel.pending')) {
|
||||
return 'bg-[#FFF18C] text-[#7F6A0D]'
|
||||
}
|
||||
|
||||
if (normalizedStatus === 'rejected' || status === i18n.t('record.statusLabel.rejected')) {
|
||||
return 'bg-[#FFB1C0] text-[#7C2941]'
|
||||
}
|
||||
|
||||
return 'bg-white/15 text-white/80'
|
||||
}
|
||||
|
||||
function TabButton({ active, label, icon: Icon, onClick }: TabButtonProps) {
|
||||
@@ -272,8 +290,9 @@ function TabButton({ active, label, icon: Icon, onClick }: TabButtonProps) {
|
||||
}
|
||||
|
||||
function OrderCard({ record, onOpenDetails }: OrderCardProps) {
|
||||
const {t} = useTranslation()
|
||||
return (
|
||||
<div className="overflow-hidden rounded-[12px] shadow-[0_10px_30px_rgba(0,0,0,0.24)]">
|
||||
<div className="shrink-0 overflow-hidden rounded-[12px] shadow-[0_10px_30px_rgba(0,0,0,0.24)]">
|
||||
<div className="bg-linear-to-r from-[#F96C02] to-[#FE9F00] px-[12px] py-[9px] text-[14px] text-white">
|
||||
{record.date} {record.time} • {record.category}
|
||||
</div>
|
||||
@@ -282,7 +301,7 @@ function OrderCard({ record, onOpenDetails }: OrderCardProps) {
|
||||
<div className="text-[16px] font-medium text-white">{record.title}</div>
|
||||
{record.trackingNumber ? (
|
||||
<div className="mt-[4px] text-[13px] text-white/45">
|
||||
Tracking Number {record.trackingNumber}
|
||||
{t('record.trackingNumber')} {record.trackingNumber}
|
||||
</div>
|
||||
) : null}
|
||||
<button
|
||||
@@ -290,7 +309,7 @@ function OrderCard({ record, onOpenDetails }: OrderCardProps) {
|
||||
className="mt-[10px] inline-flex items-center gap-[5px] text-[13px] text-[#FA6A00] focus-visible:ring-2 focus-visible:ring-[#FE9F00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070E]"
|
||||
onClick={() => onOpenDetails(record)}
|
||||
>
|
||||
Check the details
|
||||
{t('record.checkDetails')}
|
||||
<ChevronRight className="h-[14px] w-[14px]" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -313,7 +332,7 @@ function OrderCard({ record, onOpenDetails }: OrderCardProps) {
|
||||
|
||||
function PointsCard({ record }: PointsCardProps) {
|
||||
return (
|
||||
<div className="overflow-hidden rounded-[12px] shadow-[0_10px_30px_rgba(0,0,0,0.24)]">
|
||||
<div className="shrink-0 overflow-hidden rounded-[12px] shadow-[0_10px_30px_rgba(0,0,0,0.24)]">
|
||||
<div className="bg-linear-to-r from-[#F96C02] to-[#FE9F00] px-[12px] py-[9px] text-[15px] text-white">
|
||||
{record.title}
|
||||
</div>
|
||||
@@ -341,6 +360,7 @@ function OrdersTabContent({
|
||||
sessionId: string
|
||||
onOpenDetails: (record: OrderRecord) => void
|
||||
}) {
|
||||
const {t} = useTranslation()
|
||||
const ordersQuery = useQuery({
|
||||
queryKey: queryKeys.orders(sessionId),
|
||||
enabled: Boolean(sessionId),
|
||||
@@ -358,7 +378,7 @@ function OrdersTabContent({
|
||||
if (ordersQuery.isPending) {
|
||||
return (
|
||||
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
|
||||
Loading...
|
||||
{t('record.loading')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -366,21 +386,22 @@ function OrdersTabContent({
|
||||
if (!orderRecords.length) {
|
||||
return (
|
||||
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
|
||||
No Data
|
||||
{t('record.noData')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-[12px] pb-[4px]">
|
||||
{orderRecords.map((record) => (
|
||||
<OrderCard key={record.id} record={record} onOpenDetails={onOpenDetails} />
|
||||
))}
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PointsTabContent({sessionId}: {sessionId: string}) {
|
||||
const {t} = useTranslation()
|
||||
const pointsLogsQuery = useQuery({
|
||||
queryKey: queryKeys.pointsLogs(sessionId),
|
||||
enabled: Boolean(sessionId),
|
||||
@@ -398,7 +419,7 @@ function PointsTabContent({sessionId}: {sessionId: string}) {
|
||||
if (pointsLogsQuery.isPending) {
|
||||
return (
|
||||
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
|
||||
Loading...
|
||||
{t('record.loading')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -406,19 +427,20 @@ function PointsTabContent({sessionId}: {sessionId: string}) {
|
||||
if (!pointsRecords.length) {
|
||||
return (
|
||||
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
|
||||
No Data
|
||||
{t('record.noData')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-[12px] pb-[4px]">
|
||||
{pointsRecords.map((record) => <PointsCard key={record.id} record={record} />)}
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RecordPage() {
|
||||
const {t} = useTranslation()
|
||||
const sessionId = useUserStore((state) => state.authInfo?.session_id ?? '')
|
||||
const [tab, setTab] = useState<RecordButtonType>('order')
|
||||
const [selectedOrder, setSelectedOrder] = useState<OrderRecord | null>(null)
|
||||
@@ -428,78 +450,84 @@ function RecordPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<PageLayout contentClassName="mx-auto flex min-h-screen w-full max-w-[980px] flex-col px-4 pb-8 sm:px-6 lg:px-8">
|
||||
<Link
|
||||
to="/"
|
||||
className="mt-[12px] flex h-[44px] items-center justify-between rounded-[12px] bg-[#08070E]/72 px-[14px] text-[#F56E10] transition-colors hover:bg-[#0D0A14]/80 focus-visible:ring-2 focus-visible:ring-[#FE9F00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070E] sm:mt-[16px]"
|
||||
>
|
||||
<div className="flex items-center gap-[8px]">
|
||||
<ArrowLeft className="h-[16px] w-[16px]" aria-hidden="true" />
|
||||
<span className="text-[14px] font-medium text-white/92">Back</span>
|
||||
</div>
|
||||
<div className="text-[15px] font-semibold text-[#F56E10]">Record</div>
|
||||
<div className="w-[52px]"></div>
|
||||
</Link>
|
||||
|
||||
<div className="mx-auto w-full max-w-[860px] pt-[18px] pb-[24px]">
|
||||
<div className="mb-[12px] flex justify-end">
|
||||
<div className="flex gap-[8px]">
|
||||
<TabButton
|
||||
active={tab === 'order'}
|
||||
icon={PackageSearch}
|
||||
label="My Orders"
|
||||
onClick={() => {
|
||||
setSelectedOrder(null)
|
||||
setTab('order')
|
||||
}}
|
||||
/>
|
||||
<TabButton
|
||||
active={tab === 'record'}
|
||||
icon={Coins}
|
||||
label="Points Record"
|
||||
onClick={() => {
|
||||
setSelectedOrder(null)
|
||||
setTab('record')
|
||||
}}
|
||||
/>
|
||||
<PageLayout contentClassName="flex h-[100svh] w-full flex-col overflow-hidden px-4 pb-8 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto w-full max-w-[980px]">
|
||||
<Link
|
||||
to="/"
|
||||
className="mt-[12px] flex h-[44px] items-center justify-between rounded-[12px] bg-[#08070E]/72 px-[14px] text-[#F56E10] transition-colors hover:bg-[#0D0A14]/80 focus-visible:ring-2 focus-visible:ring-[#FE9F00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070E] sm:mt-[16px]"
|
||||
>
|
||||
<div className="flex items-center gap-[8px]">
|
||||
<ArrowLeft className="h-[16px] w-[16px]" aria-hidden="true" />
|
||||
<span className="text-[14px] font-medium text-white/92">{t('common.back')}</span>
|
||||
</div>
|
||||
<div className="text-[15px] font-semibold text-[#F56E10]">{t('record.title')}</div>
|
||||
<div className="w-[52px]"></div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-0 flex-1 flex-col pt-[18px] pb-[24px]">
|
||||
<div className="mx-auto w-full max-w-[860px]">
|
||||
<div className="mb-[12px] flex justify-end">
|
||||
<div className="flex gap-[8px]">
|
||||
<TabButton
|
||||
active={tab === 'order'}
|
||||
icon={PackageSearch}
|
||||
label={t('record.myOrders')}
|
||||
onClick={() => {
|
||||
setSelectedOrder(null)
|
||||
setTab('order')
|
||||
}}
|
||||
/>
|
||||
<TabButton
|
||||
active={tab === 'record'}
|
||||
icon={Coins}
|
||||
label={t('record.pointsRecord')}
|
||||
onClick={() => {
|
||||
setSelectedOrder(null)
|
||||
setTab('record')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-px bg-white/16"></div>
|
||||
</div>
|
||||
|
||||
<div className="h-px bg-white/16"></div>
|
||||
|
||||
<div className="mt-[14px] flex flex-col gap-[12px]">
|
||||
{tab === 'order' ? (
|
||||
<OrdersTabContent key="order" sessionId={sessionId} onOpenDetails={setSelectedOrder} />
|
||||
) : (
|
||||
<PointsTabContent key="record" sessionId={sessionId} />
|
||||
)}
|
||||
<div className="mt-[14px] min-h-0 flex-1 overflow-y-auto">
|
||||
<div className="mx-auto w-full max-w-[860px]">
|
||||
{tab === 'order' ? (
|
||||
<OrdersTabContent key="order" sessionId={sessionId} onOpenDetails={setSelectedOrder} />
|
||||
) : (
|
||||
<PointsTabContent key="record" sessionId={sessionId} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
open={Boolean(selectedOrder)}
|
||||
title="Order Details"
|
||||
title={t('record.orderDetails')}
|
||||
onClose={handleCloseDetails}
|
||||
className="max-w-[420px]"
|
||||
bodyClassName="pt-[0px]"
|
||||
footer={
|
||||
<Button type="button" className="h-[36px] w-full sm:min-w-[94px] sm:w-auto" onClick={handleCloseDetails}>
|
||||
Close
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{selectedOrder ? (
|
||||
<div className="mt-[10px] rounded-[10px] bg-[#1C1818]/78 px-[12px] py-[6px]">
|
||||
{[
|
||||
{ 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: t('record.orderNumber'), value: selectedOrder.orderNumber ?? '--' },
|
||||
{ label: t('record.orderTime'), value: `${selectedOrder.date} ${selectedOrder.time}` },
|
||||
{ label: t('record.orderType'), value: selectedOrder.category },
|
||||
{ label: t('record.itemName'), value: selectedOrder.title },
|
||||
...(selectedOrder.trackingNumber
|
||||
? [{ label: 'Tracking Number', value: selectedOrder.trackingNumber }]
|
||||
? [{ label: t('record.trackingNumber'), value: selectedOrder.trackingNumber }]
|
||||
: []),
|
||||
{ label: 'Points', value: selectedOrder.points.replace(/^-/, '') },
|
||||
{ label: 'Status', value: selectedOrder.status },
|
||||
{ label: t('record.points'), value: selectedOrder.points.replace(/^-/, '') },
|
||||
{ label: t('record.status'), value: selectedOrder.status },
|
||||
].map((item) => (
|
||||
<div key={item.label} className="border-b border-white/8 py-[10px] last:border-b-0">
|
||||
<div className="text-[13px] text-white/48">{item.label}</div>
|
||||
|
||||
Reference in New Issue
Block a user