init project

This commit is contained in:
JiaJun
2026-03-18 11:42:00 +08:00
commit 2db8d180ad
29 changed files with 3457 additions and 0 deletions

100
src/views/account/index.tsx Normal file
View File

@@ -0,0 +1,100 @@
import PageLayout from '@/components/layout'
import BorderlessTable, { type TableColumn } from '@/components/table'
import { Link } from 'react-router-dom'
type AccountTableRow = {
name: string
phone: string
address: string
code: string
action: string
setting: string
}
function AccountPage() {
const columns: TableColumn<AccountTableRow>[] = [
{
label: 'Name',
key: 'name',
render: (value: string) => <div>{value}</div>
},
{
label: 'Phone / Mobile',
key: 'phone',
render: (value: string) => <div>{value}</div>
},
{
label: 'Address',
key: 'address',
render: (value: string) => <div>{value}</div>
},
{
label: 'Postal Code',
key: 'code',
render: (value: string) => <div>{value}</div>
},
{
label: 'Action',
key: 'action',
render: (value: string) => <div>{value}</div>
},
{
label: 'Default Setting',
key: 'setting',
render: (value: string) => <div>{value}</div>
}
]
const dataSource: 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',
},
]
return (
<PageLayout contentClassName="min-h-screen">
<Link
to="/"
className={'relative text-[#F56E10] flex h-[40px] w-full items-center justify-center bg-[#08070E]/70'}
>
<div className={'absolute left-[16px]'}> &lt; </div>
<div>Account</div>
</Link>
<div className={'mt-[20px] w-full flex items-center justify-center'}>
<div className={'w-[80%]'}>
<div className={'w-full mb-[10px] flex items-center justify-between'}>
<div>My Shipping Address</div>
<div className={'liquid-glass-bg px-[10px] py-[5px] text-sm'}>Add Address</div>
</div>
<BorderlessTable columns={columns} dataSource={dataSource} />
</div>
</div>
</PageLayout>
)
}
export default AccountPage

391
src/views/home/index.tsx Normal file
View File

@@ -0,0 +1,391 @@
import { useState } from 'react'
import recordSvg from '@/assets/record.svg'
import accountSvg from '@/assets/account.svg'
import PageLayout from '@/components/layout'
import Modal from '@/components/modal'
import { Link } from 'react-router-dom'
type QuickNavCardProps = {
icon: string
label: string
to: string
}
type ProductItem = {
id: string
title: string
subtitle: string
points: string
ctaLabel: string
imageClassName: string
}
type ProductCategory = {
id: string
name: string
items: ProductItem[]
}
type AddressOption = {
id: string
name: string
phone: string
address: string
postalCode: string
isDefault?: boolean
}
function QuickNavCard({ icon, label, to }: QuickNavCardProps) {
return (
<Link
to={to}
className={'liquid-glass-bg rounded-[10px] flex items-center pr-[10px] cursor-pointer'}
>
<div className={'flex px-[10px] py-[5px] items-center gap-[5px]'}>
<div className={'p-[5px] rounded-[10px] bg-linear-to-b from-[#FB8001] to-[#FCAA2C]'}>
<img src={icon} className={'w-[16px] h-[16px]'} />
</div>
<div>{label}</div>
</div>
<div>&gt;</div>
</Link>
)
}
const productCategories: ProductCategory[] = [
{
id: 'transfer-to-platform',
name: 'Transfer to Platform',
items: [
{
id: 'transfer-100',
title: 'Transfer 100',
subtitle: '1 x Turnover',
points: '1000 Points',
ctaLabel: 'Transfer Now',
imageClassName: 'bg-[linear-gradient(135deg,_#5B1D00_0%,_#FA6A00_55%,_#FFBC6D_100%)]',
},
{
id: 'transfer-300',
title: 'Transfer 300',
subtitle: '2 x Turnover',
points: '2800 Points',
ctaLabel: 'Transfer Now',
imageClassName: 'bg-[linear-gradient(135deg,_#1C1A48_0%,_#5050D8_55%,_#8AB6FF_100%)]',
},
{
id: 'transfer-500',
title: 'Transfer 500',
subtitle: '3 x Turnover',
points: '4200 Points',
ctaLabel: 'Transfer Now',
imageClassName: 'bg-[linear-gradient(135deg,_#153A35_0%,_#1F9D8B_55%,_#8BF3D8_100%)]',
},
{
id: 'transfer-1000',
title: 'Transfer 1000',
subtitle: '5 x Turnover',
points: '7800 Points',
ctaLabel: 'Transfer Now',
imageClassName: 'bg-[linear-gradient(135deg,_#421515_0%,_#C93D3D_52%,_#FF9B9B_100%)]',
},
],
},
{
id: 'game-bonus',
name: 'Game Bonus',
items: [
{
id: 'bonus-spin',
title: 'Lucky Spin Pack',
subtitle: 'Bonus Voucher',
points: '850 Points',
ctaLabel: 'Redeem Bonus',
imageClassName: 'bg-[linear-gradient(135deg,_#28103D_0%,_#8E3DD1_50%,_#F1A8FF_100%)]',
},
{
id: 'bonus-chip',
title: 'Chip Booster',
subtitle: 'Casino Special',
points: '1600 Points',
ctaLabel: 'Redeem Bonus',
imageClassName: 'bg-[linear-gradient(135deg,_#2D2407_0%,_#C79200_52%,_#FFE58A_100%)]',
},
{
id: 'bonus-cashback',
title: 'Cashback Card',
subtitle: 'Weekly Reward',
points: '2400 Points',
ctaLabel: 'Redeem Bonus',
imageClassName: 'bg-[linear-gradient(135deg,_#062A2F_0%,_#1296A5_52%,_#6DE2F0_100%)]',
},
{
id: 'bonus-vip',
title: 'VIP Match Bonus',
subtitle: 'Limited Access',
points: '5000 Points',
ctaLabel: 'Redeem Bonus',
imageClassName: 'bg-[linear-gradient(135deg,_#35120A_0%,_#DD6C2F_52%,_#FFC09A_100%)]',
},
],
},
{
id: 'physical-prizes',
name: 'Physical Prizes',
items: [
{
id: 'prize-headset',
title: 'Gaming Headset',
subtitle: 'Physical Delivery',
points: '6800 Points',
ctaLabel: 'Claim Prize',
imageClassName: 'bg-[linear-gradient(135deg,_#111827_0%,_#374151_50%,_#A3AAB8_100%)]',
},
{
id: 'prize-mouse',
title: 'Wireless Mouse',
subtitle: 'Physical Delivery',
points: '4200 Points',
ctaLabel: 'Claim Prize',
imageClassName: 'bg-[linear-gradient(135deg,_#19212F_0%,_#3A5B84_50%,_#98B8E3_100%)]',
},
{
id: 'prize-speaker',
title: 'Bluetooth Speaker',
subtitle: 'Physical Delivery',
points: '7300 Points',
ctaLabel: 'Claim Prize',
imageClassName: 'bg-[linear-gradient(135deg,_#32120F_0%,_#A73F2F_50%,_#F0A28D_100%)]',
},
{
id: 'prize-keyboard',
title: 'Mechanical Keyboard',
subtitle: 'Physical Delivery',
points: '9500 Points',
ctaLabel: 'Claim Prize',
imageClassName: 'bg-[linear-gradient(135deg,_#142C17_0%,_#2B7A38_52%,_#8DE69B_100%)]',
},
],
},
]
const addressOptions: AddressOption[] = [
{
id: 'address-shanghai',
name: 'Jia Jun',
phone: '+86 138 0000 1288',
address: 'No. 88 Century Avenue, Pudong New Area, Shanghai',
postalCode: '200120',
isDefault: true,
},
{
id: 'address-singapore',
name: 'Alicia Tan',
phone: '+65 9123 4567',
address: '18 Robinson Road, Singapore',
postalCode: '048547',
},
{
id: 'address-kuala-lumpur',
name: 'Marcus Lee',
phone: '+60 12 778 9911',
address: '27 Jalan Bukit Bintang, Kuala Lumpur',
postalCode: '55100',
},
]
function HomePage() {
const [selectedProduct, setSelectedProduct] = useState<ProductItem | null>(null)
const [selectedAddressId, setSelectedAddressId] = useState<string>(addressOptions[0]?.id ?? '')
const handleOpenRedeemModal = (product: ProductItem) => {
setSelectedProduct(product)
setSelectedAddressId(addressOptions[0]?.id ?? '')
}
const handleCloseRedeemModal = () => {
setSelectedProduct(null)
}
return (
<PageLayout>
<div className={'flex justify-end gap-2 py-[10px]'}>
<QuickNavCard to="/record" icon={recordSvg} label="record" />
<QuickNavCard to="/account" icon={accountSvg} label="account" />
</div>
<div>
<div className={'flex gap-[5px]'}>
<div className={'liquid-glass-bg h-[167px] w-[267px] p-[10px] flex flex-col justify-start'}>
<div>Claimable Points</div>
<div>2,880</div>
<div>Yesterday's losses
have been converted
to points.Claim to use.
</div>
</div>
<div className={'liquid-glass-bg h-[167px] w-[267px] p-[10px] flex flex-col justify-start'}>
<div>Daily Claim Limit</div>
<div>1,500</div>
<div
className={'progress-bar'}
role="progressbar"
aria-valuemin={0}
aria-valuemax={1500}
aria-valuenow={800}
>
<div className={'progress-bar__fill'} style={{ width: '53.33%' }}></div>
</div>
<div>Claimed: 800 / 1500</div>
</div>
<div className={'flex flex-col gap-[5px]'}>
<div className={'liquid-glass-bg flex-1'}>
<div>Available for Withdrawal (Cash)</div>
<div>152 CNY</div>
</div>
<div className={'h-[54px] w-[564px] liquid-glass-bg flex gap-[10px] p-[5px]'}>
<button className={'button-play flex-1'}>Claim Now</button>
<button className={'button-play flex-1'}>Sync Balance</button>
</div>
</div>
</div>
</div>
<div>
{
productCategories.map((category) => (<div key={category.id} className={'mt-[20px]'}>
<div className={'flex items-center justify-between mb-[5px]'}>
<div className={'font-bold text-[14px]'}>{category.name}</div>
<div
className={'text-[#FA6A00] text-[12px] font-light underline cursor-pointer'}>more
</div>
</div>
<div className={'grid grid-cols-1 gap-4 md:grid-cols-4 xl:grid-cols-4'}>
{
category.items.map((product) => (
<div
key={product.id}
className={'liquid-glass-bg aspect-[215/260] w-full flex flex-col items-stretch justify-start'}>
<div className={`${product.imageClassName} w-full h-[40%] rounded-t-[10px]`}></div>
<div
className={'p-[10px] w-full flex-1 flex flex-col justify-around items-start'}>
<div>{product.title}</div>
<div className={'text-neutral-500'}>{product.subtitle}</div>
<div className={'text-[#FA6A00]'}>{product.points}</div>
<button
className={'button-play w-full h-[30px]'}
onClick={() => handleOpenRedeemModal(product)}
>
{product.ctaLabel}
</button>
</div>
</div>
))
}
</div>
</div>))
}
</div>
<Modal
open={Boolean(selectedProduct)}
title="Redeem Product"
onClose={handleCloseRedeemModal}
className="max-w-[720px]"
bodyClassName="space-y-[18px]"
footer={
<>
<button
type="button"
className="h-[38px] rounded-[8px] border border-white/15 bg-white/5 px-[18px] text-[14px] text-white transition-colors hover:bg-white/10"
onClick={handleCloseRedeemModal}
>
Cancel
</button>
<button
type="button"
className="button-play h-[38px]"
onClick={handleCloseRedeemModal}
>
Confirm
</button>
</>
}
>
{selectedProduct ? (
<>
<div className="flex gap-[14px] rounded-[12px] bg-white/5 p-[12px]">
<div
className={`${selectedProduct.imageClassName} h-[110px] w-[140px] shrink-0 rounded-[10px]`}
></div>
<div className="flex min-w-0 flex-1 flex-col justify-between">
<div>
<div className="text-[20px] font-bold text-white">{selectedProduct.title}</div>
<div className="mt-[6px] text-[13px] text-white/60">{selectedProduct.subtitle}</div>
</div>
<div className="text-[18px] font-bold text-[#FA6A00]">{selectedProduct.points}</div>
</div>
</div>
<div>
<div className="mb-[10px] text-[14px] font-bold text-white">Select Shipping Address</div>
<div className="space-y-[10px]">
{addressOptions.map((address) => {
const isSelected = selectedAddressId === address.id
return (
<button
key={address.id}
type="button"
className={`flex w-full items-start gap-[12px] rounded-[12px] border px-[14px] py-[14px] text-left transition-colors ${
isSelected
? 'border-[#FA6A00] bg-[#FA6A00]/12'
: 'border-white/10 bg-white/4 hover:bg-white/8'
}`}
onClick={() => setSelectedAddressId(address.id)}
>
<div
className={`mt-[3px] h-[16px] w-[16px] rounded-full border ${
isSelected
? 'border-[#FA6A00] bg-[#FA6A00] shadow-[0_0_12px_rgba(250,106,0,0.45)]'
: 'border-white/30'
}`}
>
<div
className={`m-auto mt-[3px] h-[6px] w-[6px] rounded-full bg-white ${
isSelected ? 'block' : 'hidden'
}`}
></div>
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-[8px]">
<div className="text-[14px] font-bold text-white">{address.name}</div>
<div className="text-[13px] text-white/60">{address.phone}</div>
{address.isDefault ? (
<div className="rounded-full bg-[#FA6A00]/18 px-[8px] py-[2px] text-[11px] text-[#FFB36D]">
Default
</div>
) : null}
</div>
<div className="mt-[6px] text-[13px] leading-[1.5] text-white/75">
{address.address}
</div>
<div className="mt-[4px] text-[12px] text-white/45">
Postal Code: {address.postalCode}
</div>
</div>
</button>
)
})}
</div>
</div>
</>
) : null}
</Modal>
</PageLayout>
)
}
export default HomePage;

View File

@@ -0,0 +1,99 @@
import PageLayout from '@/components/layout'
import {Link} from 'react-router-dom'
import type {RecordButtonType} from "@/types";
import {useState} from "react";
import {cn} from "@/lib";
function RecordPage() {
const mockData = [
{
date: '2025-03-04 10:20',
name: ' Bonus',
content: 'Daily Rebate 50',
status: 'Issued',
points: '800'
},
{
date: '2025-03-04 10:20',
name: ' Bonus',
content: 'Daily Rebate 50',
status: 'Issued',
points: '800'
},
{
date: '2025-03-04 10:20',
name: ' Bonus',
content: 'Daily Rebate 50',
status: 'Issued',
points: '800'
},
{
date: '2025-03-04 10:20',
name: ' Bonus',
content: 'Daily Rebate 50',
status: 'Issued',
points: '800'
}
]
const [btnType, setBtnType] = useState<RecordButtonType>('record')
return (
<PageLayout contentClassName="min-h-screen">
<Link
to="/"
className={'relative text-[#F56E10] flex h-[40px] w-full items-center justify-center bg-[#08070E]/70'}
>
<div className={'absolute left-[16px]'}> &lt; </div>
<div>Record</div>
</Link>
<div className={'w-[60%] mx-auto'}>
<div>
<div className={'flex gap-[10px] my-[15px]'}>
<div
className={cn('cursor-pointer text-[#B2ADAA] border-1 border-[#B2ADAA] rounded-[10px] p-[5px]', {
'text-[#ffffff] bg-[#FA6A00]': btnType === 'order',
})}
onClick={() => setBtnType('order')}
>My Orders
</div>
<div
className={cn('cursor-pointer text-[#B2ADAA] border-1 border-[#B2ADAA] rounded-[10px] p-[5px]', {
'text-[#ffffff] bg-[#FA6A00] border-[#FA6A00]': btnType === 'record',
})}
onClick={() => setBtnType('record')}
>Points Record
</div>
</div>
<hr className={'text-[#A4A4A4] border-[1px]'}></hr>
</div>
<div className={'flex flex-col mt-[10px]'}>
{
mockData.map((item, index) => (
<div key={index} className={'flex flex-col my-[10px]'}>
<div
className={'bg-linear-to-r from-[#F96C02] to-[#FE9F00] rounded-t-[10px] py-[5px] px-[10px]'}>{item.date} - {item.name} </div>
<div
className={'liquid-glass-bg !rounded-t-none px-[10px] py-[10px] pr-[20px] flex items-center justify-between'}>
<div className={'flex flex-col'}>
<div>{item.content}</div>
<div className={'text-[#FA6A00]'}>Check the Detail</div>
</div>
<div className={'flex flex-col items-end'}>
<div>{item.status}</div>
<div>{item.points}</div>
</div>
</div>
</div>
))
}
</div>
</div>
</PageLayout>
)
}
export default RecordPage;