feat: 初始化项目所有界面
This commit is contained in:
@@ -66,7 +66,7 @@ export function Modal({
|
||||
<div className="liquid-glass-bg !rounded-t-none bg-[#08070E]/75">
|
||||
<div className={cn('px-[20px] py-[20px] text-white', bodyClassName)}>{children}</div>
|
||||
{footer ? (
|
||||
<div className="flex items-center justify-end gap-[10px] px-[20px] pb-[20px]">
|
||||
<div className="flex flex-wrap items-center justify-center gap-[60px] px-[20px] pb-[20px]">
|
||||
{footer}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
BIN
src/figma/img.png
Normal file
BIN
src/figma/img.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 363 KiB |
@@ -27,6 +27,11 @@ type ProductCategory = {
|
||||
items: ProductItem[]
|
||||
}
|
||||
|
||||
type SelectedProductState = {
|
||||
categoryId: ProductCategory['id']
|
||||
product: ProductItem
|
||||
}
|
||||
|
||||
type AddressOption = {
|
||||
id: string
|
||||
name: string
|
||||
@@ -36,6 +41,17 @@ type AddressOption = {
|
||||
isDefault?: boolean
|
||||
}
|
||||
|
||||
type ModalMode = 'select-address' | 'add-address'
|
||||
|
||||
type AddAddressForm = {
|
||||
name: string
|
||||
phone: string
|
||||
region: string
|
||||
detailedAddress: string
|
||||
postalCode: string
|
||||
isDefault: boolean
|
||||
}
|
||||
|
||||
function QuickNavCard({ icon, label, to }: QuickNavCardProps) {
|
||||
return (
|
||||
<Link
|
||||
@@ -170,7 +186,7 @@ const productCategories: ProductCategory[] = [
|
||||
},
|
||||
]
|
||||
|
||||
const addressOptions: AddressOption[] = [
|
||||
const initialAddressOptions: AddressOption[] = [
|
||||
{
|
||||
id: 'address-shanghai',
|
||||
name: 'Jia Jun',
|
||||
@@ -195,19 +211,123 @@ const addressOptions: AddressOption[] = [
|
||||
},
|
||||
]
|
||||
|
||||
function HomePage() {
|
||||
const [selectedProduct, setSelectedProduct] = useState<ProductItem | null>(null)
|
||||
const [selectedAddressId, setSelectedAddressId] = useState<string>(addressOptions[0]?.id ?? '')
|
||||
const emptyAddressForm: AddAddressForm = {
|
||||
name: '',
|
||||
phone: '',
|
||||
region: '',
|
||||
detailedAddress: '',
|
||||
postalCode: '',
|
||||
isDefault: false,
|
||||
}
|
||||
|
||||
const handleOpenRedeemModal = (product: ProductItem) => {
|
||||
setSelectedProduct(product)
|
||||
function getNumericValue(value: string) {
|
||||
const matched = value.match(/\d+/)
|
||||
return matched ? matched[0] : value
|
||||
}
|
||||
|
||||
function getTurnoverRequirement(subtitle: string) {
|
||||
const matched = subtitle.match(/\d+/)
|
||||
return matched ? `${matched[0]}x` : subtitle
|
||||
}
|
||||
|
||||
function HomePage() {
|
||||
const [selectedProduct, setSelectedProduct] = useState<SelectedProductState | null>(null)
|
||||
const [claimModalOpen, setClaimModalOpen] = useState(false)
|
||||
const [modalMode, setModalMode] = useState<ModalMode>('select-address')
|
||||
const [addressOptions, setAddressOptions] = useState<AddressOption[]>(initialAddressOptions)
|
||||
const [selectedAddressId, setSelectedAddressId] = useState<string>(initialAddressOptions[0]?.id ?? '')
|
||||
const [addressForm, setAddressForm] = useState<AddAddressForm>(emptyAddressForm)
|
||||
|
||||
const handleOpenRedeemModal = (product: ProductItem, categoryId: ProductCategory['id']) => {
|
||||
setSelectedProduct({
|
||||
product,
|
||||
categoryId,
|
||||
})
|
||||
setSelectedAddressId(addressOptions[0]?.id ?? '')
|
||||
setModalMode('select-address')
|
||||
setAddressForm(emptyAddressForm)
|
||||
}
|
||||
|
||||
const handleCloseRedeemModal = () => {
|
||||
setSelectedProduct(null)
|
||||
setModalMode('select-address')
|
||||
setAddressForm(emptyAddressForm)
|
||||
}
|
||||
|
||||
const handleOpenClaimModal = () => {
|
||||
setClaimModalOpen(true)
|
||||
}
|
||||
|
||||
const handleCloseClaimModal = () => {
|
||||
setClaimModalOpen(false)
|
||||
}
|
||||
|
||||
const handleOpenAddAddress = () => {
|
||||
setModalMode('add-address')
|
||||
}
|
||||
|
||||
const handleChangeAddressForm = (field: keyof AddAddressForm, value: string | boolean) => {
|
||||
setAddressForm((previous) => ({
|
||||
...previous,
|
||||
[field]: value,
|
||||
}))
|
||||
}
|
||||
|
||||
const isAddAddressFormValid = [
|
||||
addressForm.name,
|
||||
addressForm.phone,
|
||||
addressForm.region,
|
||||
addressForm.detailedAddress,
|
||||
].every((value) => value.trim())
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (modalMode === 'add-address') {
|
||||
if (!isAddAddressFormValid) {
|
||||
return
|
||||
}
|
||||
|
||||
const newAddress: AddressOption = {
|
||||
id: `address-${Date.now()}`,
|
||||
name: addressForm.name.trim(),
|
||||
phone: addressForm.phone.trim(),
|
||||
address: `${addressForm.region.trim()}, ${addressForm.detailedAddress.trim()}`,
|
||||
postalCode: addressForm.postalCode.trim() || 'N/A',
|
||||
isDefault: addressForm.isDefault,
|
||||
}
|
||||
|
||||
setAddressOptions((previous) => {
|
||||
const normalizedPrevious = addressForm.isDefault
|
||||
? previous.map((item) => ({ ...item, isDefault: false }))
|
||||
: previous
|
||||
|
||||
return [...normalizedPrevious, newAddress]
|
||||
})
|
||||
setSelectedAddressId(newAddress.id)
|
||||
setAddressForm(emptyAddressForm)
|
||||
setModalMode('select-address')
|
||||
return
|
||||
}
|
||||
handleCloseRedeemModal()
|
||||
}
|
||||
|
||||
const selectedCategoryId = selectedProduct?.categoryId
|
||||
const selectedProductData = selectedProduct?.product ?? null
|
||||
const isPhysicalPrize = selectedCategoryId === 'physical-prizes'
|
||||
const isTransferToPlatform = selectedCategoryId === 'transfer-to-platform'
|
||||
const isGameBonus = selectedCategoryId === 'game-bonus'
|
||||
const modalTitle = modalMode === 'add-address'
|
||||
? 'Add Shipping Address'
|
||||
: isTransferToPlatform
|
||||
? 'Confirm Withdrawal'
|
||||
: isGameBonus
|
||||
? 'Confirm Bonus Redemption'
|
||||
: 'Redeem Product'
|
||||
const modalMaxWidthClassName = isTransferToPlatform
|
||||
? 'max-w-[620px]'
|
||||
: isGameBonus
|
||||
? 'max-w-[460px]'
|
||||
: 'max-w-[720px]'
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className={'flex justify-end gap-2 py-[10px]'}>
|
||||
@@ -246,7 +366,9 @@ function HomePage() {
|
||||
<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'} onClick={handleOpenClaimModal}>
|
||||
Claim Now
|
||||
</button>
|
||||
<button className={'button-play flex-1'}>Sync Balance</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -276,7 +398,7 @@ function HomePage() {
|
||||
<div className={'text-[#FA6A00]'}>{product.points}</div>
|
||||
<button
|
||||
className={'button-play w-full h-[30px]'}
|
||||
onClick={() => handleOpenRedeemModal(product)}
|
||||
onClick={() => handleOpenRedeemModal(product, category.id)}
|
||||
>
|
||||
{product.ctaLabel}
|
||||
</button>
|
||||
@@ -291,99 +413,266 @@ function HomePage() {
|
||||
|
||||
<Modal
|
||||
open={Boolean(selectedProduct)}
|
||||
title="Redeem Product"
|
||||
title={modalTitle}
|
||||
onClose={handleCloseRedeemModal}
|
||||
className="max-w-[720px]"
|
||||
className={modalMaxWidthClassName}
|
||||
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}
|
||||
onClick={modalMode === 'add-address' ? () => setModalMode('select-address') : handleCloseRedeemModal}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="button-play h-[38px]"
|
||||
onClick={handleCloseRedeemModal}
|
||||
className={`button-play h-[38px] ${modalMode === 'add-address' && !isAddAddressFormValid ? 'opacity-50' : ''}`}
|
||||
onClick={handleConfirm}
|
||||
disabled={modalMode === 'add-address' && !isAddAddressFormValid}
|
||||
>
|
||||
Confirm
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{selectedProduct ? (
|
||||
{selectedProductData ? (
|
||||
<>
|
||||
<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>
|
||||
{isTransferToPlatform ? (
|
||||
<div className="rounded-[14px] bg-[#1C1818]/80 px-[18px] py-[14px] shadow-[0_10px_30px_rgba(0,0,0,0.22)]">
|
||||
<div className="divide-y divide-white/8">
|
||||
<div className="flex items-center justify-between py-[14px] text-white">
|
||||
<div className="text-[18px]">Withdrawal Amount</div>
|
||||
<div className="text-[18px]">{getNumericValue(selectedProductData.title)}</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-[14px] text-white">
|
||||
<div className="text-[18px]">Points Required</div>
|
||||
<div className="text-[18px]">{getNumericValue(selectedProductData.points)}</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-[14px] text-white">
|
||||
<div className="text-[18px] underline decoration-[#1E90FF] underline-offset-[3px]">
|
||||
Turnover Requirement
|
||||
</div>
|
||||
<div className="text-[18px]">{getTurnoverRequirement(selectedProductData.subtitle)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-[10px] text-center text-[18px] text-white/45">
|
||||
Submit withdrawal request?
|
||||
</div>
|
||||
<div className="text-[18px] font-bold text-[#FA6A00]">{selectedProduct.points}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : isGameBonus ? (
|
||||
<div className="rounded-[10px] bg-[#1C1818]/80 px-[16px] py-[8px] shadow-[0_10px_30px_rgba(0,0,0,0.22)]">
|
||||
<div className="divide-y divide-white/8">
|
||||
<div className="flex items-center justify-between py-[14px] text-white">
|
||||
<div className="text-[13px] text-white/78">Item</div>
|
||||
<div className="text-[14px]">{selectedProductData.title}</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-[14px] text-white">
|
||||
<div className="text-[13px] text-white/78">Points Required</div>
|
||||
<div className="text-[14px]">{getNumericValue(selectedProductData.points)}</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-[14px] text-white">
|
||||
<div className="text-[13px] text-white/78">Turnover Requirement</div>
|
||||
<div className="text-[14px]">{getTurnoverRequirement(selectedProductData.subtitle)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : modalMode === 'select-address' && isPhysicalPrize ? (
|
||||
<>
|
||||
<div className="flex gap-[14px] rounded-[12px] bg-white/5 p-[12px]">
|
||||
<div
|
||||
className={`${selectedProductData.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">{selectedProductData.title}</div>
|
||||
<div className="mt-[6px] text-[13px] text-white/60">{selectedProductData.subtitle}</div>
|
||||
</div>
|
||||
<div className="text-[18px] font-bold text-[#FA6A00]">{selectedProductData.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
|
||||
<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'
|
||||
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'
|
||||
}`}
|
||||
></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
|
||||
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>
|
||||
) : 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 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>
|
||||
)
|
||||
})}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center gap-[12px] rounded-[12px] border border-dashed border-[#FA6A00]/55 bg-[#FA6A00]/8 px-[14px] py-[16px] text-left transition-colors hover:bg-[#FA6A00]/12"
|
||||
onClick={handleOpenAddAddress}
|
||||
>
|
||||
<div className="flex h-[18px] w-[18px] items-center justify-center rounded-full bg-[#FA6A00] text-[14px] leading-none text-white">
|
||||
+
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[14px] font-bold text-white">Add New Address</div>
|
||||
<div className="mt-[4px] text-[12px] text-white/55">
|
||||
Create a shipping address for this redemption
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : isPhysicalPrize ? (
|
||||
<div className="rounded-[12px] bg-[#171313]/88 px-[14px] py-[10px] shadow-[0_10px_30px_rgba(0,0,0,0.25)]">
|
||||
<div className="flex items-center justify-between border-b border-white/10 px-[6px] pb-[16px]">
|
||||
<div className="text-[18px] font-medium text-white">Address Info</div>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-[8px] text-[14px] text-white/85"
|
||||
onClick={() => handleChangeAddressForm('isDefault', !addressForm.isDefault)}
|
||||
>
|
||||
<span
|
||||
className={`flex h-[16px] w-[16px] items-center justify-center rounded-full border ${
|
||||
addressForm.isDefault ? 'border-[#FA6A00]' : 'border-white/35'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`h-[8px] w-[8px] rounded-full ${
|
||||
addressForm.isDefault ? 'bg-[#FA6A00]' : 'bg-transparent'
|
||||
}`}
|
||||
></span>
|
||||
</span>
|
||||
<span>Default Address</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-white/10">
|
||||
<div className="grid grid-cols-[170px_1fr] items-center gap-[10px] px-[6px] py-[16px]">
|
||||
<label className="text-[14px] text-white/92">
|
||||
Name<span className="text-[#FA6A00]">*</span>
|
||||
</label>
|
||||
<input
|
||||
value={addressForm.name}
|
||||
onChange={(event) => handleChangeAddressForm('name', event.target.value)}
|
||||
placeholder="Full Name"
|
||||
className="bg-transparent text-[14px] text-white outline-none placeholder:text-white/35"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-[170px_1fr] items-center gap-[10px] px-[6px] py-[16px]">
|
||||
<label className="text-[14px] text-white/92">
|
||||
Phone Number<span className="text-[#FA6A00]">*</span>
|
||||
</label>
|
||||
<input
|
||||
value={addressForm.phone}
|
||||
onChange={(event) => handleChangeAddressForm('phone', event.target.value)}
|
||||
placeholder="Phone Number"
|
||||
className="bg-transparent text-[14px] text-white outline-none placeholder:text-white/35"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-[170px_1fr] items-center gap-[10px] px-[6px] py-[16px]">
|
||||
<label className="text-[14px] text-white/92">
|
||||
Region<span className="text-[#FA6A00]">*</span>
|
||||
</label>
|
||||
<input
|
||||
value={addressForm.region}
|
||||
onChange={(event) => handleChangeAddressForm('region', event.target.value)}
|
||||
placeholder="State, City, District"
|
||||
className="bg-transparent text-[14px] text-white outline-none placeholder:text-white/35"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-[170px_1fr] items-center gap-[10px] px-[6px] py-[16px]">
|
||||
<label className="text-[14px] text-white/92">
|
||||
Detailed Address<span className="text-[#FA6A00]">*</span>
|
||||
</label>
|
||||
<input
|
||||
value={addressForm.detailedAddress}
|
||||
onChange={(event) => handleChangeAddressForm('detailedAddress', event.target.value)}
|
||||
placeholder="Apt, Suite, Bldg, etc"
|
||||
className="bg-transparent text-[14px] text-white outline-none placeholder:text-white/35"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-[170px_1fr] items-center gap-[10px] px-[6px] py-[16px]">
|
||||
<label className="text-[14px] text-white/92">Postal Code</label>
|
||||
<input
|
||||
value={addressForm.postalCode}
|
||||
onChange={(event) => handleChangeAddressForm('postalCode', event.target.value)}
|
||||
placeholder="Postal Code"
|
||||
className="bg-transparent text-[14px] text-white outline-none placeholder:text-white/35"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={claimModalOpen}
|
||||
title="Confirm Claim"
|
||||
onClose={handleCloseClaimModal}
|
||||
className="max-w-[560px]"
|
||||
bodyClassName="space-y-[18px]"
|
||||
footer={
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="h-[38px] min-w-[120px] rounded-[8px] border border-white/10 bg-white/10 px-[18px] text-[14px] text-white/75 transition-colors hover:bg-white/14"
|
||||
onClick={handleCloseClaimModal}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" className="button-play h-[38px] min-w-[130px]" onClick={handleCloseClaimModal}>
|
||||
Confirm
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<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)]">
|
||||
Once pending points are transferred to your available balance, they can be redeemed or withdrawn.
|
||||
Confirm claim?
|
||||
</div>
|
||||
</Modal>
|
||||
</PageLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,99 +1,300 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
import PageLayout from '@/components/layout'
|
||||
import {Link} from 'react-router-dom'
|
||||
import type {RecordButtonType} from "@/types";
|
||||
import {useState} from "react";
|
||||
import {cn} from "@/lib";
|
||||
import { cn } from '@/lib'
|
||||
import Modal from '@/components/modal'
|
||||
import type { RecordButtonType } from '@/types'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
type OrderRecord = {
|
||||
id: string
|
||||
date: string
|
||||
time: string
|
||||
category: string
|
||||
title: string
|
||||
trackingNumber?: string
|
||||
status: string
|
||||
points: string
|
||||
}
|
||||
|
||||
type PointsRecord = {
|
||||
id: string
|
||||
title: string
|
||||
date: string
|
||||
time: string
|
||||
amount: string
|
||||
tone: 'positive' | 'negative'
|
||||
}
|
||||
|
||||
const orderRecords: 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',
|
||||
},
|
||||
]
|
||||
|
||||
const pointsRecords: 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',
|
||||
},
|
||||
]
|
||||
|
||||
const amountToneClassName: Record<PointsRecord['tone'], string> = {
|
||||
positive: 'bg-[#9BFFC0] text-[#176640]',
|
||||
negative: 'bg-[#FF9BA4] text-[#7B2634]',
|
||||
}
|
||||
|
||||
function getOrderStatusClassName(status: string) {
|
||||
switch (status.toLowerCase()) {
|
||||
case 'issued':
|
||||
return 'bg-[#9BFFC0] text-[#176640]'
|
||||
case 'shipped':
|
||||
return 'bg-[#95F0FF] text-[#116A79]'
|
||||
case 'pending':
|
||||
return 'bg-[#FFF18C] text-[#7F6A0D]'
|
||||
case 'rejected':
|
||||
return 'bg-[#FFB1C0] text-[#7C2941]'
|
||||
default:
|
||||
return 'bg-white/15 text-white/80'
|
||||
}
|
||||
}
|
||||
|
||||
type TabButtonProps = {
|
||||
active: boolean
|
||||
label: string
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
type OrderCardProps = {
|
||||
record: OrderRecord
|
||||
onOpenDetails: (record: OrderRecord) => void
|
||||
}
|
||||
|
||||
function TabButton({ active, label, onClick }: TabButtonProps) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'min-w-[92px] cursor-pointer rounded-[6px] border px-[14px] py-[7px] text-[13px] transition-colors',
|
||||
active
|
||||
? 'border-[#F99A0B] bg-linear-to-r from-[#F96C02] to-[#FE9F00] text-white shadow-[0_0_16px_rgba(249,108,2,0.22)]'
|
||||
: 'border-white/35 bg-white/3 text-[#B8B1AA] hover:bg-white/6',
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function OrderCard({ record, onOpenDetails }: OrderCardProps) {
|
||||
return (
|
||||
<div className="overflow-hidden rounded-[10px] shadow-[0_10px_30px_rgba(0,0,0,0.24)]">
|
||||
<div className="bg-linear-to-r from-[#F96C02] to-[#FE9F00] px-[12px] py-[8px] text-[14px] text-white">
|
||||
{record.date} {record.time} • {record.category}
|
||||
</div>
|
||||
<div className="liquid-glass-bg !rounded-t-none flex items-center justify-between px-[12px] py-[12px]">
|
||||
<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">
|
||||
Tracking Number {record.trackingNumber}
|
||||
</div>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className="mt-[8px] text-[13px] text-[#FA6A00]"
|
||||
onClick={() => onOpenDetails(record)}
|
||||
>
|
||||
Check the details
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 flex-col items-end gap-[10px]">
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-[6px] px-[8px] py-[3px] text-[11px] leading-none',
|
||||
getOrderStatusClassName(record.status),
|
||||
)}
|
||||
>
|
||||
{record.status}
|
||||
</div>
|
||||
<div className="text-[13px] text-white/85">{record.points}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PointsCard({ record }: { record: PointsRecord }) {
|
||||
return (
|
||||
<div className="overflow-hidden rounded-[10px] shadow-[0_10px_30px_rgba(0,0,0,0.24)]">
|
||||
<div className="bg-linear-to-r from-[#F96C02] to-[#FE9F00] px-[12px] py-[8px] text-[15px] text-white">
|
||||
{record.title}
|
||||
</div>
|
||||
<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>
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-[6px] px-[10px] py-[3px] text-[12px] leading-none',
|
||||
amountToneClassName[record.tone],
|
||||
)}
|
||||
>
|
||||
{record.amount}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RecordPage() {
|
||||
const [tab, setTab] = useState<RecordButtonType>('order')
|
||||
const [selectedOrder, setSelectedOrder] = useState<OrderRecord | null>(null)
|
||||
|
||||
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')
|
||||
|
||||
const handleCloseDetails = () => {
|
||||
setSelectedOrder(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<PageLayout contentClassName="min-h-screen">
|
||||
<Link
|
||||
to="/"
|
||||
className={'relative text-[#F56E10] flex h-[40px] w-full items-center justify-center bg-[#08070E]/70'}
|
||||
className="relative flex h-[40px] w-full items-center justify-center bg-[#08070E]/70 text-[#F56E10]"
|
||||
>
|
||||
<div className={'absolute left-[16px]'}> < </div>
|
||||
<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 className="mx-auto w-[60%] pt-[14px] pb-[24px]">
|
||||
<div className="flex gap-[8px]">
|
||||
<TabButton active={tab === 'order'} label="My Orders" onClick={() => setTab('order')} />
|
||||
<TabButton active={tab === 'record'} label="Points Record" onClick={() => setTab('record')} />
|
||||
</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 className="mt-[10px] border-t border-white/20"></div>
|
||||
|
||||
<div className="mt-[12px] flex flex-col gap-[12px]">
|
||||
{tab === 'order'
|
||||
? orderRecords.map((record) => (
|
||||
<OrderCard key={record.id} record={record} onOpenDetails={setSelectedOrder} />
|
||||
))
|
||||
}
|
||||
: pointsRecords.map((record) => <PointsCard key={record.id} record={record} />)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
open={Boolean(selectedOrder)}
|
||||
title="Order Details"
|
||||
onClose={handleCloseDetails}
|
||||
className="max-w-[380px]"
|
||||
bodyClassName="pt-[0px]"
|
||||
footer={
|
||||
<button type="button" className="button-play h-[36px] min-w-[94px]" onClick={handleCloseDetails}>
|
||||
Close
|
||||
</button>
|
||||
}
|
||||
>
|
||||
{selectedOrder ? (
|
||||
<div className="rounded-[8px] bg-[#1C1818]/78 px-[12px] py-[6px]">
|
||||
{[
|
||||
{ label: 'Order Number', value: `ORD${selectedOrder.date.replaceAll('-', '')}${selectedOrder.time.replace(':', '')}001` },
|
||||
{ label: 'Order Time', value: `${selectedOrder.date} ${selectedOrder.time}` },
|
||||
{ label: 'Order Type', value: selectedOrder.category },
|
||||
{ label: 'Item Name', value: selectedOrder.title },
|
||||
{ label: 'Points', value: selectedOrder.points.replace('-', '') },
|
||||
{ label: 'Status', value: selectedOrder.status },
|
||||
].map((item) => (
|
||||
<div key={item.label} className="border-b border-white/8 py-[10px] last:border-b-0">
|
||||
<div className="text-[13px] text-white/48">{item.label}</div>
|
||||
<div className="mt-[4px] text-[14px] text-white">{item.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</Modal>
|
||||
</PageLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default RecordPage;
|
||||
export default RecordPage
|
||||
|
||||
Reference in New Issue
Block a user