feat: 新增首页和图片资源
This commit is contained in:
66
src/features/game/components/desktop/desktop-animal.tsx
Normal file
66
src/features/game/components/desktop/desktop-animal.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { SmartImage } from '@/components/smart-image'
|
||||
|
||||
const cx = (...classes: Array<string | false | null | undefined>) =>
|
||||
classes.filter(Boolean).join(' ')
|
||||
|
||||
const animalModules = import.meta.glob('../../../../assets/animal/*.webp', {
|
||||
eager: true,
|
||||
import: 'default',
|
||||
}) as Record<string, string>
|
||||
|
||||
const animalImageList = Object.entries(animalModules)
|
||||
.map(([path, url]) => {
|
||||
const match = path.match(/\/(\d+)\.webp$/)
|
||||
|
||||
return {
|
||||
id: Number(match?.[1] ?? 0),
|
||||
url,
|
||||
}
|
||||
})
|
||||
.filter((item) => item.id > 0)
|
||||
.sort((left, right) => left.id - right.id)
|
||||
|
||||
interface DesktopAnimalProps {
|
||||
activeId?: number | null
|
||||
className?: string
|
||||
itemClassName?: string
|
||||
imageClassName?: string
|
||||
onSelect?: (animalId: number) => void
|
||||
}
|
||||
|
||||
export function DesktopAnimal({
|
||||
activeId,
|
||||
className,
|
||||
itemClassName,
|
||||
imageClassName,
|
||||
onSelect,
|
||||
}: DesktopAnimalProps) {
|
||||
return (
|
||||
<section className={cx('grid grid-cols-6', className)}>
|
||||
{animalImageList.map((item) => {
|
||||
const isActive = item.id === activeId
|
||||
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
onClick={() => onSelect?.(item.id)}
|
||||
className={cx(
|
||||
'flex flex-col items-center transition',
|
||||
'cursor-pointer',
|
||||
isActive &&
|
||||
'border-[rgba(255,151,15,0.95)] shadow-[inset_0_0_16px_rgba(255,151,15,0.55)]',
|
||||
itemClassName,
|
||||
)}
|
||||
>
|
||||
<SmartImage
|
||||
src={item.url}
|
||||
alt={`animal-${item.id}`}
|
||||
className={cx('h-[110px] w-[220px]', imageClassName)}
|
||||
/>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
97
src/features/game/components/desktop/desktop-header.tsx
Normal file
97
src/features/game/components/desktop/desktop-header.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { CircleAlert, Mail, Plus, Volume2 } from 'lucide-react'
|
||||
import avatar from '@/assets/system/avatar.webp'
|
||||
import diamond from '@/assets/system/diamond.webp'
|
||||
import logo from '@/assets/system/logo.webp'
|
||||
import wifi from '@/assets/system/wifi.webp'
|
||||
import { SmartImage } from '@/components/smart-image.tsx'
|
||||
|
||||
export function DesktopHeader() {
|
||||
return (
|
||||
<header className="sticky top-0 z-30 border-b border-white/8 bg-slate-950/70 backdrop-blur-xl">
|
||||
<div className="h-[70px] w-full flex items-center px-[12px]">
|
||||
<div className="w-[400px] flex justify-center items-center h-full border-r border-[rgba(128,223,231,0.65)]">
|
||||
<SmartImage
|
||||
src={logo}
|
||||
alt="logo"
|
||||
priority
|
||||
className="w-[320px] h-[40px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-[148px] flex justify-center items-center gap-[10px] h-full px-[20px] border-r border-[rgba(128,223,231,0.65)]">
|
||||
<SmartImage
|
||||
src={wifi}
|
||||
alt="wifi"
|
||||
priority
|
||||
className="w-[28px] h-[20px]"
|
||||
/>
|
||||
<div className={'text-[#74FF69] text-[20px]'}>
|
||||
24 <span className={'text-[16px]'}>ms</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-[175px] flex flex-col justify-center items-center gap-[5px] h-full px-[20px] border-r border-[rgba(128,223,231,0.65)]">
|
||||
<div>System Time</div>
|
||||
<div>20:05:12 GMT+08</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex items-center justify-around gap-[10px] h-full px-[20px] text-[#D5FBFF] border-r border-[rgba(128,223,231,0.65)]">
|
||||
<div className={'flex gap-[10px] common-neon-inset'}>
|
||||
<CircleAlert color={'#57B8BF'} />
|
||||
<div>Rules & Ddds</div>
|
||||
</div>
|
||||
|
||||
<div className={'flex gap-[10px] common-neon-inset'}>
|
||||
<Mail color={'#57B8BF'} />
|
||||
<div>Pesan</div>
|
||||
</div>
|
||||
|
||||
<div className={'flex gap-[10px] common-neon-inset'}>
|
||||
<Volume2 color={'#57B8BF'} />
|
||||
<div>BGM</div>
|
||||
</div>
|
||||
|
||||
<div className={'flex gap-[10px] common-neon-inset'}>
|
||||
<CircleAlert color={'#57B8BF'} />
|
||||
<div>ID</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex items-center h-full text-[#D5FBFF]">
|
||||
<div className={'relative flex items-center justify-center'}>
|
||||
<SmartImage
|
||||
src={avatar}
|
||||
alt="avatar"
|
||||
priority
|
||||
className="absolute left-[20px] top-0 z-20 w-[50px] h-[50px]"
|
||||
/>
|
||||
<div
|
||||
className={
|
||||
'common-neon-inset !py-[20px] flex items-center justify-center box-border w-[175px] h-[36px]'
|
||||
}
|
||||
>
|
||||
Biomond Balance
|
||||
</div>
|
||||
</div>
|
||||
<div className={'relative flex items-center justify-center h-full'}>
|
||||
<SmartImage
|
||||
src={diamond}
|
||||
alt="diamond"
|
||||
priority
|
||||
className="absolute left-[30px] top-0 z-20 w-[50px] h-[50px]"
|
||||
/>
|
||||
<div
|
||||
className={
|
||||
'common-neon-inset !py-[20px] flex items-center justify-end gap-[10px] w-[175px] h-[36px]'
|
||||
}
|
||||
>
|
||||
<div>5994469974</div>
|
||||
<div className={'bg-[#2D4559] rounded-xs cursor-pointer p-[5px]'}>
|
||||
<Plus size={16} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
12
src/features/game/components/desktop/desktop-title.tsx
Normal file
12
src/features/game/components/desktop/desktop-title.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Megaphone } from 'lucide-react'
|
||||
export function DesktopTitle() {
|
||||
return (
|
||||
<section className="common-neon-inset flex items-center text-[#FF970F] flex gap-[10px] !px-[20px] h-[50px]">
|
||||
<Megaphone color={'#57B8BF'} />
|
||||
<div>
|
||||
Selamat kepada pemain Wu Yanzu yang telah memenangkan hadiah utama
|
||||
sebesar 5.000 yuan sebanyak lima kali berturut-turut!🎉🎉🎉
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
2
src/features/game/components/index.ts
Normal file
2
src/features/game/components/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { DesktopHeader } from '@/features/game/components/desktop/desktop-header'
|
||||
export { GameAnnouncementModal } from '@/features/game/components/shared/game-announcement-modal'
|
||||
115
src/features/game/components/shared/game-announcement-modal.tsx
Normal file
115
src/features/game/components/shared/game-announcement-modal.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
const cx = (...classes: Array<string | false | null | undefined>) =>
|
||||
classes.filter(Boolean).join(' ')
|
||||
|
||||
type GameTone = 'neutral' | 'brand' | 'success' | 'warning' | 'danger'
|
||||
|
||||
interface GameOverlayAction {
|
||||
label: string
|
||||
onClick: () => void
|
||||
tone?: GameTone
|
||||
}
|
||||
|
||||
interface GameAnnouncementModalProps {
|
||||
open: boolean
|
||||
title: string
|
||||
description?: ReactNode
|
||||
eyebrow?: string
|
||||
tone?: GameTone
|
||||
primaryAction?: GameOverlayAction
|
||||
secondaryAction?: GameOverlayAction
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
const toneClasses: Record<GameTone, string> = {
|
||||
neutral: 'border-white/10 bg-slate-950/92',
|
||||
brand: 'border-cyan-300/25 bg-slate-950/94',
|
||||
success: 'border-emerald-300/25 bg-slate-950/94',
|
||||
warning: 'border-amber-300/25 bg-slate-950/94',
|
||||
danger: 'border-rose-300/25 bg-slate-950/94',
|
||||
}
|
||||
|
||||
const actionToneClasses: Record<GameTone, string> = {
|
||||
neutral: 'border-white/10 bg-white/[0.06] text-white hover:bg-white/[0.1]',
|
||||
brand: 'border-cyan-300/25 bg-cyan-300/14 text-cyan-50 hover:bg-cyan-300/22',
|
||||
success:
|
||||
'border-emerald-300/25 bg-emerald-300/14 text-emerald-50 hover:bg-emerald-300/22',
|
||||
warning:
|
||||
'border-amber-300/25 bg-amber-300/14 text-amber-50 hover:bg-amber-300/22',
|
||||
danger: 'border-rose-300/25 bg-rose-300/14 text-rose-50 hover:bg-rose-300/22',
|
||||
}
|
||||
|
||||
function ModalAction({ label, onClick, tone = 'brand' }: GameOverlayAction) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={cx(
|
||||
'inline-flex min-h-12 items-center justify-center rounded-full border px-5 text-sm font-semibold tracking-[0.18em] uppercase transition duration-200',
|
||||
actionToneClasses[tone],
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export function GameAnnouncementModal({
|
||||
open,
|
||||
title,
|
||||
description,
|
||||
eyebrow = '',
|
||||
tone = 'brand',
|
||||
primaryAction,
|
||||
secondaryAction,
|
||||
children,
|
||||
}: GameAnnouncementModalProps) {
|
||||
if (!open) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/82 px-4 py-8 backdrop-blur-md">
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="game-announcement-title"
|
||||
className={cx(
|
||||
'w-full max-w-xl rounded-[32px] border p-6 shadow-[0_40px_120px_-40px_rgba(15,23,42,0.95)] sm:p-7',
|
||||
toneClasses[tone],
|
||||
)}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<span className="inline-flex rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[0.68rem] font-semibold tracking-[0.24em] text-slate-300 uppercase">
|
||||
{eyebrow}
|
||||
</span>
|
||||
<h2
|
||||
id="game-announcement-title"
|
||||
className="text-2xl font-semibold tracking-tight text-white sm:text-[2rem]"
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
{description ? (
|
||||
<div className="text-sm leading-7 text-slate-300">
|
||||
{description}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{children ? (
|
||||
<div className="rounded-[24px] border border-white/8 bg-white/[0.03] p-4">
|
||||
{children}
|
||||
</div>
|
||||
) : null}
|
||||
{secondaryAction || primaryAction ? (
|
||||
<div className="flex flex-wrap gap-3 pt-2">
|
||||
{secondaryAction ? <ModalAction {...secondaryAction} /> : null}
|
||||
{primaryAction ? <ModalAction {...primaryAction} /> : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user