feat(ui): 添加动画效果并优化界面样式
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
"i18next": "^26.0.4",
|
||||
"ky": "^2.0.0",
|
||||
"lucide-react": "^0.577.0",
|
||||
"motion": "^12.38.0",
|
||||
"province-city-china": "^8.5.8",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
|
||||
60
pnpm-lock.yaml
generated
60
pnpm-lock.yaml
generated
@@ -26,6 +26,9 @@ importers:
|
||||
lucide-react:
|
||||
specifier: ^0.577.0
|
||||
version: 0.577.0(react@19.2.4)
|
||||
motion:
|
||||
specifier: ^12.38.0
|
||||
version: 12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
province-city-china:
|
||||
specifier: ^8.5.8
|
||||
version: 8.5.8
|
||||
@@ -911,6 +914,20 @@ packages:
|
||||
flatted@3.4.1:
|
||||
resolution: {integrity: sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==}
|
||||
|
||||
framer-motion@12.38.0:
|
||||
resolution: {integrity: sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==}
|
||||
peerDependencies:
|
||||
'@emotion/is-prop-valid': '*'
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
react-dom: ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@emotion/is-prop-valid':
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
|
||||
fs-extra@11.3.4:
|
||||
resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==}
|
||||
engines: {node: '>=14.14'}
|
||||
@@ -1252,6 +1269,26 @@ packages:
|
||||
resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
motion-dom@12.38.0:
|
||||
resolution: {integrity: sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==}
|
||||
|
||||
motion-utils@12.36.0:
|
||||
resolution: {integrity: sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==}
|
||||
|
||||
motion@12.38.0:
|
||||
resolution: {integrity: sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w==}
|
||||
peerDependencies:
|
||||
'@emotion/is-prop-valid': '*'
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
react-dom: ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@emotion/is-prop-valid':
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
@@ -2502,6 +2539,15 @@ snapshots:
|
||||
|
||||
flatted@3.4.1: {}
|
||||
|
||||
framer-motion@12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||
dependencies:
|
||||
motion-dom: 12.38.0
|
||||
motion-utils: 12.36.0
|
||||
tslib: 2.8.1
|
||||
optionalDependencies:
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
|
||||
fs-extra@11.3.4:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
@@ -2756,6 +2802,20 @@ snapshots:
|
||||
dependencies:
|
||||
minipass: 7.1.3
|
||||
|
||||
motion-dom@12.38.0:
|
||||
dependencies:
|
||||
motion-utils: 12.36.0
|
||||
|
||||
motion-utils@12.36.0: {}
|
||||
|
||||
motion@12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||
dependencies:
|
||||
framer-motion: 12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
tslib: 2.8.1
|
||||
optionalDependencies:
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
BIN
public/cash-bg.png
Normal file
BIN
public/cash-bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 169 KiB |
@@ -1,6 +1,7 @@
|
||||
import {Children} from 'react'
|
||||
|
||||
import i18n from '@/lib/i18n'
|
||||
import {MotionButton, tapMotionPropsSoft} from '@/lib/motion'
|
||||
import { cn } from '@/lib'
|
||||
import type { ModalProps } from '@/types'
|
||||
|
||||
@@ -29,11 +30,12 @@ export function Modal({
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-[12px] sm:p-[16px]">
|
||||
<button
|
||||
<MotionButton
|
||||
type="button"
|
||||
aria-label={i18n.t('common.close')}
|
||||
className="absolute inset-0 bg-[#050409]/65 backdrop-blur-[6px]"
|
||||
onClick={handleOverlayClick}
|
||||
{...tapMotionPropsSoft}
|
||||
/>
|
||||
|
||||
<div
|
||||
@@ -47,13 +49,14 @@ export function Modal({
|
||||
<div className="flex min-h-[58px] shrink-0 items-center justify-between bg-linear-to-r from-[#F96C02] to-[#FE9F00] px-[16px] py-[14px] text-white sm:px-[18px]">
|
||||
<div className="pr-[12px] text-[16px] leading-[1.2] font-bold sm:text-[18px]">{title}</div>
|
||||
{onClose ? (
|
||||
<button
|
||||
<MotionButton
|
||||
type="button"
|
||||
className="flex h-[30px] w-[30px] items-center justify-center rounded-full bg-black/15 text-[18px] leading-none text-white transition-colors hover:bg-black/25 focus-visible:ring-2 focus-visible:ring-white/85 focus-visible:ring-offset-2 focus-visible:ring-offset-[#F96C02]"
|
||||
onClick={onClose}
|
||||
{...tapMotionPropsSoft}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</MotionButton>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {ChevronRight} from 'lucide-react'
|
||||
|
||||
import Button from '@/components/button'
|
||||
import {HOME_CATEGORY_META_MAP, HOME_GOOD_TYPE_ORDER} from '@/constant'
|
||||
import {MotionButton, tapMotionPropsSoft} from '@/lib/motion'
|
||||
import type {ProductCategory, ProductItem} from '@/types'
|
||||
|
||||
type GoodsCategoryListProps = {
|
||||
@@ -132,21 +133,22 @@ export function GoodsCategoryList({
|
||||
<div className="truncate text-[15px] font-semibold text-white">{t(HOME_CATEGORY_META_MAP[category.id].nameKey)}</div>
|
||||
</div>
|
||||
{showMore && onMoreClick ? (
|
||||
<button
|
||||
<MotionButton
|
||||
type="button"
|
||||
className="flex shrink-0 items-center gap-[3px] text-[12px] font-light text-[#FA6A00] underline cursor-pointer"
|
||||
onClick={() => onMoreClick(category.id)}
|
||||
{...tapMotionPropsSoft}
|
||||
>
|
||||
{t('common.more')}
|
||||
<ChevronRight className="h-[14px] w-[14px]" aria-hidden="true"/>
|
||||
</button>
|
||||
</MotionButton>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||
{category.items.map((product) => (
|
||||
<div
|
||||
key={product.id}
|
||||
className="liquid-glass-bg flex min-h-[260px] w-full flex-col items-stretch justify-start overflow-hidden"
|
||||
className="liquid-glass-bg pc-hover-float flex min-h-[260px] w-full flex-col items-stretch justify-start overflow-hidden"
|
||||
>
|
||||
<GoodsImage imageUrl={product.imageUrl}/>
|
||||
<div
|
||||
|
||||
@@ -9,6 +9,7 @@ import {useTranslation} from 'react-i18next'
|
||||
|
||||
import Button from '@/components/button'
|
||||
import Modal from '@/components/modal'
|
||||
import {MotionButton, tapMotionPropsSoft} from '@/lib/motion'
|
||||
import type {
|
||||
AddAddressForm,
|
||||
AddressOption,
|
||||
@@ -130,10 +131,11 @@ export function GoodsRedeemModal({
|
||||
<MapPinHouse className="h-[18px] w-[18px] text-[#FE9F00]" aria-hidden="true"/>
|
||||
<span>{t('goods.addressInfo')}</span>
|
||||
</div>
|
||||
<button
|
||||
<MotionButton
|
||||
type="button"
|
||||
className="flex items-center gap-[8px] text-[14px] text-white/85"
|
||||
onClick={() => onChangeAddressForm('isDefault', !addressForm.isDefault)}
|
||||
{...tapMotionPropsSoft}
|
||||
>
|
||||
<span
|
||||
className={`flex h-[16px] w-[16px] items-center justify-center rounded-full border ${
|
||||
@@ -147,7 +149,7 @@ export function GoodsRedeemModal({
|
||||
></span>
|
||||
</span>
|
||||
<span>{t('goods.defaultAddress')}</span>
|
||||
</button>
|
||||
</MotionButton>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-white/10">
|
||||
@@ -304,13 +306,14 @@ export function GoodsRedeemModal({
|
||||
{addressOptions.map((address) => {
|
||||
const isSelected = selectedAddressId === address.id
|
||||
return (
|
||||
<button
|
||||
<MotionButton
|
||||
key={address.id}
|
||||
type="button"
|
||||
className={`flex w-full items-start gap-[12px] border-b border-white/6 px-[14px] py-[14px] text-left transition-colors last:border-b-0 hover:bg-white/4 ${
|
||||
isSelected ? 'bg-white/[0.03]' : ''
|
||||
}`}
|
||||
onClick={() => onSelectAddress(address.id)}
|
||||
{...tapMotionPropsSoft}
|
||||
>
|
||||
<div
|
||||
className="mt-[2px] flex h-[20px] w-[20px] shrink-0 items-center justify-center rounded-full">
|
||||
@@ -338,15 +341,16 @@ export function GoodsRedeemModal({
|
||||
{address.address}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</MotionButton>
|
||||
)
|
||||
})}
|
||||
|
||||
<button
|
||||
<MotionButton
|
||||
type="button"
|
||||
className="flex w-full items-center justify-between px-[14px] py-[16px] text-left transition-colors hover:bg-white/4 focus-visible:ring-2 focus-visible:ring-[#FE9F00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070E]"
|
||||
onClick={onOpenAddAddress}
|
||||
disabled={addressLoading}
|
||||
{...tapMotionPropsSoft}
|
||||
>
|
||||
<div className="flex items-center gap-[10px]">
|
||||
<span
|
||||
@@ -356,7 +360,7 @@ export function GoodsRedeemModal({
|
||||
<div className="text-[14px] text-white/82">{t('goods.addAddress')}</div>
|
||||
</div>
|
||||
<ChevronRight className="h-[16px] w-[16px] text-white/45" aria-hidden="true"/>
|
||||
</button>
|
||||
</MotionButton>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -158,3 +158,80 @@ body::-webkit-scrollbar,
|
||||
box-sizing: border-box;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.home-stat-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(255, 214, 156, 0.12);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 232, 193, 0.08),
|
||||
0 18px 32px rgba(0, 0, 0, 0.22);
|
||||
}
|
||||
|
||||
.home-stat-card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(circle at 14% 18%, rgba(255, 194, 84, 0.14), transparent 36%),
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.home-stat-card-claim {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(102, 76, 54, 0.16), rgba(58, 39, 28, 0.18)),
|
||||
linear-gradient(135deg, rgba(88, 58, 40, 0.96) 0%, rgba(60, 42, 31, 0.94) 44%, rgba(49, 34, 27, 0.92) 100%);
|
||||
}
|
||||
|
||||
.home-stat-card-limit {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(124, 88, 57, 0.14), rgba(67, 47, 34, 0.12)),
|
||||
linear-gradient(135deg, rgba(99, 72, 50, 0.96) 0%, rgba(70, 48, 35, 0.95) 46%, rgba(55, 37, 28, 0.94) 100%);
|
||||
}
|
||||
|
||||
.home-withdraw-card {
|
||||
overflow: hidden;
|
||||
border-radius: 18px;
|
||||
border: 1px solid rgba(255, 220, 160, 0.16);
|
||||
background-image: url("/cash-bg.png");
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 234, 194, 0.1),
|
||||
0 22px 40px rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
|
||||
.home-withdraw-card::before {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.home-withdraw-card::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.home-withdraw-actions {
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(255, 218, 168, 0.12);
|
||||
background: linear-gradient(180deg, rgba(31, 23, 19, 0.94), rgba(22, 16, 14, 0.96));
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 230, 184, 0.06),
|
||||
0 16px 30px rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
|
||||
.pc-hover-float {
|
||||
transition:
|
||||
transform 0.26s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
box-shadow 0.26s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
border-color 0.26s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) and (hover: hover) and (pointer: fine) {
|
||||
.pc-hover-float:hover {
|
||||
transform: translateY(-6px);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 232, 193, 0.1),
|
||||
0 24px 40px rgba(0, 0, 0, 0.26);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,5 +5,6 @@ export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
export * from './motion'
|
||||
export * from './request'
|
||||
export * from './tool'
|
||||
|
||||
37
src/lib/motion.ts
Normal file
37
src/lib/motion.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import {motion, type Transition} from 'motion/react'
|
||||
import {Link} from 'react-router-dom'
|
||||
|
||||
const tapTransition: Transition = {
|
||||
type: 'spring',
|
||||
stiffness: 700,
|
||||
damping: 28,
|
||||
mass: 0.55,
|
||||
}
|
||||
|
||||
export const tapMotionProps = {
|
||||
whileTap: {
|
||||
scale: 0.94,
|
||||
opacity: 0.96,
|
||||
},
|
||||
transition: tapTransition,
|
||||
} as const
|
||||
|
||||
export const tapMotionPropsSoft = {
|
||||
whileTap: {
|
||||
scale: 0.965,
|
||||
opacity: 0.98,
|
||||
},
|
||||
transition: tapTransition,
|
||||
} as const
|
||||
|
||||
export const tapMotionPropsIcon = {
|
||||
whileTap: {
|
||||
scale: 0.84,
|
||||
opacity: 0.9,
|
||||
},
|
||||
transition: tapTransition,
|
||||
} as const
|
||||
|
||||
export const MotionButton = motion.button
|
||||
export const MotionDiv = motion.div
|
||||
export const MotionLink = motion.create(Link)
|
||||
@@ -32,7 +32,8 @@ const en = {
|
||||
dailyClaimLimit: 'Daily Claim Limit',
|
||||
claimed: 'Claimed',
|
||||
availableForWithdrawal: 'Available for Withdrawal (Cash)',
|
||||
cashUnit: 'CNY',
|
||||
availablePoints: 'Available Points',
|
||||
cashUnit: 'RM',
|
||||
claimNow: 'Claim Now',
|
||||
syncBalance: 'Sync Balance',
|
||||
syncing: 'Syncing...',
|
||||
|
||||
@@ -32,7 +32,8 @@ const zh = {
|
||||
dailyClaimLimit: '今日可领取上限',
|
||||
claimed: '已领取',
|
||||
availableForWithdrawal: '目前可提现(现金)',
|
||||
cashUnit: '元',
|
||||
availablePoints: '可使用积分',
|
||||
cashUnit: 'RM',
|
||||
claimNow: '立即领取',
|
||||
syncBalance: '同步额度',
|
||||
syncing: '同步中...',
|
||||
|
||||
@@ -9,6 +9,7 @@ import {ArrowLeft, BadgeCheck, MapPinHouse, PencilLine, Plus, Trash2} from 'luci
|
||||
import {useAddressBook} from '@/features/addressBook'
|
||||
import {GoodsRedeemModal} from '@/features/goods'
|
||||
import {notifySuccess} from '@/features/notifications'
|
||||
import {MotionButton, MotionDiv, tapMotionProps, tapMotionPropsIcon, tapMotionPropsSoft} from '@/lib/motion'
|
||||
import type {AddressListItem} from '@/types/address.type.ts'
|
||||
|
||||
function AccountPage() {
|
||||
@@ -66,7 +67,9 @@ function AccountPage() {
|
||||
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]">
|
||||
<MotionDiv {...tapMotionPropsIcon}>
|
||||
<ArrowLeft className="h-[16px] w-[16px]" aria-hidden="true" />
|
||||
</MotionDiv>
|
||||
<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>
|
||||
@@ -85,14 +88,15 @@ function AccountPage() {
|
||||
<div className="text-[16px] font-semibold text-white">{t('account.myShippingAddress')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
<MotionButton
|
||||
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}
|
||||
{...tapMotionProps}
|
||||
>
|
||||
<Plus className="h-[14px] w-[14px]" aria-hidden="true" />
|
||||
{t('account.addAddress')}
|
||||
</button>
|
||||
</MotionButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -140,22 +144,24 @@ function AccountPage() {
|
||||
<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
|
||||
<MotionButton
|
||||
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)}
|
||||
{...tapMotionPropsSoft}
|
||||
>
|
||||
<PencilLine className="h-[12px] w-[12px]" aria-hidden="true" />
|
||||
{t('account.edit')}
|
||||
</button>
|
||||
<button
|
||||
</MotionButton>
|
||||
<MotionButton
|
||||
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)}
|
||||
{...tapMotionPropsSoft}
|
||||
>
|
||||
<Trash2 className="h-[12px] w-[12px]" aria-hidden="true" />
|
||||
{t('account.delete')}
|
||||
</button>
|
||||
</MotionButton>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import {Link, useSearchParams} from 'react-router-dom'
|
||||
|
||||
import PageLayout from '@/components/layout'
|
||||
import {HOME_CATEGORY_META_MAP, HOME_GOOD_TYPE_ORDER} from '@/constant'
|
||||
import {MotionDiv, tapMotionPropsIcon} from '@/lib/motion'
|
||||
import {
|
||||
GoodsCategoryList,
|
||||
GoodsRedeemModal,
|
||||
@@ -29,7 +30,9 @@ function GoodsPage() {
|
||||
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]">
|
||||
<MotionDiv {...tapMotionPropsIcon}>
|
||||
<ArrowLeft className="h-[16px] w-[16px]" aria-hidden="true"/>
|
||||
</MotionDiv>
|
||||
<span className="text-[14px] font-medium text-white/92">{t('common.back')}</span>
|
||||
</div>
|
||||
<div className="text-[15px] font-semibold text-[#F56E10]">{pageTitle}</div>
|
||||
|
||||
@@ -7,13 +7,12 @@ import {useTranslation} from 'react-i18next'
|
||||
import PageLayout from '@/components/layout'
|
||||
import Modal from '@/components/modal'
|
||||
import Button from '@/components/button'
|
||||
import {Link, useNavigate} from 'react-router-dom'
|
||||
import {useNavigate} from 'react-router-dom'
|
||||
import {
|
||||
ChevronRight,
|
||||
Coins,
|
||||
Gauge,
|
||||
History,
|
||||
Wallet,
|
||||
UserRound,
|
||||
} from 'lucide-react'
|
||||
import type {
|
||||
@@ -31,14 +30,16 @@ import {
|
||||
import {validateClaimSubmission} from '@/features/home/claimValidation'
|
||||
import {claim} from '@/api/business.ts'
|
||||
import {notifyError, notifySuccess} from '@/features/notifications'
|
||||
import {MotionButton, MotionLink, tapMotionProps} from '@/lib/motion'
|
||||
import {useUserStore} from "@/store/user.ts";
|
||||
import {normalizeLanguage} from '@/lib/i18n'
|
||||
|
||||
function QuickNavCard({icon: Icon, label, to}: QuickNavCardProps) {
|
||||
return (
|
||||
<Link
|
||||
<MotionLink
|
||||
to={to}
|
||||
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]"
|
||||
{...tapMotionProps}
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-[10px]">
|
||||
<div
|
||||
@@ -48,7 +49,7 @@ function QuickNavCard({icon: Icon, label, to}: QuickNavCardProps) {
|
||||
<div className="truncate font-medium capitalize">{label}</div>
|
||||
</div>
|
||||
<ChevronRight className="h-[16px] w-[16px] shrink-0 text-white/70" aria-hidden="true"/>
|
||||
</Link>
|
||||
</MotionLink>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -85,6 +86,7 @@ function HomePage() {
|
||||
const {assetsInfo} = useAssetsQuery()
|
||||
const claimProgress = getProgressPercent(assetsInfo?.today_claimed, assetsInfo?.today_limit)
|
||||
const isClaimAvailable = (assetsInfo?.locked_points ?? 0) > 0
|
||||
const isEnglish = normalizeLanguage(language) === 'en'
|
||||
const previewCategories: ProductCategory[] = productCategories.map((category) => ({
|
||||
...category,
|
||||
items: category.items.slice(0, 4),
|
||||
@@ -145,49 +147,57 @@ function HomePage() {
|
||||
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
|
||||
<MotionButton
|
||||
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}
|
||||
{...tapMotionProps}
|
||||
>
|
||||
<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)]">
|
||||
<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
|
||||
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>
|
||||
</MotionButton>
|
||||
</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-[12px] sm:p-[14px]">
|
||||
<div
|
||||
className="home-stat-card home-stat-card-claim pc-hover-float flex min-h-[167px] flex-col justify-between p-[12px] sm:p-[14px]">
|
||||
<div className="flex items-start justify-between gap-[12px]">
|
||||
<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="text-[12px] tracking-[0.1em] text-white/58 sm:text-[13px] sm:tracking-[0.16em]">{t('home.claimablePoints')}</div>
|
||||
<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-[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]">
|
||||
className="flex h-[34px] w-[34px] shrink-0 items-center justify-center rounded-[10px] bg-[#d58a2d]/18 text-[#f5a322] 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-[20ch] text-[12px] leading-[1.5] text-white/68 sm:max-w-[28ch] sm:text-[13px] sm:leading-[1.6]">
|
||||
<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-[12px] sm:p-[14px]">
|
||||
<div
|
||||
className="home-stat-card home-stat-card-limit pc-hover-float flex min-h-[167px] flex-col justify-around p-[12px] sm:p-[14px]">
|
||||
<div className="flex items-start justify-between gap-[12px]">
|
||||
<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="text-[12px] tracking-[0.1em] text-white/58 sm:text-[13px] sm:tracking-[0.16em]">{t('home.dailyClaimLimit')}</div>
|
||||
<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-[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]">
|
||||
className="flex h-[34px] w-[34px] shrink-0 items-center justify-center rounded-[10px] bg-[#d58a2d]/18 text-[#f5a322] 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>
|
||||
@@ -211,24 +221,42 @@ function HomePage() {
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div
|
||||
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">{t('home.availableForWithdrawal')}</div>
|
||||
className="home-withdraw-card pc-hover-float h-full min-h-[120px] py-[14px] sm:py-[18px]">
|
||||
<div className="flex h-full w-full flex-col justify-between gap-[18px]">
|
||||
<div
|
||||
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}
|
||||
className={`text-center text-white/68 ${
|
||||
isEnglish
|
||||
? 'text-[10px] tracking-[0.01em] sm:text-[13px] sm:tracking-[0.06em]'
|
||||
: 'text-[11px] tracking-[0.04em] sm:text-[14px] sm:tracking-[0.16em]'
|
||||
}`}>{t('home.availableForWithdrawal')}</div>
|
||||
<div className="flex flex-1 items-center justify-center text-center">
|
||||
<div
|
||||
className="min-w-0 break-all text-[clamp(1.6rem,5vw,2.6rem)] font-semibold leading-none tracking-[-0.02em] text-white">
|
||||
{assetsInfo?.withdrawable_cash || 0}
|
||||
<span
|
||||
className="text-[13px] tracking-[0.16em] text-white/58 ml-[10px]">{t('home.cashUnit')}</span>
|
||||
className="ml-[8px] align-middle text-[12px] tracking-[0.14em] text-white/62 sm:text-[14px]">{t('home.cashUnit')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex h-[38px] w-[38px] items-center justify-center rounded-[12px] bg-[#FA6A00]/16 text-[#FE9F00]">
|
||||
<Wallet className="h-[18px] w-[18px]" aria-hidden="true"/>
|
||||
className="home-withdraw-card pc-hover-float h-full min-h-[120px] px-[12px] py-[14px] sm:px-[16px] sm:py-[18px]">
|
||||
<div className="flex h-full w-full flex-col justify-between gap-[18px]">
|
||||
<div
|
||||
className="text-center text-[12px] tracking-[0.08em] text-white/68 sm:text-[14px] sm:tracking-[0.16em]">{t('home.availablePoints')}</div>
|
||||
<div className="flex flex-1 items-center justify-center text-center">
|
||||
<div
|
||||
className="min-w-0 break-all text-[clamp(1.6rem,5vw,2.6rem)] font-semibold leading-none tracking-[-0.02em] text-white">
|
||||
{assetsInfo?.available_points || 0}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="liquid-glass-bg grid grid-cols-2 gap-[10px] p-[5px]">
|
||||
</div>
|
||||
</div>
|
||||
<div className="home-withdraw-actions grid grid-cols-2 gap-[10px] p-[10px] box-border">
|
||||
<Button
|
||||
className="h-[44px] w-full text-[13px]"
|
||||
onClick={handleOpenClaimModal}
|
||||
|
||||
@@ -6,6 +6,7 @@ import PageLayout from '@/components/layout'
|
||||
import {ORDER_STATUS} from '@/constant'
|
||||
import i18n from '@/lib/i18n'
|
||||
import { cn } from '@/lib'
|
||||
import {MotionButton, MotionDiv, tapMotionPropsIcon, tapMotionPropsSoft} from '@/lib/motion'
|
||||
import Modal from '@/components/modal'
|
||||
import Button from '@/components/button'
|
||||
import type { OrderCardProps, OrderRecord, PointsCardProps, RecordButtonType, TabButtonProps } from '@/types'
|
||||
@@ -266,7 +267,7 @@ function getOrderStatusClassName(status: string) {
|
||||
|
||||
function TabButton({ active, label, icon: Icon, onClick }: TabButtonProps) {
|
||||
return (
|
||||
<button
|
||||
<MotionButton
|
||||
type="button"
|
||||
className={cn(
|
||||
'inline-flex min-w-[140px] cursor-pointer items-center justify-center gap-[8px] rounded-[10px] border px-[14px] py-[9px] text-[13px] transition-colors focus-visible:ring-2 focus-visible:ring-[#FE9F00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070E]',
|
||||
@@ -275,10 +276,11 @@ function TabButton({ active, label, icon: Icon, onClick }: TabButtonProps) {
|
||||
: 'border-white/35 bg-white/3 text-[#B8B1AA] hover:bg-white/6',
|
||||
)}
|
||||
onClick={onClick}
|
||||
{...tapMotionPropsSoft}
|
||||
>
|
||||
<Icon className="h-[15px] w-[15px]" aria-hidden="true" />
|
||||
{label}
|
||||
</button>
|
||||
</MotionButton>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -297,14 +299,15 @@ function OrderCard({ record, onOpenDetails }: OrderCardProps) {
|
||||
{t('record.trackingNumber')} {record.trackingNumber}
|
||||
</div>
|
||||
) : null}
|
||||
<button
|
||||
<MotionButton
|
||||
type="button"
|
||||
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)}
|
||||
{...tapMotionPropsSoft}
|
||||
>
|
||||
{t('record.checkDetails')}
|
||||
<ChevronRight className="h-[14px] w-[14px]" aria-hidden="true" />
|
||||
</button>
|
||||
</MotionButton>
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 flex-col items-end gap-[10px]">
|
||||
@@ -450,7 +453,9 @@ function RecordPage() {
|
||||
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]">
|
||||
<MotionDiv {...tapMotionPropsIcon}>
|
||||
<ArrowLeft className="h-[16px] w-[16px]" aria-hidden="true" />
|
||||
</MotionDiv>
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user