feat:项目界面补充
This commit is contained in:
@@ -21,8 +21,6 @@ function App() {
|
||||
//
|
||||
// const {token, language} = message.payload
|
||||
//
|
||||
// console.log('postMessage', token, language)
|
||||
//
|
||||
// if (typeof token === 'string' && token.trim()) {
|
||||
// sessionStorage.setItem('host_token', token)
|
||||
// }
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import {Children} from 'react'
|
||||
|
||||
import { cn } from '@/lib'
|
||||
import type { ModalProps } from '@/types'
|
||||
|
||||
@@ -15,6 +17,9 @@ export function Modal({
|
||||
return null
|
||||
}
|
||||
|
||||
const footerItems = Children.toArray(footer)
|
||||
const hasMultipleFooterItems = footerItems.length > 1
|
||||
|
||||
const handleOverlayClick = () => {
|
||||
if (closeOnOverlayClick) {
|
||||
onClose?.()
|
||||
@@ -62,8 +67,15 @@ export function Modal({
|
||||
</div>
|
||||
{footer ? (
|
||||
<div className="shrink-0 border-t border-white/8 px-[16px] py-[14px] sm:px-[20px] sm:py-[16px]">
|
||||
<div className="flex flex-wrap items-center justify-center gap-3 sm:gap-6">
|
||||
{footer}
|
||||
<div
|
||||
className={cn(
|
||||
'items-center justify-center gap-3 sm:gap-6',
|
||||
hasMultipleFooterItems
|
||||
? 'flex flex-nowrap [&>*]:min-w-0 [&>*]:flex-1 sm:[&>*]:flex-none'
|
||||
: 'flex',
|
||||
)}
|
||||
>
|
||||
{footerItems}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import type { BorderlessTableProps } from '@/types'
|
||||
|
||||
function BorderlessTable<T extends Record<string, string>>({
|
||||
columns,
|
||||
dataSource,
|
||||
}: BorderlessTableProps<T>) {
|
||||
return (
|
||||
<div className="overflow-x-auto rounded-[14px] border border-white/8 bg-[#08070E]/58 p-[10px] shadow-[0_12px_32px_rgba(0,0,0,0.22)]">
|
||||
<table className="w-full min-w-[860px] table-fixed border-collapse text-left text-[14px] text-white">
|
||||
<thead>
|
||||
<tr className="bg-linear-to-r from-[#F96C02] to-[#FE9F00] text-white">
|
||||
{columns.map((column) => (
|
||||
<th
|
||||
key={String(column.key)}
|
||||
className="px-[16px] py-[14px] text-[12px] font-bold uppercase tracking-[0.08em] first:rounded-tl-[10px] last:rounded-tr-[10px]"
|
||||
>
|
||||
{column.label}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{dataSource.map((record, index) => (
|
||||
<tr
|
||||
key={`${record.name}-${index}`}
|
||||
className="border-b border-white/6 bg-white/4 transition-colors last:border-b-0 hover:bg-white/8"
|
||||
>
|
||||
{columns.map((column, columnIndex) => {
|
||||
const value = record[column.key]
|
||||
const isLastRow = index === dataSource.length - 1
|
||||
const isFirstColumn = columnIndex === 0
|
||||
const isLastColumn = columnIndex === columns.length - 1
|
||||
|
||||
return (
|
||||
<td
|
||||
key={String(column.key)}
|
||||
className={`px-[16px] py-[16px] align-top text-[13px] leading-[1.55] text-white/90 ${
|
||||
isLastRow && isFirstColumn ? 'rounded-bl-[10px]' : ''
|
||||
} ${
|
||||
isLastRow && isLastColumn ? 'rounded-br-[10px]' : ''
|
||||
}`}
|
||||
>
|
||||
{column.render ? column.render(value, record, index) : value}
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BorderlessTable
|
||||
@@ -5,6 +5,8 @@ type AddressValidationResult =
|
||||
| { valid: false; message: string }
|
||||
|
||||
export function validateAddressFormSubmission(addressForm: AddAddressForm): AddressValidationResult {
|
||||
const normalizedRegion = addressForm.region.filter((value) => value.trim())
|
||||
|
||||
if (!addressForm.name.trim()) {
|
||||
return {
|
||||
valid: false,
|
||||
@@ -19,10 +21,10 @@ export function validateAddressFormSubmission(addressForm: AddAddressForm): Addr
|
||||
}
|
||||
}
|
||||
|
||||
if (addressForm.region.length !== 3) {
|
||||
if (normalizedRegion.length === 0) {
|
||||
return {
|
||||
valid: false,
|
||||
message: 'Please select province, city and district.',
|
||||
message: 'Please select a region.',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import {addressAdd, addressDelete, addressEdit, addressList} from '@/api/address
|
||||
import {validateAddressFormSubmission} from '@/features/addressBook/addressValidation'
|
||||
import {notifyError} from '@/features/notifications'
|
||||
import {queryKeys} from '@/lib/queryKeys.ts'
|
||||
import {emptyAddressFormMock} from '@/mock'
|
||||
import {useUserStore} from '@/store/user.ts'
|
||||
import type {AddressListItem} from '@/types/address.type.ts'
|
||||
import type {AddAddressForm, AddressOption} from '@/types'
|
||||
@@ -14,6 +13,14 @@ type UseAddressBookOptions = {
|
||||
autoLoad?: boolean
|
||||
}
|
||||
|
||||
const emptyAddressForm: AddAddressForm = {
|
||||
name: '',
|
||||
phone: '',
|
||||
region: [],
|
||||
detailedAddress: '',
|
||||
isDefault: false,
|
||||
}
|
||||
|
||||
export function getAddressText(item: AddressListItem) {
|
||||
const regionText = item.region_text || item.region.map((part) => part.trim()).join(', ')
|
||||
return [regionText, item.detail_address].filter(Boolean).join(', ')
|
||||
@@ -42,7 +49,7 @@ export function mapAddressToForm(item: AddressListItem): AddAddressForm {
|
||||
export function useAddressBook(options?: UseAddressBookOptions) {
|
||||
const queryClient = useQueryClient()
|
||||
const sessionId = useUserStore((state) => state.authInfo?.session_id ?? '')
|
||||
const [addressForm, setAddressForm] = useState<AddAddressForm>(emptyAddressFormMock)
|
||||
const [addressForm, setAddressForm] = useState<AddAddressForm>(emptyAddressForm)
|
||||
|
||||
const addressListQuery = useQuery({
|
||||
queryKey: queryKeys.addressList(sessionId),
|
||||
@@ -111,12 +118,12 @@ export function useAddressBook(options?: UseAddressBookOptions) {
|
||||
}, [queryClient, sessionId])
|
||||
|
||||
const resetAddressForm = () => {
|
||||
setAddressForm(emptyAddressFormMock)
|
||||
setAddressForm(emptyAddressForm)
|
||||
saveAddressMutation.reset()
|
||||
}
|
||||
|
||||
const fillAddressForm = (address?: AddressListItem | null) => {
|
||||
setAddressForm(address ? mapAddressToForm(address) : emptyAddressFormMock)
|
||||
setAddressForm(address ? mapAddressToForm(address) : emptyAddressForm)
|
||||
saveAddressMutation.reset()
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ export function GoodsCategoryList({
|
||||
|
||||
if (!categories.length) {
|
||||
return (
|
||||
<div className="pb-[24px]">
|
||||
<div className="pb-[24px] mt-[20px]">
|
||||
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
|
||||
{emptyText}
|
||||
</div>
|
||||
|
||||
@@ -92,7 +92,7 @@ export function GoodsRedeemModal({
|
||||
<div
|
||||
className="rounded-[12px] bg-[#171313]/88 px-[14px] py-[10px] shadow-[0_10px_30px_rgba(0,0,0,0.25)]">
|
||||
<div
|
||||
className="flex flex-col gap-[10px] border-b border-white/10 px-[6px] pb-[16px] sm:flex-row sm:items-center sm:justify-between">
|
||||
className="flex items-center justify-between gap-[10px] border-b border-white/10 px-[6px] pb-[16px]">
|
||||
<div className="flex items-center gap-[8px] text-[18px] font-medium text-white">
|
||||
<MapPinHouse className="h-[18px] w-[18px] text-[#FE9F00]" aria-hidden="true"/>
|
||||
<span>Address Info</span>
|
||||
@@ -140,7 +140,7 @@ export function GoodsRedeemModal({
|
||||
type="number"
|
||||
onChange={(event) => onChangeAddressForm('phone', event.target.value)}
|
||||
placeholder="Enter a reachable mobile number"
|
||||
className="bg-transparent text-[14px] text-white outline-none placeholder:text-white/35"
|
||||
className="input-no-spin bg-transparent text-[14px] text-white outline-none placeholder:text-white/35"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
@@ -161,7 +161,7 @@ export function GoodsRedeemModal({
|
||||
<input
|
||||
value={addressForm.detailedAddress}
|
||||
onChange={(event) => onChangeAddressForm('detailedAddress', event.target.value)}
|
||||
placeholder="Street, building, unit and room number"
|
||||
placeholder="Enter detail address"
|
||||
className="bg-transparent text-[14px] text-white outline-none placeholder:text-white/35"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {useEffect, useState} from 'react'
|
||||
import {ChevronDown} from 'lucide-react'
|
||||
|
||||
type ProvinceNode = {
|
||||
c: string
|
||||
@@ -32,6 +33,8 @@ type RegionDataset = {
|
||||
area: AreaNode[]
|
||||
}
|
||||
|
||||
const DIRECT_MUNICIPALITY_NAMES = new Set(['北京市', '上海市', '天津市', '重庆市'])
|
||||
|
||||
function RegionSelect({
|
||||
value,
|
||||
placeholder,
|
||||
@@ -60,8 +63,8 @@ function RegionSelect({
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="pointer-events-none absolute right-[12px] top-1/2 -translate-y-1/2 text-[12px] text-white/38">
|
||||
v
|
||||
<div className="pointer-events-none absolute right-[12px] top-1/2 -translate-y-1/2 text-white/38">
|
||||
<ChevronDown className="h-[14px] w-[14px]" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -70,7 +73,8 @@ function RegionSelect({
|
||||
export function RegionPicker({value, onChange}: RegionPickerProps) {
|
||||
const [regionData, setRegionData] = useState<RegionDataset | null>(null)
|
||||
const [loadFailed, setLoadFailed] = useState(false)
|
||||
const [province = '', city = '', district = ''] = value
|
||||
const normalizedValue = value.filter(Boolean)
|
||||
const province = normalizedValue[0] ?? ''
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
@@ -120,18 +124,24 @@ export function RegionPicker({value, onChange}: RegionPickerProps) {
|
||||
const areaList = regionData?.area ?? []
|
||||
|
||||
const selectedProvince = provinceList.find((item) => item.n === province)
|
||||
const selectedCity = cityList.find((item) => item.n === city && item.p === selectedProvince?.p)
|
||||
|
||||
const cities = selectedProvince
|
||||
? cityList.filter((item) => item.p === selectedProvince.p)
|
||||
: []
|
||||
const isDirectMunicipality = province ? DIRECT_MUNICIPALITY_NAMES.has(province) || (selectedProvince != null && cities.length === 0) : false
|
||||
const city = isDirectMunicipality ? '' : normalizedValue[1] ?? ''
|
||||
const district = isDirectMunicipality ? normalizedValue[1] ?? '' : normalizedValue[2] ?? ''
|
||||
const selectedCity = cityList.find((item) => item.n === city && item.p === selectedProvince?.p)
|
||||
|
||||
const districts = selectedCity
|
||||
const districts = isDirectMunicipality
|
||||
? selectedProvince
|
||||
? areaList.filter((item) => item.p === selectedProvince.p)
|
||||
: []
|
||||
: selectedCity
|
||||
? areaList.filter((item) => item.p === selectedCity.p && item.y === selectedCity.y)
|
||||
: []
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-[10px] sm:grid-cols-3">
|
||||
<div className={`grid grid-cols-1 gap-[10px] ${isDirectMunicipality ? 'sm:grid-cols-2' : 'sm:grid-cols-3'}`}>
|
||||
<RegionSelect
|
||||
value={province}
|
||||
placeholder={loadFailed ? 'failed to load' : 'province'}
|
||||
@@ -139,6 +149,15 @@ export function RegionPicker({value, onChange}: RegionPickerProps) {
|
||||
onChange={(nextProvince) => onChange(nextProvince ? [nextProvince] : [])}
|
||||
disabled={loadFailed || !regionData}
|
||||
/>
|
||||
{isDirectMunicipality ? (
|
||||
<RegionSelect
|
||||
value={district}
|
||||
placeholder="district"
|
||||
options={districts}
|
||||
disabled={loadFailed || !regionData || !province}
|
||||
onChange={(nextDistrict) => onChange(nextDistrict ? [province, nextDistrict] : [province])}
|
||||
/>
|
||||
) : (
|
||||
<RegionSelect
|
||||
value={city}
|
||||
placeholder="city"
|
||||
@@ -146,6 +165,8 @@ export function RegionPicker({value, onChange}: RegionPickerProps) {
|
||||
disabled={loadFailed || !regionData || !province}
|
||||
onChange={(nextCity) => onChange(nextCity ? [province, nextCity] : [province])}
|
||||
/>
|
||||
)}
|
||||
{isDirectMunicipality ? null : (
|
||||
<RegionSelect
|
||||
value={district}
|
||||
placeholder="district"
|
||||
@@ -153,6 +174,7 @@ export function RegionPicker({value, onChange}: RegionPickerProps) {
|
||||
disabled={loadFailed || !regionData || !city}
|
||||
onChange={(nextDistrict) => onChange(nextDistrict ? [province, city, nextDistrict] : [province, city])}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -33,7 +33,5 @@ export function useAssetsQuery() {
|
||||
|
||||
return {
|
||||
assetsInfo: assetsQuery.data ?? null,
|
||||
assetsLoading: assetsQuery.isPending,
|
||||
assetsError: assetsQuery.error instanceof Error ? assetsQuery.error.message : null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {useQuery, useQueryClient} from '@tanstack/react-query'
|
||||
import {useQuery} from '@tanstack/react-query'
|
||||
|
||||
import {goodList} from '@/api/business.ts'
|
||||
import {
|
||||
@@ -52,7 +52,6 @@ type UseGoodsCatalogOptions = {
|
||||
}
|
||||
|
||||
export function useGoodsCatalog(options?: UseGoodsCatalogOptions) {
|
||||
const queryClient = useQueryClient()
|
||||
const types = options?.types?.length ? [...options.types] : HOME_GOOD_TYPE_ORDER
|
||||
const goodsCatalogQuery = useQuery({
|
||||
queryKey: queryKeys.goodsCatalog(types),
|
||||
@@ -83,7 +82,5 @@ export function useGoodsCatalog(options?: UseGoodsCatalogOptions) {
|
||||
return {
|
||||
productCategories: goodsCatalogQuery.data ?? [],
|
||||
loading: goodsCatalogQuery.isPending,
|
||||
error: goodsCatalogQuery.error instanceof Error ? goodsCatalogQuery.error.message : null,
|
||||
invalidateGoods: () => queryClient.invalidateQueries({queryKey: ['goods-catalog']}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,16 +5,24 @@ import {useToastStore} from './store'
|
||||
export function GlobalToast() {
|
||||
const toast = useToastStore((state) => state.toast)
|
||||
|
||||
if (!toast?.visible) {
|
||||
if (!toast) {
|
||||
return null
|
||||
}
|
||||
|
||||
const isError = toast.type === 'error'
|
||||
|
||||
return (
|
||||
<div className="pointer-events-none fixed right-[16px] top-[16px] z-[60] max-w-[320px]">
|
||||
<div
|
||||
className={`flex items-start gap-[10px] rounded-[14px] border px-[14px] py-[12px] text-white shadow-[0_18px_40px_rgba(0,0,0,0.35)] backdrop-blur-[8px] ${
|
||||
className={`pointer-events-none fixed right-[16px] top-[16px] z-[60] max-w-[320px] transition-all duration-220 ease-out ${
|
||||
toast.visible
|
||||
? 'translate-y-0 opacity-100'
|
||||
: '-translate-y-2 opacity-0'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`flex items-start gap-[10px] rounded-[14px] border px-[14px] py-[12px] text-white shadow-[0_18px_40px_rgba(0,0,0,0.35)] backdrop-blur-[8px] transition-all duration-220 ease-out ${
|
||||
toast.visible ? 'scale-100' : 'scale-[0.98]'
|
||||
} ${
|
||||
isError
|
||||
? 'border-[#FF9BA4]/35 bg-[#3A1318]/94'
|
||||
: 'border-[#9BFFC0]/35 bg-[#11311E]/94'
|
||||
|
||||
@@ -15,6 +15,7 @@ type ToastStore = {
|
||||
}
|
||||
|
||||
let toastTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let toastRemoveTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
export function resolveToastMessage(source: unknown, fallback = '') {
|
||||
if (typeof source === 'string' && source.trim()) {
|
||||
@@ -37,18 +38,44 @@ export const useToastStore = create<ToastStore>((set) => ({
|
||||
if (toastTimer) {
|
||||
clearTimeout(toastTimer)
|
||||
}
|
||||
if (toastRemoveTimer) {
|
||||
clearTimeout(toastRemoveTimer)
|
||||
}
|
||||
|
||||
set({
|
||||
toast: {
|
||||
message,
|
||||
type,
|
||||
visible: true,
|
||||
visible: false,
|
||||
},
|
||||
})
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
set((state) => state.toast
|
||||
? {
|
||||
toast: {
|
||||
...state.toast,
|
||||
visible: true,
|
||||
},
|
||||
}
|
||||
: state)
|
||||
})
|
||||
|
||||
toastTimer = setTimeout(() => {
|
||||
set({toast: null})
|
||||
set((state) => state.toast
|
||||
? {
|
||||
toast: {
|
||||
...state.toast,
|
||||
visible: false,
|
||||
},
|
||||
}
|
||||
: state)
|
||||
toastTimer = null
|
||||
|
||||
toastRemoveTimer = setTimeout(() => {
|
||||
set({toast: null})
|
||||
toastRemoveTimer = null
|
||||
}, 220)
|
||||
}, 2000)
|
||||
},
|
||||
clearToast: () => {
|
||||
@@ -56,6 +83,10 @@ export const useToastStore = create<ToastStore>((set) => ({
|
||||
clearTimeout(toastTimer)
|
||||
toastTimer = null
|
||||
}
|
||||
if (toastRemoveTimer) {
|
||||
clearTimeout(toastRemoveTimer)
|
||||
toastRemoveTimer = null
|
||||
}
|
||||
set({toast: null})
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -6,6 +6,17 @@ body,
|
||||
font-family: "Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
.input-no-spin::-webkit-outer-spin-button,
|
||||
.input-no-spin::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.input-no-spin[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
appearance: textfield;
|
||||
}
|
||||
|
||||
|
||||
.button-play {
|
||||
align-items: center;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import ky, {HTTPError, type Input, type KyInstance, type Options} from 'ky'
|
||||
import ky, {HTTPError, type AfterResponseHook, type Input, type KyInstance, type Options} from 'ky'
|
||||
|
||||
import {notifyError, resolveToastMessage} from '@/features/notifications'
|
||||
import {useUserStore} from '@/store/user.ts'
|
||||
import type {ValidateTokenData} from '@/types/auth.type.ts'
|
||||
import {objectToFormData} from './tool'
|
||||
|
||||
type RequestMethod = 'get' | 'post' | 'put' | 'patch' | 'delete'
|
||||
type ResponseType = 'json' | 'text' | 'blob' | 'arrayBuffer' | 'formData' | 'response'
|
||||
@@ -44,13 +46,97 @@ const isApiEnvelope = (value: unknown): value is ApiEnvelope =>
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? '/api/'
|
||||
const REQUEST_TIMEOUT = 10_000
|
||||
const AUTH_RETRY_HEADER = 'x-auth-retried'
|
||||
const VERIFY_TOKEN_PATH = '/v1/mall/verifyToken'
|
||||
|
||||
let accessTokenFormatter: TokenFormatter = (token) => `Bearer ${token}`
|
||||
let refreshAuthInfoPromise: Promise<ValidateTokenData> | null = null
|
||||
|
||||
export const setAccessTokenFormatter = (formatter?: TokenFormatter) => {
|
||||
accessTokenFormatter = formatter ?? ((token) => `Bearer ${token}`)
|
||||
}
|
||||
|
||||
const authRefreshClient = ky.create({
|
||||
baseUrl: API_BASE_URL,
|
||||
timeout: REQUEST_TIMEOUT,
|
||||
retry: 0,
|
||||
headers: {
|
||||
lang: 'zh',
|
||||
},
|
||||
})
|
||||
|
||||
const handleUnauthorizedRetry: AfterResponseHook = async ({request, response}) => {
|
||||
if (!shouldRefreshOnUnauthorized(request, response)) {
|
||||
return response
|
||||
}
|
||||
|
||||
try {
|
||||
const authInfo = await refreshAuthInfo()
|
||||
const nextRequest = withRefreshedSessionId(request, authInfo.session_id)
|
||||
return await requestClient(nextRequest)
|
||||
} catch {
|
||||
useUserStore.getState().clearUserInfo()
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAuthInfo() {
|
||||
if (refreshAuthInfoPromise) {
|
||||
return refreshAuthInfoPromise
|
||||
}
|
||||
|
||||
refreshAuthInfoPromise = (async () => {
|
||||
const token = useUserStore.getState().userInfo?.token?.trim()
|
||||
if (!token) {
|
||||
throw new RequestError('Unauthorized')
|
||||
}
|
||||
|
||||
const response = await authRefreshClient.post('v1/mall/verifyToken', {
|
||||
body: objectToFormData({token}),
|
||||
}).json<{
|
||||
code: number
|
||||
msg?: string
|
||||
data?: ValidateTokenData
|
||||
}>()
|
||||
|
||||
if (!response || (response.code !== 1 && response.code !== 200) || !response.data?.session_id) {
|
||||
throw new RequestError(resolveToastMessage(response, 'Unauthorized'))
|
||||
}
|
||||
|
||||
useUserStore.getState().setAuthInfo(response.data)
|
||||
return response.data
|
||||
})()
|
||||
|
||||
try {
|
||||
return await refreshAuthInfoPromise
|
||||
} finally {
|
||||
refreshAuthInfoPromise = null
|
||||
}
|
||||
}
|
||||
|
||||
function shouldRefreshOnUnauthorized(request: Request, response: Response) {
|
||||
if (response.status !== 401) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (request.headers.get(AUTH_RETRY_HEADER) === '1') {
|
||||
return false
|
||||
}
|
||||
|
||||
return !request.url.includes(VERIFY_TOKEN_PATH)
|
||||
}
|
||||
|
||||
function withRefreshedSessionId(request: Request, sessionId: string) {
|
||||
const nextUrl = new URL(request.url)
|
||||
if (nextUrl.searchParams.has('session_id')) {
|
||||
nextUrl.searchParams.set('session_id', sessionId)
|
||||
}
|
||||
|
||||
const nextRequest = new Request(nextUrl, request.clone())
|
||||
nextRequest.headers.set(AUTH_RETRY_HEADER, '1')
|
||||
return nextRequest
|
||||
}
|
||||
|
||||
const requestClient = ky.create({
|
||||
baseUrl: API_BASE_URL,
|
||||
timeout: REQUEST_TIMEOUT,
|
||||
@@ -69,6 +155,9 @@ const requestClient = ky.create({
|
||||
request.headers.set('Authorization', accessTokenFormatter(token))
|
||||
},
|
||||
],
|
||||
afterResponse: [
|
||||
handleUnauthorizedRetry,
|
||||
],
|
||||
beforeError: [
|
||||
async ({error}) => {
|
||||
if (error instanceof HTTPError) {
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
import type {
|
||||
AccountTableRow,
|
||||
AddressOption,
|
||||
AddAddressForm,
|
||||
OrderRecord,
|
||||
PointsRecord,
|
||||
PointsRecordTone,
|
||||
} from '@/types'
|
||||
|
||||
export const accountAddressRowsMock: AccountTableRow[] = [
|
||||
{
|
||||
name: 'Jia Jun',
|
||||
phone: '+86 138 0000 1288',
|
||||
address: 'No. 88 Century Avenue, Pudong New Area, Shanghai',
|
||||
code: '200120',
|
||||
action: 'Edit',
|
||||
setting: 'Default',
|
||||
},
|
||||
{
|
||||
name: 'Alicia Tan',
|
||||
phone: '+65 9123 4567',
|
||||
address: '18 Robinson Road, Singapore',
|
||||
code: '048547',
|
||||
action: 'Edit',
|
||||
setting: 'Optional',
|
||||
},
|
||||
{
|
||||
name: 'Marcus Lee',
|
||||
phone: '+60 12 778 9911',
|
||||
address: '27 Jalan Bukit Bintang, Kuala Lumpur',
|
||||
code: '55100',
|
||||
action: 'Edit',
|
||||
setting: 'Optional',
|
||||
},
|
||||
]
|
||||
|
||||
export const initialAddressOptionsMock: AddressOption[] = [
|
||||
{
|
||||
id: 'address-shanghai',
|
||||
name: 'Jia Jun',
|
||||
phone: '+86 138 0000 1288',
|
||||
address: 'No. 88 Century Avenue, Pudong New Area, Shanghai',
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
id: 'address-singapore',
|
||||
name: 'Alicia Tan',
|
||||
phone: '+65 9123 4567',
|
||||
address: '18 Robinson Road, Singapore',
|
||||
},
|
||||
{
|
||||
id: 'address-kuala-lumpur',
|
||||
name: 'Marcus Lee',
|
||||
phone: '+60 12 778 9911',
|
||||
address: '27 Jalan Bukit Bintang, Kuala Lumpur',
|
||||
},
|
||||
]
|
||||
|
||||
export const emptyAddressFormMock: AddAddressForm = {
|
||||
name: '',
|
||||
phone: '',
|
||||
region: [],
|
||||
detailedAddress: '',
|
||||
isDefault: false,
|
||||
}
|
||||
|
||||
export const orderRecordsMock: 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',
|
||||
},
|
||||
]
|
||||
|
||||
export const pointsRecordsMock: 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',
|
||||
},
|
||||
]
|
||||
|
||||
export const pointsRecordToneClassNameMock: Record<PointsRecordTone, string> = {
|
||||
positive: 'bg-[#9BFFC0] text-[#176640]',
|
||||
negative: 'bg-[#FF9BA4] text-[#7B2634]',
|
||||
}
|
||||
@@ -40,11 +40,6 @@ export type TableColumn<T extends Record<string, string>> = {
|
||||
render?: (value: T[keyof T], record: T, index: number) => ReactNode
|
||||
}
|
||||
|
||||
export type BorderlessTableProps<T extends Record<string, string>> = {
|
||||
columns: TableColumn<T>[]
|
||||
dataSource: T[]
|
||||
}
|
||||
|
||||
export type QuickNavCardProps = {
|
||||
icon: LucideIcon
|
||||
label: string
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {useState} from 'react'
|
||||
|
||||
import PageLayout from '@/components/layout'
|
||||
import BorderlessTable from '@/components/table'
|
||||
import Modal from '@/components/modal'
|
||||
import Button from '@/components/button'
|
||||
import {Link} from 'react-router-dom'
|
||||
@@ -10,16 +9,6 @@ import {useAddressBook} from '@/features/addressBook'
|
||||
import {GoodsRedeemModal} from '@/features/goods'
|
||||
import {notifySuccess} from '@/features/notifications'
|
||||
import type {AddressListItem} from '@/types/address.type.ts'
|
||||
import type {TableColumn} from '@/types'
|
||||
|
||||
type AddressTableRow = {
|
||||
id: string
|
||||
name: string
|
||||
phone: string
|
||||
address: string
|
||||
action: string
|
||||
setting: string
|
||||
}
|
||||
|
||||
function AccountPage() {
|
||||
const addressBook = useAddressBook({autoLoad: true})
|
||||
@@ -27,15 +16,6 @@ function AccountPage() {
|
||||
const [editingAddress, setEditingAddress] = useState<AddressListItem | null>(null)
|
||||
const [deleteTarget, setDeleteTarget] = useState<AddressListItem | null>(null)
|
||||
|
||||
const rows: AddressTableRow[] = addressBook.addresses.map((item) => ({
|
||||
id: String(item.id),
|
||||
name: item.receiver_name,
|
||||
phone: item.phone,
|
||||
address: addressBook.addressOptions.find((option) => option.id === String(item.id))?.address ?? '',
|
||||
action: 'Edit',
|
||||
setting: item.default_setting === 1 ? 'Default' : 'Optional',
|
||||
}))
|
||||
|
||||
const isAddressFormValid = addressBook.isAddressFormValid
|
||||
|
||||
const handleOpenAddAddress = () => {
|
||||
@@ -76,63 +56,6 @@ function AccountPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const columns: TableColumn<AddressTableRow>[] = [
|
||||
{
|
||||
label: 'Name',
|
||||
key: 'name',
|
||||
render: (value: string) => <div className="font-medium text-white">{value}</div>,
|
||||
},
|
||||
{
|
||||
label: 'Phone / Mobile',
|
||||
key: 'phone',
|
||||
render: (value: string) => <div className="text-white/72">{value}</div>,
|
||||
},
|
||||
{
|
||||
label: 'Address',
|
||||
key: 'address',
|
||||
render: (value: string) => <div className="max-w-[280px] text-white/72">{value}</div>,
|
||||
},
|
||||
{
|
||||
label: 'Action',
|
||||
key: 'action',
|
||||
render: (_value: string, _record: AddressTableRow, index: number) => (
|
||||
<div className="flex items-center gap-[8px]">
|
||||
<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(addressBook.addresses[index])}
|
||||
>
|
||||
<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(addressBook.addresses[index])}
|
||||
>
|
||||
<Trash2 className="h-[12px] w-[12px]" aria-hidden="true" />
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Default Setting',
|
||||
key: 'setting',
|
||||
render: (value: string) => (
|
||||
<div
|
||||
className={`inline-flex rounded-full px-[10px] py-[5px] text-[12px] ${
|
||||
value === 'Default'
|
||||
? 'bg-[#FA6A00]/14 text-[#FFB36D]'
|
||||
: 'bg-white/6 text-white/62'
|
||||
}`}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
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
|
||||
@@ -173,61 +96,62 @@ function AccountPage() {
|
||||
</div>
|
||||
) : !addressBook.addresses.length ? (
|
||||
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
|
||||
No shipping address found. Add one to start redeeming physical rewards.
|
||||
No shipping address found. Add one.
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="space-y-[12px] lg:hidden">
|
||||
{rows.map((item, index) => (
|
||||
<div key={item.id} className="liquid-glass-bg p-[14px]">
|
||||
<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">{item.name}</div>
|
||||
<div className="mt-[4px] text-[13px] text-white/62">{item.phone}</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] ${
|
||||
item.setting === 'Default'
|
||||
isDefault
|
||||
? 'bg-[#FA6A00]/14 text-[#FFB36D]'
|
||||
: 'bg-white/6 text-white/62'
|
||||
}`}
|
||||
>
|
||||
{item.setting === 'Default' ? (
|
||||
{isDefault ? (
|
||||
<span className="inline-flex items-center gap-[5px]">
|
||||
<BadgeCheck className="h-[12px] w-[12px]" aria-hidden="true" />
|
||||
{item.setting}
|
||||
Default
|
||||
</span>
|
||||
) : item.setting}
|
||||
) : '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">Address</div>
|
||||
<div className="mt-[6px] text-[13px] leading-[1.6] text-white/78">{item.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="mt-[12px] 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(addressBook.addresses[index])}
|
||||
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="mt-[10px] 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(addressBook.addresses[index])}
|
||||
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 className="hidden lg:block">
|
||||
<BorderlessTable columns={columns} dataSource={rows} />
|
||||
)})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -132,11 +132,11 @@ function HomePage() {
|
||||
|
||||
<div className="mt-[4px]">
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-stretch">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:w-[544px] lg:shrink-0">
|
||||
<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="flex items-start justify-between gap-[12px]">
|
||||
<div>
|
||||
<div className="text-[13px] uppercase tracking-[0.16em] text-white/58">Claimable
|
||||
<div className="text-[13px] tracking-[0.16em] text-white/58">Claimable
|
||||
Points
|
||||
</div>
|
||||
<div
|
||||
@@ -154,7 +154,7 @@ function HomePage() {
|
||||
<div className="liquid-glass-bg flex min-h-[167px] flex-col justify-around p-[14px]">
|
||||
<div className="flex items-start justify-between gap-[12px]">
|
||||
<div>
|
||||
<div className="text-[13px] uppercase tracking-[0.16em] text-white/58">Daily Claim
|
||||
<div className="text-[13px] tracking-[0.16em] text-white/58">Daily Claim
|
||||
Limit
|
||||
</div>
|
||||
<div
|
||||
@@ -178,20 +178,25 @@ function HomePage() {
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
className="mt-[10px] text-[13px] text-white/68">Claimed: <span className={'text-[#FE9C00]'}>{assetsInfo?.today_claimed || 0}</span> / {assetsInfo?.today_limit || 0}</div>
|
||||
className="mt-[10px] text-[13px] text-white/68">Claimed: <span
|
||||
className={'text-[#FE9C00]'}>{assetsInfo?.today_claimed || 0}</span> / {assetsInfo?.today_limit || 0}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-3">
|
||||
<div
|
||||
className="liquid-glass-bg flex min-h-[109px] flex-col justify-between p-[14px] sm:p-[16px]">
|
||||
<div className="flex items-start justify-between gap-[12px]">
|
||||
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] uppercase tracking-[0.16em] text-white/58">Available for
|
||||
Withdrawal
|
||||
<div className="text-[13px] tracking-[0.16em] text-white/58">Available for
|
||||
Withdrawal (Cash)
|
||||
</div>
|
||||
<div
|
||||
className="mt-[10px] text-[32px] font-semibold leading-none text-white">{assetsInfo?.withdrawable_cash || 0} CNY</div>
|
||||
className="mt-[10px] text-[32px] font-semibold leading-none text-white">{assetsInfo?.withdrawable_cash || 0}
|
||||
<span
|
||||
className="text-[13px] tracking-[0.16em] text-white/58 ml-[10px]">CNY</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="flex h-[38px] w-[38px] items-center justify-center rounded-[12px] bg-[#FA6A00]/16 text-[#FE9F00]">
|
||||
@@ -266,7 +271,8 @@ function HomePage() {
|
||||
>
|
||||
<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?
|
||||
After converting the points to be collected into usable points, they can be redeemed or withdrawn.
|
||||
Are you sure to claim it?
|
||||
</div>
|
||||
</Modal>
|
||||
</PageLayout>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {useEffect, useState} from 'react'
|
||||
import {useState} from 'react'
|
||||
import {useQuery} from '@tanstack/react-query'
|
||||
|
||||
import PageLayout from '@/components/layout'
|
||||
@@ -274,11 +274,11 @@ function TabButton({ active, label, icon: Icon, onClick }: TabButtonProps) {
|
||||
function OrderCard({ record, onOpenDetails }: OrderCardProps) {
|
||||
return (
|
||||
<div className="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-[13px] text-white sm:text-[14px]">
|
||||
<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>
|
||||
<div className="liquid-glass-bg !rounded-t-none flex flex-col gap-[14px] px-[12px] py-[14px] sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="liquid-glass-bg !rounded-t-none flex items-center justify-between gap-[14px] px-[12px] py-[14px]">
|
||||
<div className="min-w-0 flex-1 pr-[16px]">
|
||||
<div className="text-[16px] font-medium text-white">{record.title}</div>
|
||||
{record.trackingNumber ? (
|
||||
<div className="mt-[4px] text-[13px] text-white/45">
|
||||
@@ -295,7 +295,7 @@ function OrderCard({ record, onOpenDetails }: OrderCardProps) {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 items-center justify-between gap-[10px] sm:flex-col sm:items-end">
|
||||
<div className="flex shrink-0 flex-col items-end gap-[10px]">
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-[6px] px-[8px] py-[3px] text-[11px] leading-none',
|
||||
@@ -314,10 +314,10 @@ 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="bg-linear-to-r from-[#F96C02] to-[#FE9F00] px-[12px] py-[9px] text-[14px] text-white sm:text-[15px]">
|
||||
<div className="bg-linear-to-r from-[#F96C02] to-[#FE9F00] px-[12px] py-[9px] text-[15px] text-white">
|
||||
{record.title}
|
||||
</div>
|
||||
<div className="liquid-glass-bg !rounded-t-none flex flex-col gap-[10px] px-[12px] py-[18px] sm:flex-row sm:items-center sm:justify-between sm:py-[22px]">
|
||||
<div className="liquid-glass-bg !rounded-t-none flex items-center justify-between px-[12px] py-[22px]">
|
||||
<div className="text-[13px] text-white/40">
|
||||
{record.date} {record.time}
|
||||
</div>
|
||||
@@ -427,10 +427,6 @@ function RecordPage() {
|
||||
setSelectedOrder(null)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedOrder(null)
|
||||
}, [tab])
|
||||
|
||||
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
|
||||
@@ -452,13 +448,19 @@ function RecordPage() {
|
||||
active={tab === 'order'}
|
||||
icon={PackageSearch}
|
||||
label="My Orders"
|
||||
onClick={() => setTab('order')}
|
||||
onClick={() => {
|
||||
setSelectedOrder(null)
|
||||
setTab('order')
|
||||
}}
|
||||
/>
|
||||
<TabButton
|
||||
active={tab === 'record'}
|
||||
icon={Coins}
|
||||
label="Points Record"
|
||||
onClick={() => setTab('record')}
|
||||
onClick={() => {
|
||||
setSelectedOrder(null)
|
||||
setTab('record')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -487,7 +489,7 @@ function RecordPage() {
|
||||
}
|
||||
>
|
||||
{selectedOrder ? (
|
||||
<div className="rounded-[10px] bg-[#1C1818]/78 px-[12px] py-[6px]">
|
||||
<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}` },
|
||||
|
||||
Reference in New Issue
Block a user