init project
This commit is contained in:
18
src/App.tsx
Normal file
18
src/App.tsx
Normal 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
9
src/assets/account.svg
Normal 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 |
BIN
src/assets/home-figma-frame.png
Normal file
BIN
src/assets/home-figma-frame.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
9
src/assets/record.svg
Normal file
9
src/assets/record.svg
Normal 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 |
23
src/components/button/index.tsx
Normal file
23
src/components/button/index.tsx
Normal 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;
|
||||
3
src/components/card/index.tsx
Normal file
3
src/components/card/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export function Card(){
|
||||
return (<div className={'w-full h-[]'}>card</div>)
|
||||
}
|
||||
16
src/components/layout/index.tsx
Normal file
16
src/components/layout/index.tsx
Normal 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
|
||||
79
src/components/modal/index.tsx
Normal file
79
src/components/modal/index.tsx
Normal 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
|
||||
59
src/components/table/index.tsx
Normal file
59
src/components/table/index.tsx
Normal 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
95
src/index.css
Normal 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
5
src/lib/index.ts
Normal 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
10
src/main.tsx
Normal 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
1
src/types/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type RecordButtonType = 'order' | 'record'
|
||||
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