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

18
src/App.tsx Normal file
View File

@@ -0,0 +1,18 @@
import { BrowserRouter, Route, Routes } from 'react-router-dom'
import HomePage from './views/home'
import RecordPage from './views/record'
import AccountPage from './views/account'
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/record" element={<RecordPage />} />
<Route path="/account" element={<AccountPage />} />
</Routes>
</BrowserRouter>
)
}
export default App

9
src/assets/account.svg Normal file
View File

@@ -0,0 +1,9 @@
<svg width="34" height="34" viewBox="0 0 34 34" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect width="34" height="34" fill="url(#pattern0_18_206)"/>
<defs>
<pattern id="pattern0_18_206" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_18_206" transform="scale(0.0294118)"/>
</pattern>
<image id="image0_18_206" width="34" height="34" preserveAspectRatio="none" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACIAAAAiCAMAAAANmfvwAAAAAXNSR0IB2cksfwAAAblQTFRF//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//XrKAqIygAAAJN0Uk5TADWgz+Tq3siCGCy6//eUCVngHlvEuNPsHyL5hjmi29koU284EfILsHrKWvUKQocC6LwZ/qoE664X/cUboftgfZOlSE/Wn1iQRumyXTAdP27UCMmoDLEgIcOpxirS7b82/FJyKaMjN5KcmZuWRxDvYsJli4yJiIoySkCEfmjVrVZDOjvnByfLzHF1jWoNAfCDTOLx5sxRPAAAAg1JREFUeJxjZEAGjDDwHVkQic3FyIhQ+hGbEoE/SMpZGd9hKhH+CaY4GBm/gWhuxhfoSiQ/Awk+xn/PGRhkGN8D2UKP0JWwswINYnwA4Si+BhJi91CVKL8EssXvILtLkvEWihL1p0ALbiCcq/mYgUHuGooSuXcM8oxXEEp07zMwKF1CVqLPeIdB9TySp40YbzJonEFWYsp4jUH7JJISC8bLDJyvUSxi4WDQO4akxPoiA4PBERQlYt8YDG+/QCixO8fAYHwQRYkD0F7T/Qgl4l8ZGMz2oSgBGcxgvpcBSYfFHgYUJQyux4GE1S4Ix/0okLDZgaaE3+owyAmQ5ALSL/iYAU0JQyAjxAj1m2BSYS0DhpIQxu1IntZ9/ISBIWyL2CvflXAlDj8vMyADnxUMkZtBDD+okmjGDRDbdv7zOn2XDSSzLH4tskW6xmtA9n+xOgqO/zCeHw8050p8QVLicwBERjDORrGKIQ2aHYDMXMZ5QDJ5IgN2AFJSMAdoifEshGAhI+N/EM3E+J9paQxQScUUkEntCBVVk9BNMQOmv/xWJDGxb2hKnE4xMAQsQRar2YscRIWMDHV9DAzFjSj6JDIYGM4Zgz37/+ksRobmTgYtvxocvgFb1Ap0p+9yTHFPG3DiNu0tYexsZmCoK0NXofYMSXU30BmKl9GV9DQgKekt7WEsxHTABMYpEEbuVQAqHIIU/MvtggAAAABJRU5ErkJggg=="/>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

9
src/assets/record.svg Normal file
View File

@@ -0,0 +1,9 @@
<svg width="32" height="34" viewBox="0 0 32 34" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect width="32" height="34" fill="url(#pattern0_18_197)"/>
<defs>
<pattern id="pattern0_18_197" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_18_197" transform="scale(0.03125 0.0294118)"/>
</pattern>
<image id="image0_18_197" width="32" height="34" preserveAspectRatio="none" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAiCAMAAAAJbCvNAAAAAXNSR0IB2cksfwAAAVZQTFRF//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//XrBUhsGwAAAHJ0Uk5TPej/4SUA2Mfv5+Xccunb5LTQ2r/XNEhGRUcrdGwC+wGxnuJxkmvNKd0fOjk7NW39wAzBz69eBGINwgUHpAlzgX97NtHqBmp29Wdp9Atd1WNcCArUZFrZ02CrFfpmtyeaDsoqLC+mMdbueM5bmZaXfoOYXJMCGQAAAb5JREFUeJxjZGBkZMAJGIGA7Q9ueQYG1n+MHL/wKWD/w8j1g4EThyWMXxl4vjPyf2bg+4pdAc9HBoHPIAWCn7Ar4H9HVQUCjMjh8Z+J8TWqArHn6NqlXiIrkHiKYb7McwbGP3AFUoyP0BXIP2H4959B4THMDXJogcX4gIFB8Q4Dx298vlBifMTzHqZAhZEBagYj4w0UoyAKNBkvI8T0GK8ASQVeoM8vQBUYnkEx2/QcgzHjSRCL1RjqC6mTyAqEP5idhiYTK6gVFoiABIbiYWPu/UCW0z6EG1CBw0GQfpfvXLsZ3LAp0HnwEUi672Fw3cHgiUWB+3eQ+S77GTy2YrVCR3YLkPTdBpE3wlAA1++9CUgL2aIr8GVcDySDNjIErAXSIY/Ooilw514NMT94FZBm0b6C7oaQlUDCYzdcP7ojQ1cwMEQyrkTohyiIXgFTELmEgSF2GVQ/11IGqIJ4xh1Alifji53//zEwJP/8twzI5VG+AtbD6LIP7gAnpavHYezUn0shDMaMWQg/sv5j+w5hwfQDFWTORCiIXabx8QmIkfFtKUyMkTVtpwc4phn/MT/awiAa9of1z61/B+CaALF6szdHXa+5AAAAAElFTkSuQmCC"/>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,23 @@
import type { ButtonHTMLAttributes, PropsWithChildren } from "react";
import { twMerge } from "tailwind-merge";
type ButtonProps = PropsWithChildren<ButtonHTMLAttributes<HTMLButtonElement>>;
export function Button({
children,
className = "",
type = "button",
...props
}: ButtonProps) {
return (
<button
type={type}
className={twMerge("button-play", className)}
{...props}
>
{children}
</button>
);
}
export default Button;

View File

@@ -0,0 +1,3 @@
export function Card(){
return (<div className={'w-full h-[]'}>card</div>)
}

View File

@@ -0,0 +1,16 @@
import type { ReactNode } from 'react'
type PageLayoutProps = {
children: ReactNode
contentClassName?: string
}
function PageLayout({ children, contentClassName = 'w-[90%] lg:w-[60%] h-full mx-auto flex flex-col' }: PageLayoutProps) {
return (
<main className="text-white h-screen w-screen overflow-x-hidden bg-[url('/home-figma-frame.png')] bg-cover bg-center bg-no-repeat">
<div className={contentClassName}>{children}</div>
</main>
)
}
export default PageLayout

View File

@@ -0,0 +1,79 @@
import type { ReactNode } from 'react'
import { cn } from '@/lib'
type ModalProps = {
open: boolean
title?: ReactNode
children: ReactNode
footer?: ReactNode
onClose?: () => void
closeOnOverlayClick?: boolean
className?: string
bodyClassName?: string
}
export function Modal({
open,
title,
children,
footer,
onClose,
closeOnOverlayClick = true,
className,
bodyClassName,
}: ModalProps) {
if (!open) {
return null
}
const handleOverlayClick = () => {
if (closeOnOverlayClick) {
onClose?.()
}
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-[16px]">
<button
type="button"
aria-label="Close modal"
className="absolute inset-0 bg-[#050409]/65 backdrop-blur-[6px]"
onClick={handleOverlayClick}
/>
<div
role="dialog"
aria-modal="true"
className={cn(
'relative z-10 w-full max-w-[560px] overflow-hidden rounded-[14px] shadow-[0_18px_60px_rgba(0,0,0,0.45)]',
className,
)}
>
<div className="flex min-h-[58px] items-center justify-between bg-linear-to-r from-[#F96C02] to-[#FE9F00] px-[18px] py-[14px] text-white">
<div className="text-[18px] leading-[1.2] font-bold">{title}</div>
{onClose ? (
<button
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"
onClick={onClose}
>
×
</button>
) : null}
</div>
<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]">
{footer}
</div>
) : null}
</div>
</div>
</div>
)
}
export default Modal

View File

@@ -0,0 +1,59 @@
import type { ReactNode } from 'react'
export type TableColumn<T extends Record<string, string>> = {
label: string
key: keyof T
render?: (value: T[keyof T], record: T, index: number) => ReactNode
}
type BorderlessTableProps<T extends Record<string, string>> = {
columns: TableColumn<T>[]
dataSource: T[]
}
function BorderlessTable<T extends Record<string, string>>({
columns,
dataSource,
}: BorderlessTableProps<T>) {
return (
<div className="overflow-x-auto rounded-[12px] bg-[#08070E]/55 p-[10px]">
<table className="w-full min-w-[960px] table-fixed border-collapse text-left text-[14px] text-white">
<thead>
<tr className="bg-linear-to-r from-[#F96C02] to-[#FE9F00] text-white">
{columns.map((column) => (
<th
key={String(column.key)}
className="px-[16px] py-[14px] text-[13px] font-bold first:rounded-l-[10px] last:rounded-r-[10px]"
>
{column.label}
</th>
))}
</tr>
</thead>
<tbody>
{dataSource.map((record, index) => (
<tr
key={`${record.name}-${index}`}
className="bg-white/4 transition-colors hover:bg-white/8"
>
{columns.map((column) => {
const value = record[column.key]
return (
<td
key={String(column.key)}
className="px-[16px] py-[16px] align-top text-[13px] text-white/90"
>
{column.render ? column.render(value, record, index) : value}
</td>
)
})}
</tr>
))}
</tbody>
</table>
</div>
)
}
export default BorderlessTable

95
src/index.css Normal file
View File

@@ -0,0 +1,95 @@
@import "tailwindcss";
.button-play {
align-items: center;
appearance: none;
background-image: radial-gradient(100% 100% at 100% 0, #ffbe5c 0, #fe9c00 45%, #fa6d02 100%);
border: 0;
border-radius: 6px;
box-shadow:
rgba(254, 156, 0, .45) 0 0 18px,
rgba(123, 54, 16, .35) 0 2px 4px,
rgba(123, 54, 16, .28) 0 7px 13px -3px,
#c2460e 0 -3px 0 inset;
box-sizing: border-box;
color: #fff;
cursor: pointer;
display: inline-flex;
font-family: "JetBrains Mono",monospace;
justify-content: center;
line-height: 1;
list-style: none;
overflow: hidden;
padding-left: 16px;
padding-right: 16px;
position: relative;
text-align: left;
text-decoration: none;
transition: box-shadow .15s,transform .15s;
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
white-space: nowrap;
will-change: box-shadow,transform;
font-size: 14px;
font-weight: bold;
}
.button-play:focus {
box-shadow:
#c2460e 0 0 0 1.5px inset,
rgba(254, 156, 0, .55) 0 0 22px,
rgba(123, 54, 16, .35) 0 2px 4px,
rgba(123, 54, 16, .28) 0 7px 13px -3px,
#c2460e 0 -3px 0 inset;
}
.button-play:hover {
box-shadow:
rgba(254, 156, 0, .6) 0 0 24px,
rgba(123, 54, 16, .35) 0 4px 8px,
rgba(123, 54, 16, .28) 0 7px 13px -3px,
#c2460e 0 -3px 0 inset;
transform: translateY(-2px);
}
.button-play:active {
box-shadow: #c2460e 0 3px 7px inset, rgba(254, 156, 0, .28) 0 0 12px;
transform: translateY(2px);
}
.liquid-glass-bg {
position: relative;
overflow: hidden;
background: rgba(255, 255, 255, 0.25);
backdrop-filter: blur(2px) saturate(180%) contrast(100%) brightness(100%);
-webkit-backdrop-filter: blur(2px) saturate(180%) contrast(100%) brightness(100%);
border-radius: 10px;
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-sizing: border-box;
font-weight: normal;
}
.progress-bar {
width: 100%;
height: 10px;
margin: 10px 0 8px;
overflow: hidden;
border-radius: 999px;
background: linear-gradient(180deg, rgba(18, 14, 10, 0.92) 0%, rgba(34, 22, 14, 0.96) 100%);
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.45);
}
.progress-bar__fill {
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, #fe9c00 0%, #fa6d02 100%);
box-shadow:
inset 0 0 0 1px rgba(255, 220, 160, 0.35),
0 0 8px rgba(254, 156, 0, 0.55),
0 0 16px rgba(250, 109, 2, 0.4),
0 0 24px rgba(250, 109, 2, 0.22);
}

5
src/lib/index.ts Normal file
View File

@@ -0,0 +1,5 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

10
src/main.tsx Normal file
View File

@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

1
src/types/index.ts Normal file
View File

@@ -0,0 +1 @@
export type RecordButtonType = 'order' | 'record'

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;