init project
This commit is contained in:
100
src/views/account/index.tsx
Normal file
100
src/views/account/index.tsx
Normal 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]'}> < </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
391
src/views/home/index.tsx
Normal 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>></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;
|
||||
99
src/views/record/index.tsx
Normal file
99
src/views/record/index.tsx
Normal 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]'}> < </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;
|
||||
Reference in New Issue
Block a user