diff --git a/package.json b/package.json
index 716f01e..e18ebc6 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 57aeb3b..085140e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -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: {}
diff --git a/public/cash-bg.png b/public/cash-bg.png
new file mode 100644
index 0000000..7648391
Binary files /dev/null and b/public/cash-bg.png differ
diff --git a/src/components/modal/index.tsx b/src/components/modal/index.tsx
index 022b48b..d773e2e 100644
--- a/src/components/modal/index.tsx
+++ b/src/components/modal/index.tsx
@@ -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 (
-
{title}
{onClose ? (
-
+
) : null}
diff --git a/src/features/goods/GoodsCategoryList.tsx b/src/features/goods/GoodsCategoryList.tsx
index d8f634e..f9921a3 100644
--- a/src/features/goods/GoodsCategoryList.tsx
+++ b/src/features/goods/GoodsCategoryList.tsx
@@ -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({
{t(HOME_CATEGORY_META_MAP[category.id].nameKey)}
{showMore && onMoreClick ? (
-
+
) : null}
{category.items.map((product) => (
-
+
@@ -304,13 +306,14 @@ export function GoodsRedeemModal({
{addressOptions.map((address) => {
const isSelected = selectedAddressId === address.id
return (
-
-
+
)
})}
-
+
>
diff --git a/src/index.css b/src/index.css
index 4db64b4..c94bb9a 100644
--- a/src/index.css
+++ b/src/index.css
@@ -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);
+ }
+}
diff --git a/src/lib/index.ts b/src/lib/index.ts
index 951b523..e550405 100644
--- a/src/lib/index.ts
+++ b/src/lib/index.ts
@@ -5,5 +5,6 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
+export * from './motion'
export * from './request'
export * from './tool'
diff --git a/src/lib/motion.ts b/src/lib/motion.ts
new file mode 100644
index 0000000..ace309c
--- /dev/null
+++ b/src/lib/motion.ts
@@ -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)
diff --git a/src/message/en.ts b/src/message/en.ts
index 929b79f..4856b42 100644
--- a/src/message/en.ts
+++ b/src/message/en.ts
@@ -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...',
diff --git a/src/message/zh.ts b/src/message/zh.ts
index b32b624..7116572 100644
--- a/src/message/zh.ts
+++ b/src/message/zh.ts
@@ -32,7 +32,8 @@ const zh = {
dailyClaimLimit: '今日可领取上限',
claimed: '已领取',
availableForWithdrawal: '目前可提现(现金)',
- cashUnit: '元',
+ availablePoints: '可使用积分',
+ cashUnit: 'RM',
claimNow: '立即领取',
syncBalance: '同步额度',
syncing: '同步中...',
diff --git a/src/views/account/index.tsx b/src/views/account/index.tsx
index 02f4c66..29e114b 100644
--- a/src/views/account/index.tsx
+++ b/src/views/account/index.tsx
@@ -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]"
>
-
+
+
+
{t('common.back')}
{t('account.title')}
@@ -85,14 +88,15 @@ function AccountPage() {
{t('account.myShippingAddress')}
-
+
@@ -140,22 +144,24 @@ function AccountPage() {
{addressText}
-
-
+
)
diff --git a/src/views/goods/index.tsx b/src/views/goods/index.tsx
index 3c9b907..4d8295f 100644
--- a/src/views/goods/index.tsx
+++ b/src/views/goods/index.tsx
@@ -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]"
>
-
+
+
+
{t('common.back')}
{pageTitle}
diff --git a/src/views/home/index.tsx b/src/views/home/index.tsx
index a4ab252..2670216 100644
--- a/src/views/home/index.tsx
+++ b/src/views/home/index.tsx
@@ -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 (
-
-
+
)
}
@@ -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">
-