Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
E
el3ab-Player
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Administrator
el3ab-Player
Commits
4d37e0d3
Commit
4d37e0d3
authored
May 26, 2026
by
Mahmoud Aglan
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
go
parent
6a8bf93c
Changes
24
Show whitespace changes
Inline
Side-by-side
Showing
24 changed files
with
2697 additions
and
1027 deletions
+2697
-1027
screenshot-all.ts
scripts/screenshot-all.ts
+82
-0
AppShell.tsx
src/components/layout/AppShell.tsx
+10
-6
BottomNav.tsx
src/components/layout/BottomNav.tsx
+107
-40
DecorativeBackground.tsx
src/components/layout/DecorativeBackground.tsx
+82
-0
Header.tsx
src/components/layout/Header.tsx
+83
-39
PageTransition.tsx
src/components/layout/PageTransition.tsx
+4
-4
index.ts
src/components/layout/index.ts
+1
-0
GamePanel.tsx
src/components/ui/GamePanel.tsx
+64
-0
GameProgressBar.tsx
src/components/ui/GameProgressBar.tsx
+49
-0
RibbonHeader.tsx
src/components/ui/RibbonHeader.tsx
+68
-0
ShieldBadge.tsx
src/components/ui/ShieldBadge.tsx
+83
-0
index.ts
src/components/ui/index.ts
+4
-0
index.css
src/index.css
+198
-30
BotSelectPage.tsx
src/pages/BotSelectPage.tsx
+82
-31
FriendsPage.tsx
src/pages/FriendsPage.tsx
+246
-172
HomePage.tsx
src/pages/HomePage.tsx
+202
-92
LeaderboardPage.tsx
src/pages/LeaderboardPage.tsx
+317
-122
MatchmakingPage.tsx
src/pages/MatchmakingPage.tsx
+112
-26
PlayPage.tsx
src/pages/PlayPage.tsx
+227
-84
ProfilePage.tsx
src/pages/ProfilePage.tsx
+289
-110
SettingsPage.tsx
src/pages/SettingsPage.tsx
+59
-26
ShopPage.tsx
src/pages/ShopPage.tsx
+182
-145
TournamentsPage.tsx
src/pages/TournamentsPage.tsx
+142
-100
.last-run.json
test-results/.last-run.json
+4
-0
No files found.
scripts/screenshot-all.ts
0 → 100644
View file @
4d37e0d3
import
{
chromium
}
from
'playwright'
import
{
mkdirSync
}
from
'fs'
const
PAGES
=
[
{
name
:
'login'
,
url
:
'/login'
},
{
name
:
'register'
,
url
:
'/register'
},
{
name
:
'home'
,
url
:
'/'
},
{
name
:
'play'
,
url
:
'/play'
},
{
name
:
'profile'
,
url
:
'/profile'
},
{
name
:
'friends'
,
url
:
'/friends'
},
{
name
:
'tournaments'
,
url
:
'/tournaments'
},
{
name
:
'leaderboard'
,
url
:
'/leaderboard'
},
{
name
:
'shop'
,
url
:
'/shop'
},
{
name
:
'notifications'
,
url
:
'/notifications'
},
{
name
:
'settings'
,
url
:
'/settings'
},
{
name
:
'bot-select'
,
url
:
'/bot-select'
},
{
name
:
'matchmaking'
,
url
:
'/matchmaking'
},
]
const
BASE
=
'https://el3ab-player.caprover.al-arcade.com'
async
function
main
()
{
mkdirSync
(
'screenshots'
,
{
recursive
:
true
})
const
browser
=
await
chromium
.
launch
()
// Mobile viewport
const
mobile
=
await
browser
.
newContext
({
viewport
:
{
width
:
390
,
height
:
844
},
deviceScaleFactor
:
2
,
locale
:
'ar'
,
})
// Desktop viewport
const
desktop
=
await
browser
.
newContext
({
viewport
:
{
width
:
1440
,
height
:
900
},
deviceScaleFactor
:
1
,
locale
:
'ar'
,
})
// Login first to get session for protected pages
const
loginPage
=
await
mobile
.
newPage
()
await
loginPage
.
goto
(
`
${
BASE
}
/login`
,
{
waitUntil
:
'networkidle'
,
timeout
:
20000
})
await
loginPage
.
waitForTimeout
(
1000
)
await
loginPage
.
fill
(
'input[type="email"]'
,
'testplayer1@el3ab.com'
)
await
loginPage
.
fill
(
'input[type="password"]'
,
'test123456'
)
await
loginPage
.
click
(
'button[type="submit"]'
)
await
loginPage
.
waitForTimeout
(
3000
)
await
loginPage
.
close
()
// Also login on desktop context
const
loginDesktop
=
await
desktop
.
newPage
()
await
loginDesktop
.
goto
(
`
${
BASE
}
/login`
,
{
waitUntil
:
'networkidle'
,
timeout
:
20000
})
await
loginDesktop
.
waitForTimeout
(
1000
)
await
loginDesktop
.
fill
(
'input[type="email"]'
,
'testplayer1@el3ab.com'
)
await
loginDesktop
.
fill
(
'input[type="password"]'
,
'test123456'
)
await
loginDesktop
.
click
(
'button[type="submit"]'
)
await
loginDesktop
.
waitForTimeout
(
3000
)
await
loginDesktop
.
close
()
for
(
const
{
name
,
url
}
of
PAGES
)
{
// Mobile screenshot
const
mPage
=
await
mobile
.
newPage
()
await
mPage
.
goto
(
`
${
BASE
}${
url
}
`
,
{
waitUntil
:
'networkidle'
,
timeout
:
15000
})
await
mPage
.
waitForTimeout
(
1500
)
await
mPage
.
screenshot
({
path
:
`screenshots/
${
name
}
-mobile.png`
})
console
.
log
(
`captured
${
name
}
-mobile`
)
await
mPage
.
close
()
// Desktop screenshot
const
dPage
=
await
desktop
.
newPage
()
await
dPage
.
goto
(
`
${
BASE
}${
url
}
`
,
{
waitUntil
:
'networkidle'
,
timeout
:
15000
})
await
dPage
.
waitForTimeout
(
1500
)
await
dPage
.
screenshot
({
path
:
`screenshots/
${
name
}
-desktop.png`
})
console
.
log
(
`captured
${
name
}
-desktop`
)
await
dPage
.
close
()
}
await
browser
.
close
()
console
.
log
(
'Done! All screenshots captured.'
)
}
main
()
src/components/layout/AppShell.tsx
View file @
4d37e0d3
import
{
Outlet
}
from
'react-router-dom'
import
{
Outlet
}
from
'react-router-dom'
import
{
Header
}
from
'./Header'
import
{
Header
}
from
'./Header'
import
{
BottomNav
}
from
'./BottomNav'
import
{
BottomNav
}
from
'./BottomNav'
import
{
DecorativeBackground
}
from
'./DecorativeBackground'
import
{
ToastContainer
}
from
'../ui/ToastContainer'
import
{
ToastContainer
}
from
'../ui/ToastContainer'
import
{
usePresence
}
from
'../../hooks/usePresence'
import
{
usePresence
}
from
'../../hooks/usePresence'
import
{
useNotifications
}
from
'../../hooks/useNotifications'
import
{
useNotifications
}
from
'../../hooks/useNotifications'
...
@@ -10,12 +11,15 @@ export function AppShell() {
...
@@ -10,12 +11,15 @@ export function AppShell() {
useNotifications
()
useNotifications
()
return
(
return
(
<
div
className=
"flex flex-col min-h-dvh bg-background"
>
<
div
className=
"relative flex flex-col min-h-dvh overflow-hidden"
>
<
DecorativeBackground
/>
<
div
className=
"relative z-10 flex flex-col min-h-dvh"
>
<
Header
/>
<
Header
/>
<
main
className=
"flex-1 pb-28 overflow-y-auto"
>
<
main
className=
"flex-1 pb-28 overflow-y-auto"
>
<
Outlet
/>
<
Outlet
/>
</
main
>
</
main
>
<
BottomNav
/>
<
BottomNav
/>
</
div
>
<
ToastContainer
/>
<
ToastContainer
/>
</
div
>
</
div
>
)
)
...
...
src/components/layout/BottomNav.tsx
View file @
4d37e0d3
...
@@ -10,46 +10,107 @@ const NAV_ITEMS = [
...
@@ -10,46 +10,107 @@ const NAV_ITEMS = [
{
path
:
'/profile'
,
icon
:
User
,
label
:
'حسابي'
},
{
path
:
'/profile'
,
icon
:
User
,
label
:
'حسابي'
},
]
]
const
CENTER_INDEX
=
2
export
function
BottomNav
()
{
export
function
BottomNav
()
{
const
location
=
useLocation
()
const
location
=
useLocation
()
const
navigate
=
useNavigate
()
const
navigate
=
useNavigate
()
return
(
return
(
<
nav
className=
"fixed bottom-0 left-0 right-0 z-50 px-4 pb-3 pt-1
md:px-6
"
>
<
nav
className=
"fixed bottom-0 left-0 right-0 z-50 px-4 pb-3 pt-1"
>
<
div
className=
"app-container !p-0"
>
<
div
className=
"app-container !p-0"
>
<
div
className=
"flex items-center justify-around px-2 py-2 bg-surface-2 border-3 border-border rounded-2xl shadow-2xl shadow-black/50"
>
{
/* Decorative corner accents */
}
{
NAV_ITEMS
.
map
((
item
)
=>
{
<
div
className=
"relative"
>
{
/* Left L-accent */
}
<
div
className=
"absolute -top-1 left-1 w-3 h-3 border-t-2 border-l-2 border-gold/40 rounded-tl-sm pointer-events-none"
/>
{
/* Right L-accent */
}
<
div
className=
"absolute -top-1 right-1 w-3 h-3 border-t-2 border-r-2 border-gold/40 rounded-tr-sm pointer-events-none"
/>
{
/* Main bar */
}
<
div
className=
"game-panel border-4 border-border bg-surface-2 shadow-[0_8px_32px_rgba(0,0,0,0.6),0_2px_8px_rgba(0,0,0,0.4)]"
>
<
div
className=
"flex items-center justify-around px-2 py-2 relative"
>
{
NAV_ITEMS
.
map
((
item
,
index
)
=>
{
const
isActive
=
location
.
pathname
===
item
.
path
const
isActive
=
location
.
pathname
===
item
.
path
const
isCenter
=
index
===
CENTER_INDEX
const
Icon
=
item
.
icon
const
Icon
=
item
.
icon
return
(
return
(
<
motion
.
button
<
motion
.
button
key=
{
item
.
path
}
key=
{
item
.
path
}
onClick=
{
()
=>
navigate
(
item
.
path
)
}
onClick=
{
()
=>
navigate
(
item
.
path
)
}
className=
{
`flex flex-col items-center gap-0.5 px-3 py-2 rounded-xl relative ${
className=
"relative flex flex-col items-center gap-0.5 px-2 py-1"
isActive ? 'bg-gold/15' : ''
}`
}
whileTap=
{
{
scale
:
0.8
}
}
whileTap=
{
{
scale
:
0.8
}
}
whileHover=
{
{
scale
:
1.05
}
}
>
>
{
/* Slot background */
}
<
div
className=
{
`relative flex items-center justify-center rounded-xl transition-colors duration-200 ${
isCenter ? 'w-12 h-12' : 'w-10 h-10'
} ${
isActive
? 'bg-gold/15'
: 'bg-surface-3/60'
} ${
isCenter ? 'border-2 border-gold/20' : 'border border-border/50'
}`
}
>
{
/* Hex outline for center item */
}
{
isCenter
&&
(
<
div
className=
"absolute inset-0 border border-gold/15"
style=
{
{
clipPath
:
'polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%)'
,
}
}
/>
)
}
<
motion
.
div
<
motion
.
div
animate=
{
isActive
?
{
y
:
-
2
}
:
{
y
:
0
}
}
animate=
{
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
20
}
}
isActive
?
{
y
:
-
14
,
scale
:
1.15
}
:
{
y
:
0
,
scale
:
1
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
400
,
damping
:
22
}
}
>
>
{
/* Glowing badge behind active icon */
}
{
isActive
&&
(
<
motion
.
div
className=
"absolute inset-0 -m-2 rounded-full bg-gold/20 blur-sm"
initial=
{
{
opacity
:
0
}
}
animate=
{
{
opacity
:
1
}
}
transition=
{
{
duration
:
0.2
}
}
/>
)
}
<
Icon
<
Icon
size=
{
22
}
size=
{
isCenter
?
24
:
20
}
className=
{
isActive
?
'text-gold'
:
'text-text-muted'
}
className=
{
isActive
?
'text-gold drop-shadow-[0_0_6px_rgba(255,200,60,0.6)]'
:
'text-text-muted'
}
strokeWidth=
{
isActive
?
2.8
:
2
}
strokeWidth=
{
isActive
?
2.8
:
2
}
/>
/>
</
motion
.
div
>
</
motion
.
div
>
<
span
className=
{
`text-[10px] font-bold ${isActive ? 'text-gold' : 'text-text-muted'}`
}
>
</
div
>
{
/* Label: shows when active */
}
<
motion
.
span
className=
"text-[9px] font-bold text-gold"
initial=
{
false
}
animate=
{
{
opacity
:
isActive
?
1
:
0
,
y
:
isActive
?
0
:
4
,
}
}
transition=
{
{
duration
:
0.2
}
}
>
{
item
.
label
}
{
item
.
label
}
</
span
>
</
motion
.
span
>
{
/* Active glow indicator */
}
{
isActive
&&
(
{
isActive
&&
(
<
motion
.
div
<
motion
.
div
className=
"absolute -bottom-0.5 w-6 h-[4
px] rounded-full bg-gold"
className=
"absolute -bottom-1 w-5 h-[3
px] rounded-full bg-gold"
layoutId=
"nav-indicator"
layoutId=
"nav-indicator"
style=
{
{
boxShadow
:
'0 0 10px rgba(255, 200, 60, 0.8
)'
}
}
style=
{
{
boxShadow
:
'0 0 8px rgba(255, 200, 60, 0.7
)'
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
30
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
30
}
}
/>
/>
)
}
)
}
...
@@ -58,6 +119,12 @@ export function BottomNav() {
...
@@ -58,6 +119,12 @@ export function BottomNav() {
})
}
})
}
</
div
>
</
div
>
</
div
>
</
div
>
{
/* Bottom L-accents */
}
<
div
className=
"absolute -bottom-1 left-1 w-3 h-3 border-b-2 border-l-2 border-gold/40 rounded-bl-sm pointer-events-none"
/>
<
div
className=
"absolute -bottom-1 right-1 w-3 h-3 border-b-2 border-r-2 border-gold/40 rounded-br-sm pointer-events-none"
/>
</
div
>
</
div
>
</
nav
>
</
nav
>
)
)
}
}
src/components/layout/DecorativeBackground.tsx
0 → 100644
View file @
4d37e0d3
import
{
motion
}
from
'framer-motion'
const
particles
=
[
{
x
:
'12%'
,
delay
:
0
,
duration
:
4.5
},
{
x
:
'28%'
,
delay
:
1.2
,
duration
:
5.2
},
{
x
:
'45%'
,
delay
:
0.5
,
duration
:
3.8
},
{
x
:
'62%'
,
delay
:
2.1
,
duration
:
4.8
},
{
x
:
'78%'
,
delay
:
0.8
,
duration
:
5.5
},
{
x
:
'88%'
,
delay
:
1.6
,
duration
:
4.2
},
{
x
:
'35%'
,
delay
:
2.8
,
duration
:
5.0
},
]
function
generateRayLines
()
{
const
lines
=
[]
for
(
let
i
=
0
;
i
<
12
;
i
++
)
{
const
angle
=
(
i
*
30
*
Math
.
PI
)
/
180
const
x2
=
50
+
45
*
Math
.
cos
(
angle
)
const
y2
=
50
+
45
*
Math
.
sin
(
angle
)
lines
.
push
(
<
line
key=
{
i
}
x1=
"50"
y1=
"50"
x2=
{
x2
}
y2=
{
y2
}
stroke=
"rgba(255, 200, 60, 0.04)"
strokeWidth=
"0.5"
/>
)
}
return
lines
}
export
function
DecorativeBackground
()
{
return
(
<
div
className=
"fixed inset-0 pointer-events-none z-0 overflow-hidden"
>
{
/* Arena gradient base */
}
<
div
className=
"absolute inset-0 bg-arena"
/>
{
/* Rotating ray pattern */
}
<
motion
.
div
className=
"absolute inset-0 flex items-center justify-center"
animate=
{
{
rotate
:
360
}
}
transition=
{
{
duration
:
90
,
repeat
:
Infinity
,
ease
:
'linear'
,
}
}
>
<
svg
viewBox=
"0 0 100 100"
className=
"w-[140vmax] h-[140vmax] opacity-60"
xmlns=
"http://www.w3.org/2000/svg"
>
{
generateRayLines
()
}
</
svg
>
</
motion
.
div
>
{
/* Floating gold particles */
}
{
particles
.
map
((
particle
,
i
)
=>
(
<
motion
.
div
key=
{
i
}
className=
"absolute w-1 h-1 rounded-full bg-gold/30"
style=
{
{
left
:
particle
.
x
,
top
:
'80%'
,
}
}
animate=
{
{
y
:
[
0
,
-
window
.
innerHeight
*
0.7
,
-
window
.
innerHeight
],
opacity
:
[
0
,
0.8
,
0
],
}
}
transition=
{
{
duration
:
particle
.
duration
,
repeat
:
Infinity
,
delay
:
particle
.
delay
,
ease
:
'linear'
,
}
}
/>
))
}
</
div
>
)
}
src/components/layout/Header.tsx
View file @
4d37e0d3
...
@@ -11,28 +11,65 @@ export function Header() {
...
@@ -11,28 +11,65 @@ export function Header() {
const
navigate
=
useNavigate
()
const
navigate
=
useNavigate
()
return
(
return
(
<
header
className=
"sticky top-0 z-50 px-4 pt-3 pb-
1 md:px-6
"
>
<
header
className=
"sticky top-0 z-50 px-4 pt-3 pb-
2
"
>
<
div
className=
"app-container !p-0"
>
<
div
className=
"app-container !p-0"
>
<
div
className=
"flex items-center justify-between px-4 py-2.5 bg-surface-2 border-3 border-border rounded-2xl shadow-lg shadow-black/30"
>
{
/* HUD Bar with diagonal bottom edge */
}
<
div
className=
"flex items-center gap-2"
>
<
div
<
GoldCrown
size=
{
28
}
animate=
{
false
}
/>
className=
"game-panel relative border-4 border-border bg-surface-2"
<
span
className=
"text-base font-black text-gold tracking-wider"
>
EL3AB
</
span
>
style=
{
{
clipPath
:
'polygon(0 0, 100% 0, 100% 85%, 98% 100%, 2% 100%, 0% 85%)'
,
}
}
>
<
div
className=
"flex items-center justify-between px-4 py-2.5"
>
{
/* Logo section (right side in RTL) */
}
<
div
className=
"flex items-center gap-2.5"
>
{
/* Shield-shaped container */
}
<
div
className=
"relative flex items-center justify-center w-11 h-11 bg-surface-3 border-2 border-gold/40"
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
,
}
}
>
<
GoldCrown
size=
{
22
}
animate=
{
false
}
/>
</
div
>
<
span
className=
"text-lg font-black text-gold tracking-widest"
style=
{
{
textShadow
:
'0 2px 4px rgba(0,0,0,0.5), 0 0 12px rgba(255,200,60,0.3)'
,
}
}
>
EL3AB
</
span
>
</
div
>
</
div
>
{
/* Center: Level indicator */
}
{
profile
&&
(
<
div
className=
"absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2"
>
<
div
className=
"w-8 h-8 rounded-full bg-surface-3 border-2 border-gold/50 flex items-center justify-center shadow-[0_0_8px_rgba(255,200,60,0.2)]"
>
<
span
className=
"text-[11px] font-black text-gold"
>
{
profile
.
level
||
1
}
</
span
>
</
div
>
</
div
>
)
}
{
/* Resources section (left side in RTL) */
}
<
div
className=
"flex items-center gap-2"
>
<
div
className=
"flex items-center gap-2"
>
{
profile
&&
(
{
profile
&&
(
<>
<>
{
/* Coins capsule */
}
<
motion
.
div
<
motion
.
div
className=
"flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-gold/15 border-2 border-gold/40
"
className=
"flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-gold/10 border-2 border-gold/40 shadow-[inset_0_0_8px_rgba(255,200,60,0.15)]
"
whileTap=
{
{
scale
:
0.9
}
}
whileTap=
{
{
scale
:
0.9
}
}
>
>
<
Coins
size=
{
14
}
className=
"text-gold"
/>
<
Coins
size=
{
14
}
className=
"text-gold"
/>
<
span
className=
"text-xs font-black text-gold"
>
{
profile
.
coins
}
</
span
>
<
span
className=
"text-xs font-black text-gold"
>
{
profile
.
coins
}
</
span
>
</
motion
.
div
>
</
motion
.
div
>
{
/* Gems capsule */
}
{
profile
.
gems
>
0
&&
(
{
profile
.
gems
>
0
&&
(
<
motion
.
div
<
motion
.
div
className=
"flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-purple/15 border-2 border-purple/40
"
className=
"flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-purple/10 border-2 border-purple/40 shadow-[inset_0_0_8px_rgba(180,77,255,0.15)]
"
whileTap=
{
{
scale
:
0.9
}
}
whileTap=
{
{
scale
:
0.9
}
}
>
>
<
Gem
size=
{
12
}
className=
"text-purple"
/>
<
Gem
size=
{
12
}
className=
"text-purple"
/>
...
@@ -42,27 +79,34 @@ export function Header() {
...
@@ -42,27 +79,34 @@ export function Header() {
</>
</>
)
}
)
}
{
/* Bell: hexagonal button */
}
<
motion
.
button
<
motion
.
button
className=
"relative p-2.5 rounded-xl bg-surface-3 border-2 border-border"
className=
"relative flex items-center justify-center w-10 h-10 bg-surface-3 border-2 border-border"
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
,
}
}
onClick=
{
()
=>
navigate
(
'/notifications'
)
}
onClick=
{
()
=>
navigate
(
'/notifications'
)
}
whileTap=
{
{
scale
:
0.85
}
}
whileTap=
{
{
scale
:
0.85
}
}
whileHover=
{
{
scale
:
1.05
}
}
whileHover=
{
{
scale
:
1.05
}
}
>
>
<
Bell
size=
{
18
}
className=
"text-text-secondary"
/>
<
Bell
size=
{
16
}
className=
"text-text-secondary"
/>
{
unreadCount
>
0
&&
(
{
unreadCount
>
0
&&
(
<
motion
.
div
<
motion
.
div
className=
"absolute -top-1 -right-1 w-5 h-5 rounded-full bg-coral border-2 border-surface-2 flex items-center justify-center
"
className=
"absolute -top-1 -right-1 w-5 h-5 rounded-full bg-coral border-2 border-surface-2 flex items-center justify-center animate-pulse-glow
"
initial=
{
{
scale
:
0
}
}
initial=
{
{
scale
:
0
}
}
animate=
{
{
scale
:
1
}
}
animate=
{
{
scale
:
1
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
15
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
15
}
}
>
>
<
span
className=
"text-[9px] font-black text-white"
>
{
unreadCount
>
9
?
'9+'
:
unreadCount
}
</
span
>
<
span
className=
"text-[9px] font-black text-white"
>
{
unreadCount
>
9
?
'9+'
:
unreadCount
}
</
span
>
</
motion
.
div
>
</
motion
.
div
>
)
}
)
}
</
motion
.
button
>
</
motion
.
button
>
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
</
header
>
</
header
>
)
)
}
}
src/components/layout/PageTransition.tsx
View file @
4d37e0d3
...
@@ -9,10 +9,10 @@ interface PageTransitionProps {
...
@@ -9,10 +9,10 @@ interface PageTransitionProps {
export
function
PageTransition
({
children
,
className
=
''
}:
PageTransitionProps
)
{
export
function
PageTransition
({
children
,
className
=
''
}:
PageTransitionProps
)
{
return
(
return
(
<
motion
.
div
<
motion
.
div
className=
{
`app-container py-
6 flex flex-col gap-6
${className}`
}
className=
{
`app-container py-
8 flex flex-col gap-7
${className}`
}
initial=
{
{
opacity
:
0
,
y
:
2
0
}
}
initial=
{
{
opacity
:
0
,
y
:
2
4
,
scale
:
0.97
}
}
animate=
{
{
opacity
:
1
,
y
:
0
}
}
animate=
{
{
opacity
:
1
,
y
:
0
,
scale
:
1
}
}
exit=
{
{
opacity
:
0
,
y
:
-
20
}
}
exit=
{
{
opacity
:
0
,
y
:
-
16
,
scale
:
0.98
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
300
,
damping
:
28
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
300
,
damping
:
28
}
}
>
>
{
children
}
{
children
}
...
...
src/components/layout/index.ts
View file @
4d37e0d3
export
{
AppShell
}
from
'./AppShell'
export
{
AppShell
}
from
'./AppShell'
export
{
DecorativeBackground
}
from
'./DecorativeBackground'
export
{
Header
}
from
'./Header'
export
{
Header
}
from
'./Header'
export
{
BottomNav
}
from
'./BottomNav'
export
{
BottomNav
}
from
'./BottomNav'
export
{
PageTransition
}
from
'./PageTransition'
export
{
PageTransition
}
from
'./PageTransition'
src/components/ui/GamePanel.tsx
0 → 100644
View file @
4d37e0d3
import
{
motion
}
from
'framer-motion'
import
type
{
ReactNode
}
from
'react'
interface
GamePanelProps
{
children
:
ReactNode
className
?:
string
variant
?:
'default'
|
'gold'
|
'legendary'
|
'recessed'
rivets
?:
boolean
onClick
?:
()
=>
void
}
export
function
GamePanel
({
children
,
className
=
''
,
variant
=
'default'
,
rivets
=
false
,
onClick
,
}:
GamePanelProps
)
{
const
baseClass
=
variant
===
'recessed'
?
'game-panel-recessed'
:
'game-panel'
const
goldClass
=
variant
===
'gold'
||
variant
===
'legendary'
?
'game-panel-gold'
:
''
return
(
<
motion
.
div
className=
{
`${baseClass} ${goldClass} p-5 relative overflow-hidden ${onClick ? 'cursor-pointer' : ''} ${className}`
}
whileHover=
{
onClick
?
{
y
:
-
4
,
scale
:
1.01
}
:
undefined
}
whileTap=
{
onClick
?
{
scale
:
0.97
}
:
undefined
}
onClick=
{
onClick
}
transition=
{
{
type
:
'spring'
,
stiffness
:
400
,
damping
:
22
}
}
>
{
/* Legendary shimmer overlay */
}
{
variant
===
'legendary'
&&
(
<
motion
.
div
className=
"absolute inset-0 pointer-events-none"
style=
{
{
background
:
'linear-gradient(105deg, transparent 40%, rgba(255, 200, 60, 0.12) 45%, rgba(255, 200, 60, 0.2) 50%, rgba(255, 200, 60, 0.12) 55%, transparent 60%)'
,
}
}
animate=
{
{
x
:
[
'-100%'
,
'200%'
],
}
}
transition=
{
{
duration
:
3
,
repeat
:
Infinity
,
ease
:
'easeInOut'
,
repeatDelay
:
1.5
,
}
}
/>
)
}
{
/* Rivets at 4 corners */
}
{
rivets
&&
(
<>
<
span
className=
"absolute top-3 left-3 w-[6px] h-[6px] rounded-full bg-surface-3 shadow-[inset_0_1px_2px_rgba(0,0,0,0.6),0_1px_0_rgba(255,255,255,0.05)]"
/>
<
span
className=
"absolute top-3 right-3 w-[6px] h-[6px] rounded-full bg-surface-3 shadow-[inset_0_1px_2px_rgba(0,0,0,0.6),0_1px_0_rgba(255,255,255,0.05)]"
/>
<
span
className=
"absolute bottom-3 left-3 w-[6px] h-[6px] rounded-full bg-surface-3 shadow-[inset_0_1px_2px_rgba(0,0,0,0.6),0_1px_0_rgba(255,255,255,0.05)]"
/>
<
span
className=
"absolute bottom-3 right-3 w-[6px] h-[6px] rounded-full bg-surface-3 shadow-[inset_0_1px_2px_rgba(0,0,0,0.6),0_1px_0_rgba(255,255,255,0.05)]"
/>
</>
)
}
{
/* Content */
}
<
div
className=
"relative z-10"
>
{
children
}
</
div
>
</
motion
.
div
>
)
}
src/components/ui/GameProgressBar.tsx
0 → 100644
View file @
4d37e0d3
interface
GameProgressBarProps
{
value
:
number
max
:
number
color
?:
'gold'
|
'cyan'
|
'green'
|
'purple'
showLabel
?:
boolean
}
const
fillColorClass
:
Record
<
string
,
string
>
=
{
gold
:
'progress-game-fill'
,
cyan
:
'progress-game-fill progress-game-fill-cyan'
,
green
:
'progress-game-fill progress-game-fill-green'
,
purple
:
'progress-game-fill progress-game-fill-purple'
,
}
export
function
GameProgressBar
({
value
,
max
,
color
=
'gold'
,
showLabel
=
false
,
}:
GameProgressBarProps
)
{
const
percentage
=
Math
.
min
(
100
,
Math
.
max
(
0
,
(
value
/
max
)
*
100
))
return
(
<
div
className=
"w-full"
>
<
div
className=
"progress-game relative"
>
{
/* Fill bar */
}
<
div
className=
{
`${fillColorClass[color]} relative`
}
style=
{
{
width
:
`${percentage}%`
}
}
>
{
/* Segmented overlay */
}
<
div
className=
"absolute inset-0 opacity-20"
style=
{
{
background
:
'repeating-linear-gradient(90deg, transparent 0px, transparent 8px, rgba(0,0,0,0.3) 8px, rgba(0,0,0,0.3) 10px)'
,
}
}
/>
</
div
>
</
div
>
{
showLabel
&&
(
<
div
className=
"flex justify-between mt-1"
>
<
span
className=
"text-xs text-text-muted font-bold"
>
{
value
}
</
span
>
<
span
className=
"text-xs text-text-muted"
>
{
max
}
</
span
>
</
div
>
)
}
</
div
>
)
}
src/components/ui/RibbonHeader.tsx
0 → 100644
View file @
4d37e0d3
import
type
{
ComponentType
}
from
'react'
interface
RibbonHeaderProps
{
text
:
string
icon
?:
ComponentType
<
{
size
?:
number
;
className
?:
string
}
>
color
?:
'gold'
|
'cyan'
|
'purple'
|
'coral'
size
?:
'sm'
|
'md'
}
const
colorStyles
=
{
gold
:
{
gradient
:
'linear-gradient(135deg, #FFC83D 0%, #C9972E 100%)'
,
border
:
'rgba(255, 200, 60, 0.6)'
,
text
:
'#1a1a2e'
,
},
cyan
:
{
gradient
:
'linear-gradient(135deg, #00E5CC 0%, #009E8C 100%)'
,
border
:
'rgba(0, 229, 204, 0.6)'
,
text
:
'#0a1a1a'
,
},
purple
:
{
gradient
:
'linear-gradient(135deg, #B44DFF 0%, #7B2EBF 100%)'
,
border
:
'rgba(180, 77, 255, 0.6)'
,
text
:
'#ffffff'
,
},
coral
:
{
gradient
:
'linear-gradient(135deg, #FF5252 0%, #BF2E2E 100%)'
,
border
:
'rgba(255, 82, 82, 0.6)'
,
text
:
'#ffffff'
,
},
}
const
sizeStyles
=
{
sm
:
{
padding
:
'6px 24px'
,
fontSize
:
'0.8rem'
,
iconSize
:
14
,
},
md
:
{
padding
:
'10px 36px'
,
fontSize
:
'0.95rem'
,
iconSize
:
18
,
},
}
export
function
RibbonHeader
({
text
,
icon
:
Icon
,
color
=
'gold'
,
size
=
'md'
}:
RibbonHeaderProps
)
{
const
colors
=
colorStyles
[
color
]
const
sizes
=
sizeStyles
[
size
]
return
(
<
div
className=
"flex justify-center w-full"
>
<
div
className=
"ribbon-banner inline-flex items-center gap-2 font-bold"
style=
{
{
background
:
colors
.
gradient
,
color
:
colors
.
text
,
padding
:
sizes
.
padding
,
fontSize
:
sizes
.
fontSize
,
borderBottom
:
`3px solid ${colors.border}`
,
boxShadow
:
`0 4px 12px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.2)`
,
}
}
>
{
Icon
&&
<
Icon
size=
{
sizes
.
iconSize
}
className=
"flex-shrink-0"
/>
}
<
span
>
{
text
}
</
span
>
</
div
>
</
div
>
)
}
src/components/ui/ShieldBadge.tsx
0 → 100644
View file @
4d37e0d3
import
type
{
ReactNode
}
from
'react'
interface
ShieldBadgeProps
{
children
:
ReactNode
size
?:
'sm'
|
'md'
|
'lg'
|
'xl'
color
?:
'gold'
|
'cyan'
|
'purple'
|
'coral'
|
'default'
glow
?:
boolean
className
?:
string
}
const
sizeMap
=
{
sm
:
'w-10 h-10'
,
md
:
'w-14 h-14'
,
lg
:
'w-20 h-20'
,
xl
:
'w-28 h-28'
,
}
const
colorMap
=
{
default
:
{
bg
:
'bg-surface-2'
,
ring
:
'rgba(61, 69, 112, 0.8)'
,
glow
:
'none'
,
},
gold
:
{
bg
:
'bg-gradient-to-br from-[#FFC83D] to-[#C9972E]'
,
ring
:
'rgba(255, 200, 60, 0.8)'
,
glow
:
'0 0 20px rgba(255, 200, 60, 0.4)'
,
},
cyan
:
{
bg
:
'bg-gradient-to-br from-[#00E5CC] to-[#009E8C]'
,
ring
:
'rgba(0, 229, 204, 0.8)'
,
glow
:
'0 0 20px rgba(0, 229, 204, 0.4)'
,
},
purple
:
{
bg
:
'bg-gradient-to-br from-[#B44DFF] to-[#7B2EBF]'
,
ring
:
'rgba(180, 77, 255, 0.8)'
,
glow
:
'0 0 20px rgba(180, 77, 255, 0.4)'
,
},
coral
:
{
bg
:
'bg-gradient-to-br from-[#FF5252] to-[#BF2E2E]'
,
ring
:
'rgba(255, 82, 82, 0.8)'
,
glow
:
'0 0 20px rgba(255, 82, 82, 0.4)'
,
},
}
export
function
ShieldBadge
({
children
,
size
=
'md'
,
color
=
'default'
,
glow
=
false
,
className
=
''
,
}:
ShieldBadgeProps
)
{
const
sizeClass
=
sizeMap
[
size
]
const
colorConfig
=
colorMap
[
color
]
return
(
<
div
className=
{
`relative inline-flex items-center justify-center ${sizeClass} ${className}`
}
style=
{
{
boxShadow
:
glow
?
colorConfig
.
glow
:
'none'
,
}
}
>
{
/* Outer ring */
}
<
div
className=
"absolute inset-0 clip-shield"
style=
{
{
background
:
colorConfig
.
ring
,
}
}
/>
{
/* Inner shield body */
}
<
div
className=
{
`absolute clip-shield ${colorConfig.bg} flex items-center justify-center`
}
style=
{
{
inset
:
'3px'
,
}
}
/>
{
/* Content */
}
<
div
className=
"relative z-10 flex items-center justify-center text-white font-bold"
>
{
children
}
</
div
>
</
div
>
)
}
src/components/ui/index.ts
View file @
4d37e0d3
export
{
Button
}
from
'./Button'
export
{
Button
}
from
'./Button'
export
{
Card
}
from
'./Card'
export
{
Card
}
from
'./Card'
export
{
GamePanel
}
from
'./GamePanel'
export
{
GameProgressBar
}
from
'./GameProgressBar'
export
{
Input
}
from
'./Input'
export
{
Input
}
from
'./Input'
export
{
RibbonHeader
}
from
'./RibbonHeader'
export
{
ShieldBadge
}
from
'./ShieldBadge'
export
{
ToastContainer
}
from
'./ToastContainer'
export
{
ToastContainer
}
from
'./ToastContainer'
src/index.css
View file @
4d37e0d3
...
@@ -76,56 +76,224 @@ body {
...
@@ -76,56 +76,224 @@ body {
background
:
var
(
--color-gold-muted
);
background
:
var
(
--color-gold-muted
);
}
}
/* === GAME CARD THICK BORDERS === */
/* ============================================================
.game-card
{
ARENA BACKGROUND SYSTEM
border
:
3px
solid
var
(
--color-border
);
============================================================ */
border-radius
:
20px
;
.bg-arena
{
background
:
var
(
--color-surface-1
);
background
:
transition
:
transform
0.15s
,
box-shadow
0.15s
;
repeating-conic-gradient
(
from
0deg
at
50%
50%
,
rgba
(
255
,
200
,
60
,
0.012
)
0deg
10deg
,
transparent
10deg
20deg
),
radial-gradient
(
ellipse
at
center
,
transparent
40%
,
rgba
(
0
,
0
,
0
,
0.6
)
100%
),
linear-gradient
(
180deg
,
#0B0E1A
0%
,
#0F1324
50%
,
#0B0E1A
100%
);
}
/* ============================================================
DECORATIVE CLIP-PATH SHAPES
============================================================ */
.clip-shield
{
clip-path
:
polygon
(
50%
0%
,
93%
25%
,
93%
75%
,
50%
100%
,
7%
75%
,
7%
25%
);
}
.clip-diamond
{
clip-path
:
polygon
(
50%
0%
,
100%
50%
,
50%
100%
,
0%
50%
);
}
}
.game-card
:hover
{
.clip-nameplate
{
transform
:
translateY
(
-3px
);
clip-path
:
polygon
(
5%
0%
,
100%
0%
,
95%
100%
,
0%
100%
);
box-shadow
:
0
8px
24px
rgba
(
0
,
0
,
0
,
0.5
);
}
}
.game-card-gold
{
.ribbon-banner
{
clip-path
:
polygon
(
8%
0%
,
92%
0%
,
100%
100%
,
0%
100%
);
}
/* ============================================================
GAME PANEL SYSTEM
============================================================ */
.game-panel
{
position
:
relative
;
border-radius
:
20px
;
background
:
linear-gradient
(
160deg
,
var
(
--color-surface-2
)
0%
,
var
(
--color-surface-1
)
100%
);
border
:
3px
solid
var
(
--color-border
);
box-shadow
:
inset
0
1px
0
rgba
(
255
,
255
,
255
,
0.05
),
inset
0
-3px
6px
rgba
(
0
,
0
,
0
,
0.3
),
0
6px
20px
rgba
(
0
,
0
,
0
,
0.4
);
}
.game-panel-gold
{
border-color
:
var
(
--color-gold
);
border-color
:
var
(
--color-gold
);
box-shadow
:
0
0
20px
rgba
(
255
,
200
,
60
,
0.2
),
inset
0
1px
0
rgba
(
255
,
255
,
255
,
0.05
);
box-shadow
:
inset
0
1px
0
rgba
(
255
,
255
,
255
,
0.05
),
inset
0
-3px
6px
rgba
(
0
,
0
,
0
,
0.3
),
0
6px
20px
rgba
(
0
,
0
,
0
,
0.4
),
0
0
24px
rgba
(
255
,
200
,
60
,
0.25
);
}
.game-panel-recessed
{
position
:
relative
;
border-radius
:
16px
;
background
:
var
(
--color-surface-1
);
border
:
2px
solid
var
(
--color-border
);
box-shadow
:
inset
0
3px
8px
rgba
(
0
,
0
,
0
,
0.5
),
inset
0
1px
2px
rgba
(
0
,
0
,
0
,
0.3
);
}
}
/* === PLAYFUL BUTTON PUSH EFFECT === */
/* ============================================================
.btn-push
{
3D BUTTON SYSTEM
============================================================ */
.btn-3d
{
position
:
relative
;
position
:
relative
;
border-bottom
:
4px
solid
rgba
(
0
,
0
,
0
,
0.3
);
border-radius
:
16px
;
transition
:
all
0.1s
;
box-shadow
:
0
6px
0
var
(
--btn-shadow-color
,
rgba
(
0
,
0
,
0
,
0.4
)),
0
8px
16px
rgba
(
0
,
0
,
0
,
0.3
),
inset
0
2px
0
rgba
(
255
,
255
,
255
,
0.15
);
transform
:
translateY
(
0
);
transition
:
transform
0.08s
,
box-shadow
0.08s
;
}
.btn-3d
:active
{
transform
:
translateY
(
4px
);
box-shadow
:
0
2px
0
var
(
--btn-shadow-color
,
rgba
(
0
,
0
,
0
,
0.4
)),
0
3px
8px
rgba
(
0
,
0
,
0
,
0.2
),
inset
0
2px
0
rgba
(
255
,
255
,
255
,
0.1
);
}
.btn-3d-gold
{
--btn-shadow-color
:
#8B6914
;
background
:
linear-gradient
(
180deg
,
var
(
--color-gold-light
)
0%
,
var
(
--color-gold
)
100%
);
color
:
#1a1a2e
;
}
.btn-3d-cyan
{
--btn-shadow-color
:
#006B5E
;
background
:
linear-gradient
(
180deg
,
#33FFDD
0%
,
var
(
--color-cyan
)
100%
);
color
:
#0a1a1a
;
}
.btn-3d-coral
{
--btn-shadow-color
:
#8B1A1A
;
background
:
linear-gradient
(
180deg
,
#FF7575
0%
,
var
(
--color-coral
)
100%
);
color
:
#fff
;
}
.btn-3d-purple
{
--btn-shadow-color
:
#4A1A8B
;
background
:
linear-gradient
(
180deg
,
#C97AFF
0%
,
var
(
--color-purple
)
100%
);
color
:
#fff
;
}
/* ============================================================
GAME PROGRESS BAR
============================================================ */
.progress-game
{
height
:
16px
;
border-radius
:
8px
;
background
:
var
(
--color-surface-3
);
border
:
2px
solid
var
(
--color-border
);
overflow
:
hidden
;
}
.progress-game-fill
{
height
:
100%
;
border-radius
:
6px
;
background
:
linear-gradient
(
90deg
,
var
(
--color-gold
),
var
(
--color-gold-light
));
box-shadow
:
0
0
12px
rgba
(
255
,
200
,
60
,
0.5
);
}
.progress-game-fill-cyan
{
background
:
linear-gradient
(
90deg
,
#00B8A5
,
var
(
--color-cyan
));
box-shadow
:
0
0
12px
rgba
(
0
,
229
,
204
,
0.5
);
}
.progress-game-fill-green
{
background
:
linear-gradient
(
90deg
,
#2DBF60
,
var
(
--color-green
));
box-shadow
:
0
0
12px
rgba
(
74
,
222
,
128
,
0.5
);
}
.progress-game-fill-purple
{
background
:
linear-gradient
(
90deg
,
#9333EA
,
var
(
--color-purple
));
box-shadow
:
0
0
12px
rgba
(
180
,
77
,
255
,
0.5
);
}
/* ============================================================
ANIMATIONS
============================================================ */
@keyframes
shimmer
{
0
%
{
transform
:
translateX
(
-100%
)
skewX
(
-15deg
);
}
100
%
{
transform
:
translateX
(
200%
)
skewX
(
-15deg
);
}
}
@keyframes
badge-bounce
{
0
%,
100
%
{
transform
:
scale
(
1
);
}
25
%
{
transform
:
scale
(
1.15
);
}
50
%
{
transform
:
scale
(
0.95
);
}
75
%
{
transform
:
scale
(
1.05
);
}
}
@keyframes
rays-rotate
{
from
{
transform
:
rotate
(
0deg
);
}
to
{
transform
:
rotate
(
360deg
);
}
}
@keyframes
float-gentle
{
0
%,
100
%
{
transform
:
translateY
(
0
);
}
50
%
{
transform
:
translateY
(
-8px
);
}
}
@keyframes
pulse-soft
{
0
%,
100
%
{
opacity
:
0.6
;
}
50
%
{
opacity
:
1
;
}
}
}
.btn-push
:active
{
.animate-shimmer
{
border-bottom-width
:
1px
;
animation
:
shimmer
3s
ease-in-out
infinite
;
transform
:
translateY
(
3px
);
}
}
/* === GLOW ANIMATIONS === */
.animate-badge-bounce
{
@keyframes
pulse-glow
{
animation
:
badge-bounce
0.5s
ease-in-out
;
0
%,
100
%
{
box-shadow
:
0
0
8px
rgba
(
255
,
200
,
60
,
0.3
);
}
50
%
{
box-shadow
:
0
0
24px
rgba
(
255
,
200
,
60
,
0.6
);
}
}
}
@keyframes
float
{
.animate-rays-rotate
{
0
%,
100
%
{
transform
:
translateY
(
0
);
}
animation
:
rays-rotate
90s
linear
infinite
;
50
%
{
transform
:
translateY
(
-6px
);
}
}
}
.animate-
pulse-glow
{
.animate-
float-gentle
{
animation
:
pulse-glow
2
s
ease-in-out
infinite
;
animation
:
float-gentle
3
s
ease-in-out
infinite
;
}
}
.animate-
floa
t
{
.animate-
pulse-sof
t
{
animation
:
float
3
s
ease-in-out
infinite
;
animation
:
pulse-soft
2
s
ease-in-out
infinite
;
}
}
/* === RESPONSIVE CONTAINER === */
/* ============================================================
RESPONSIVE CONTAINER
============================================================ */
.app-container
{
.app-container
{
width
:
100%
;
width
:
100%
;
max-width
:
480px
;
max-width
:
480px
;
...
...
src/pages/BotSelectPage.tsx
View file @
4d37e0d3
...
@@ -2,10 +2,8 @@ import { motion } from 'framer-motion'
...
@@ -2,10 +2,8 @@ import { motion } from 'framer-motion'
import
{
useEffect
,
useState
}
from
'react'
import
{
useEffect
,
useState
}
from
'react'
import
{
useNavigate
}
from
'react-router-dom'
import
{
useNavigate
}
from
'react-router-dom'
import
{
PageTransition
}
from
'../components/layout/PageTransition'
import
{
PageTransition
}
from
'../components/layout/PageTransition'
import
{
Card
}
from
'../components/ui/Card'
import
{
Button
}
from
'../components/ui/Button'
import
{
fetchBots
,
getBotPortraitUrl
,
type
Bot
}
from
'../lib/stockfish'
import
{
fetchBots
,
getBotPortraitUrl
,
type
Bot
}
from
'../lib/stockfish'
import
{
ChevronRight
,
Swords
}
from
'lucide-react'
import
{
ChevronRight
,
Swords
,
Check
}
from
'lucide-react'
const
DIFFICULTY_COLORS
:
Record
<
string
,
string
>
=
{
const
DIFFICULTY_COLORS
:
Record
<
string
,
string
>
=
{
beginner
:
'#00E5CC'
,
beginner
:
'#00E5CC'
,
...
@@ -17,7 +15,7 @@ const DIFFICULTY_COLORS: Record<string, string> = {
...
@@ -17,7 +15,7 @@ const DIFFICULTY_COLORS: Record<string, string> = {
near_perfect
:
'#FFE066'
,
near_perfect
:
'#FFE066'
,
}
}
const
TOTAL_STRENGTH_
DO
TS
=
7
const
TOTAL_STRENGTH_
SEGMEN
TS
=
7
export
function
BotSelectPage
()
{
export
function
BotSelectPage
()
{
const
navigate
=
useNavigate
()
const
navigate
=
useNavigate
()
...
@@ -40,7 +38,7 @@ export function BotSelectPage() {
...
@@ -40,7 +38,7 @@ export function BotSelectPage() {
if
(
loading
)
{
if
(
loading
)
{
return
(
return
(
<
PageTransition
className=
"flex flex-col items-center justify-center min-h-[60vh]"
>
<
PageTransition
className=
"flex flex-col items-center justify-center min-h-[60vh]"
>
<
div
className=
"w-12 h-12 border-3 border-
gold/30 border-t-gold
rounded-full animate-spin"
/>
<
div
className=
"w-12 h-12 border-3 border-
[#FFC83D]/30 border-t-[#FFC83D]
rounded-full animate-spin"
/>
<
p
className=
"mt-4 text-sm text-text-muted font-bold"
>
جاري تحميل الروبوتات...
</
p
>
<
p
className=
"mt-4 text-sm text-text-muted font-bold"
>
جاري تحميل الروبوتات...
</
p
>
</
PageTransition
>
</
PageTransition
>
)
)
...
@@ -48,13 +46,19 @@ export function BotSelectPage() {
...
@@ -48,13 +46,19 @@ export function BotSelectPage() {
return
(
return
(
<
PageTransition
className=
"pb-36"
>
<
PageTransition
className=
"pb-36"
>
{
/* Header */
}
<
div
className=
"flex items-center gap-4"
>
<
div
className=
"flex items-center gap-4"
>
<
motion
.
button
<
motion
.
button
onClick=
{
()
=>
navigate
(
-
1
)
}
onClick=
{
()
=>
navigate
(
-
1
)
}
className=
"w-11 h-11 rounded-xl bg-surface-2 border-2 border-border flex items-center justify-center"
className=
"w-12 h-12 flex items-center justify-center"
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
}
}
whileTap=
{
{
scale
:
0.85
}
}
whileTap=
{
{
scale
:
0.85
}
}
>
<
div
className=
"w-full h-full bg-surface-2 border-2 border-border flex items-center justify-center"
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
}
}
>
>
<
ChevronRight
size=
{
20
}
className=
"text-text-secondary"
/>
<
ChevronRight
size=
{
20
}
className=
"text-text-secondary"
/>
</
div
>
</
motion
.
button
>
</
motion
.
button
>
<
div
>
<
div
>
<
h1
className=
"text-xl font-black"
>
العب ضد الروبوت
</
h1
>
<
h1
className=
"text-xl font-black"
>
العب ضد الروبوت
</
h1
>
...
@@ -62,27 +66,48 @@ export function BotSelectPage() {
...
@@ -62,27 +66,48 @@ export function BotSelectPage() {
</
div
>
</
div
>
</
div
>
</
div
>
{
/* Bot cards */
}
<
div
className=
"flex flex-col gap-3"
>
<
div
className=
"flex flex-col gap-3"
>
{
bots
.
map
((
bot
,
i
)
=>
{
{
bots
.
map
((
bot
,
i
)
=>
{
const
isSelected
=
selectedBot
===
bot
.
id
const
isSelected
=
selectedBot
===
bot
.
id
const
diffColor
=
DIFFICULTY_COLORS
[
bot
.
style
]
||
'#6E748C'
const
diffColor
=
DIFFICULTY_COLORS
[
bot
.
style
]
||
'#6E748C'
const
strengthLevel
=
Math
.
min
(
i
+
1
,
TOTAL_STRENGTH_
DO
TS
)
const
strengthLevel
=
Math
.
min
(
i
+
1
,
TOTAL_STRENGTH_
SEGMEN
TS
)
return
(
return
(
<
motion
.
div
<
motion
.
div
key=
{
bot
.
id
}
key=
{
bot
.
id
}
initial=
{
{
opacity
:
0
,
x
:
20
}
}
initial=
{
{
opacity
:
0
,
x
:
20
}
}
animate=
{
{
opacity
:
1
,
x
:
0
}
}
animate=
{
{
opacity
:
1
,
x
:
0
,
y
:
isSelected
?
-
4
:
0
,
}
}
transition=
{
{
delay
:
i
*
0.06
,
type
:
'spring'
,
stiffness
:
400
,
damping
:
22
}
}
transition=
{
{
delay
:
i
*
0.06
,
type
:
'spring'
,
stiffness
:
400
,
damping
:
22
}
}
>
>
<
Card
<
motion
.
div
glow=
{
isSelected
}
onClick=
{
()
=>
setSelectedBot
(
bot
.
id
)
}
onClick=
{
()
=>
setSelectedBot
(
bot
.
id
)
}
className=
{
`flex items-center gap-4 ${isSelected ? '!border-gold' : ''}`
}
className=
{
`game-panel !p-4 flex items-center gap-4 relative overflow-hidden cursor-pointer transition-all ${
isSelected ? '!border-[#FFC83D]' : ''
}`
}
style=
{
{
boxShadow
:
isSelected
?
`0 0 16px rgba(255,200,61,0.3), inset 0 1px 0 rgba(255,255,255,0.05)`
:
undefined
,
}
}
>
>
{
/* Colored left accent */
}
<
div
<
div
className=
"relative w-14 h-14 rounded-xl overflow-hidden flex-shrink-0 border-2"
className=
"absolute right-0 top-0 bottom-0 w-[5px] rounded-r-full"
style=
{
{
backgroundColor
:
`${diffColor}15`
,
borderColor
:
`${diffColor}60`
}
}
style=
{
{
backgroundColor
:
diffColor
}
}
/>
{
/* Portrait frame */
}
<
div
className=
"relative w-[60px] h-[60px] rounded-xl overflow-hidden flex-shrink-0 border-3 mr-1"
style=
{
{
borderColor
:
diffColor
,
boxShadow
:
`inset 0 0 12px ${diffColor}30`
,
}
}
>
>
<
img
<
img
src=
{
getBotPortraitUrl
(
bot
.
id
)
}
src=
{
getBotPortraitUrl
(
bot
.
id
)
}
...
@@ -91,51 +116,71 @@ export function BotSelectPage() {
...
@@ -91,51 +116,71 @@ export function BotSelectPage() {
onError=
{
(
e
)
=>
{
(
e
.
target
as
HTMLImageElement
).
style
.
display
=
'none'
}
}
onError=
{
(
e
)
=>
{
(
e
.
target
as
HTMLImageElement
).
style
.
display
=
'none'
}
}
/>
/>
<
div
className=
"absolute inset-0 flex items-center justify-center"
style=
{
{
color
:
diffColor
}
}
>
<
div
className=
"absolute inset-0 flex items-center justify-center"
style=
{
{
color
:
diffColor
}
}
>
<
span
className=
"text-xl font-black"
>
{
bot
.
name_ar
?.
charAt
(
0
)
||
bot
.
name
.
charAt
(
0
)
}
</
span
>
<
span
className=
"text-
2
xl font-black"
>
{
bot
.
name_ar
?.
charAt
(
0
)
||
bot
.
name
.
charAt
(
0
)
}
</
span
>
</
div
>
</
div
>
</
div
>
</
div
>
{
/* Info */
}
<
div
className=
"flex-1 min-w-0 space-y-1.5"
>
<
div
className=
"flex-1 min-w-0 space-y-1.5"
>
<
div
className=
"flex items-center gap-2"
>
<
div
className=
"flex items-center gap-2"
>
<
h3
className=
"text-sm font-black truncate"
>
{
bot
.
name_ar
}
</
h3
>
<
h3
className=
"text-base font-black truncate"
>
{
bot
.
name_ar
}
</
h3
>
{
/* Style badge - shield shape */
}
<
span
<
span
className=
"px-2 py-0.5 rounded-lg text-[9px] font-black"
className=
"px-2.5 py-0.5 text-[9px] font-black rounded-md"
style=
{
{
backgroundColor
:
`${diffColor}25`
,
color
:
diffColor
}
}
style=
{
{
backgroundColor
:
`${diffColor}20`
,
color
:
diffColor
,
border
:
`2px solid ${diffColor}40`
,
}
}
>
>
{
bot
.
style_ar
}
{
bot
.
style_ar
}
</
span
>
</
span
>
</
div
>
</
div
>
<
p
className=
"text-[11px] text-text-muted truncate font-semibold"
>
{
bot
.
bio_ar
}
</
p
>
<
div
className=
"flex items-center gap-3"
>
{
/* Strength - segmented game bar */
}
<
div
className=
"flex items-center gap-2"
>
<
span
className=
"text-[10px] text-text-muted font-bold"
>
{
bot
.
elo_min
}
-
{
bot
.
elo_max
}
</
span
>
<
span
className=
"text-[10px] text-text-muted font-bold"
>
{
bot
.
elo_min
}
-
{
bot
.
elo_max
}
</
span
>
<
div
className=
"flex items-center gap-[3px]"
>
<
div
className=
"flex items-center gap-[3px]"
>
{
Array
.
from
({
length
:
TOTAL_STRENGTH_
DOTS
}).
map
((
_
,
dot
Index
)
=>
(
{
Array
.
from
({
length
:
TOTAL_STRENGTH_
SEGMENTS
}).
map
((
_
,
seg
Index
)
=>
(
<
div
<
div
key=
{
dotIndex
}
key=
{
segIndex
}
className=
"w-[6px] h-[6px] rounded-full"
className=
"w-[14px] h-[7px] rounded-sm"
style=
{
{
backgroundColor
:
dotIndex
<
strengthLevel
?
diffColor
:
`${diffColor}20`
}
}
style=
{
{
backgroundColor
:
segIndex
<
strengthLevel
?
diffColor
:
`${diffColor}15`
,
boxShadow
:
segIndex
<
strengthLevel
?
`0 0 4px ${diffColor}40`
:
'none'
,
}
}
/>
/>
))
}
))
}
</
div
>
</
div
>
</
div
>
</
div
>
{
/* Bio */
}
<
p
className=
"text-[11px] text-text-muted truncate font-semibold"
>
{
bot
.
bio_ar
}
</
p
>
</
div
>
</
div
>
{
/* Selected state - checkmark in shield */
}
{
isSelected
&&
(
{
isSelected
&&
(
<
motion
.
div
<
motion
.
div
className=
"w-8 h-8 rounded-full bg-gold flex items-center justify-center flex-shrink-0"
className=
"w-10 h-10 flex items-center justify-center flex-shrink-0"
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
}
}
initial=
{
{
scale
:
0
,
rotate
:
-
90
}
}
initial=
{
{
scale
:
0
,
rotate
:
-
90
}
}
animate=
{
{
scale
:
1
,
rotate
:
0
}
}
animate=
{
{
scale
:
1
,
rotate
:
0
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
15
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
15
}
}
>
>
<
Swords
size=
{
14
}
className=
"text-background"
/>
<
div
className=
"w-full h-full bg-[#FFC83D] flex items-center justify-center"
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
}
}
>
<
Check
size=
{
16
}
className=
"text-background"
/>
</
div
>
</
motion
.
div
>
</
motion
.
div
>
)
}
)
}
</
Card
>
</
motion
.
div
>
</
motion
.
div
>
</
motion
.
div
>
)
)
})
}
})
}
</
div
>
</
div
>
{
/* Bottom sticky button */
}
{
selectedBot
&&
(
{
selectedBot
&&
(
<
motion
.
div
<
motion
.
div
className=
"fixed bottom-0 left-0 right-0 px-5 pt-4 pb-10 bg-gradient-to-t from-background via-background/95 to-transparent"
className=
"fixed bottom-0 left-0 right-0 px-5 pt-4 pb-10 bg-gradient-to-t from-background via-background/95 to-transparent"
...
@@ -143,10 +188,16 @@ export function BotSelectPage() {
...
@@ -143,10 +188,16 @@ export function BotSelectPage() {
animate=
{
{
opacity
:
1
,
y
:
0
}
}
animate=
{
{
opacity
:
1
,
y
:
0
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
400
,
damping
:
22
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
400
,
damping
:
22
}
}
>
>
<
div
className=
"app-container !p-0"
>
<
div
className=
"flex justify-center"
>
<
Button
onClick=
{
startGame
}
className=
"w-[80%] mx-auto block"
size=
"lg"
>
<
motion
.
button
ابدا المباراة
onClick=
{
startGame
}
</
Button
>
whileTap=
{
{
scale
:
0.95
}
}
className=
"btn-3d w-[80%] py-4 rounded-2xl bg-gradient-to-b from-[#FFC83D] to-[#E5A800] text-background text-lg font-black flex items-center justify-center gap-2"
style=
{
{
boxShadow
:
'0 6px 0 #B8860B, 0 8px 20px rgba(255,200,61,0.3)'
}
}
>
<
Swords
size=
{
20
}
/>
<
span
>
ابدا المباراة
</
span
>
</
motion
.
button
>
</
div
>
</
div
>
</
motion
.
div
>
</
motion
.
div
>
)
}
)
}
...
...
src/pages/FriendsPage.tsx
View file @
4d37e0d3
import
{
useState
}
from
'react'
import
{
useState
}
from
'react'
import
{
motion
,
AnimatePresence
}
from
'framer-motion'
import
{
motion
,
AnimatePresence
}
from
'framer-motion'
import
{
UserPlus
,
UserCheck
,
UserX
,
Search
,
Loader2
}
from
'lucide-react'
import
{
UserPlus
,
UserCheck
,
UserX
,
Search
,
Loader2
,
Shield
}
from
'lucide-react'
import
{
PageTransition
}
from
'../components/layout/PageTransition'
import
{
PageTransition
}
from
'../components/layout/PageTransition'
import
{
Card
}
from
'../components/ui/Card'
import
{
Button
}
from
'../components/ui/Button'
import
{
useFriends
}
from
'../hooks/useFriends'
import
{
useFriends
}
from
'../hooks/useFriends'
import
{
supabase
}
from
'../lib/supabase'
import
{
supabase
}
from
'../lib/supabase'
import
{
useAuthStore
}
from
'../stores/authStore'
import
{
useAuthStore
}
from
'../stores/authStore'
...
@@ -75,25 +73,37 @@ export function FriendsPage() {
...
@@ -75,25 +73,37 @@ export function FriendsPage() {
return
(
return
(
<
PageTransition
>
<
PageTransition
>
<
div
className=
"flex flex-col gap-5 py-6"
>
{
/* Header */
}
<
div
className=
"flex items-center justify-between"
>
<
div
className=
"flex items-center justify-between"
>
<
h1
className=
"text-xl font-black"
>
الأصدقاء
</
h1
>
<
div
className=
"flex items-center gap-3"
>
<
h1
className=
"text-2xl font-black text-text-primary"
>
الأصدقاء
</
h1
>
</
div
>
<
motion
.
div
<
motion
.
div
className=
"p-2.5 rounded-xl bg-[#FFC83D]/10 border-3 border-[#FFC83D]/30"
className=
"w-11 h-11 flex items-center justify-center"
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
}
}
whileTap=
{
{
scale
:
0.85
}
}
whileTap=
{
{
scale
:
0.85
}
}
>
>
<
UserPlus
size=
{
20
}
className=
"text-[#FFC83D]"
/>
<
div
className=
"w-full h-full bg-gradient-to-br from-[#FFC83D]/20 to-[#FFC83D]/5 border-2 border-[#FFC83D]/40 flex items-center justify-center"
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
}
}
>
<
UserPlus
size=
{
18
}
className=
"text-[#FFC83D]"
/>
</
div
>
</
motion
.
div
>
</
motion
.
div
>
</
div
>
</
div
>
{
/* Decorative accent line */
}
<
div
className=
"h-[3px] rounded-full bg-gradient-to-l from-[#FFC83D] via-[#FFC83D]/40 to-transparent"
/>
{
/* Search Input - recessed game style */
}
<
div
className=
"relative"
>
<
div
className=
"relative"
>
<
Search
size=
{
16
}
className=
"absolute right-3 top-1/2 -translate-y-1/2 text-text-muted
"
/>
<
Search
size=
{
16
}
className=
"absolute right-4 top-1/2 -translate-y-1/2 text-text-muted z-10
"
/>
<
input
<
input
type=
"text"
type=
"text"
placeholder=
"بحث عن لاعب..."
placeholder=
"بحث عن لاعب..."
value=
{
searchQuery
}
value=
{
searchQuery
}
onChange=
{
(
e
)
=>
handleSearch
(
e
.
target
.
value
)
}
onChange=
{
(
e
)
=>
handleSearch
(
e
.
target
.
value
)
}
className=
"w-full pr-10 pl-4 py-3 rounded-xl bg-surface-2 border-3 border-border text-sm font-bold placeholder:text-text-muted outline-none focus:border-[#FFC83D]/50 transition-colors"
className=
"w-full pr-11 pl-4 py-3.5 rounded-2xl bg-surface-2 border-3 border-border text-sm font-bold placeholder:text-text-muted outline-none focus:border-[#FFC83D]/50 transition-colors"
style=
{
{
boxShadow
:
'inset 0 3px 8px rgba(0,0,0,0.4), inset 0 1px 2px rgba(0,0,0,0.3)'
}
}
dir=
"rtl"
dir=
"rtl"
/>
/>
</
div
>
</
div
>
...
@@ -118,12 +128,18 @@ export function FriendsPage() {
...
@@ -118,12 +128,18 @@ export function FriendsPage() {
const
alreadySent
=
sentIds
.
has
(
result
.
id
)
const
alreadySent
=
sentIds
.
has
(
result
.
id
)
return
(
return
(
<
motion
.
div
key=
{
result
.
id
}
variants=
{
item
}
>
<
motion
.
div
key=
{
result
.
id
}
variants=
{
item
}
>
<
Card
className=
"!p-3.5 flex items-center gap-3"
>
<
div
className=
"game-panel !p-3.5 flex items-center gap-3"
>
<
div
className=
"w-10 h-10 rounded-full bg-gradient-to-br from-[#FFC83D]/20 to-[#B44DFF]/10 border-3 border-border flex items-center justify-center shrink-0"
>
<
div
className=
"w-10 h-10 flex items-center justify-center shrink-0"
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
}
}
>
<
div
className=
"w-full h-full bg-gradient-to-br from-[#FFC83D]/20 to-[#B44DFF]/10 flex items-center justify-center"
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
}
}
>
<
span
className=
"text-sm font-black text-[#FFC83D]"
>
<
span
className=
"text-sm font-black text-[#FFC83D]"
>
{
result
.
display_name
?.[
0
]
||
result
.
username
[
0
]
}
{
result
.
display_name
?.[
0
]
||
result
.
username
[
0
]
}
</
span
>
</
span
>
</
div
>
</
div
>
</
div
>
<
div
className=
"flex-1 min-w-0"
>
<
div
className=
"flex-1 min-w-0"
>
<
p
className=
"text-sm font-black truncate"
>
{
result
.
display_name
||
result
.
username
}
</
p
>
<
p
className=
"text-sm font-black truncate"
>
{
result
.
display_name
||
result
.
username
}
</
p
>
<
p
className=
"text-xs text-text-muted font-bold"
>
{
result
.
elo_blitz
}
</
p
>
<
p
className=
"text-xs text-text-muted font-bold"
>
{
result
.
elo_blitz
}
</
p
>
...
@@ -133,11 +149,15 @@ export function FriendsPage() {
...
@@ -133,11 +149,15 @@ export function FriendsPage() {
)
:
alreadySent
?
(
)
:
alreadySent
?
(
<
span
className=
"text-xs font-bold text-text-muted"
>
تم الارسال
</
span
>
<
span
className=
"text-xs font-bold text-text-muted"
>
تم الارسال
</
span
>
)
:
(
)
:
(
<
Button
size=
"sm"
variant=
"gold"
onClick=
{
()
=>
handleSendRequest
(
result
.
id
)
}
>
<
motion
.
button
whileTap=
{
{
scale
:
0.9
}
}
onClick=
{
()
=>
handleSendRequest
(
result
.
id
)
}
className=
"btn-3d px-4 py-2 rounded-xl bg-[#FFC83D] text-background text-xs font-black"
>
اضافة
اضافة
</
B
utton
>
</
motion
.
b
utton
>
)
}
)
}
</
Card
>
</
div
>
</
motion
.
div
>
</
motion
.
div
>
)
)
})
}
})
}
...
@@ -151,17 +171,27 @@ export function FriendsPage() {
...
@@ -151,17 +171,27 @@ export function FriendsPage() {
</
div
>
</
div
>
)
:
(
)
:
(
<
motion
.
div
className=
"flex flex-col gap-5"
variants=
{
stagger
}
initial=
"hidden"
animate=
"show"
>
<
motion
.
div
className=
"flex flex-col gap-5"
variants=
{
stagger
}
initial=
"hidden"
animate=
"show"
>
{
/* Friend Requests */
}
{
pendingReceived
.
length
>
0
&&
(
{
pendingReceived
.
length
>
0
&&
(
<
motion
.
div
className=
"flex flex-col gap-2"
variants=
{
item
}
>
<
motion
.
div
className=
"flex flex-col gap-2.5"
variants=
{
item
}
>
<
h2
className=
"text-sm font-black text-[#FFC83D]"
>
طلبات الصداقة (
{
pendingReceived
.
length
}
)
</
h2
>
<
h2
className=
"text-sm font-black text-[#FFC83D] flex items-center gap-2"
>
<
div
className=
"flex flex-col gap-2"
>
<
div
className=
"w-1.5 h-4 rounded-full bg-[#FFC83D]"
/>
طلبات الصداقة (
{
pendingReceived
.
length
}
)
</
h2
>
<
div
className=
"flex flex-col gap-2.5"
>
{
pendingReceived
.
map
((
req
)
=>
(
{
pendingReceived
.
map
((
req
)
=>
(
<
Card
key=
{
req
.
id
}
className=
"!p-3.5 flex items-center gap-3"
>
<
div
key=
{
req
.
id
}
className=
"game-panel-gold !p-3.5 flex items-center gap-3"
>
<
div
className=
"w-10 h-10 rounded-full bg-gradient-to-br from-[#FFC83D]/20 to-[#B44DFF]/10 border-3 border-[#FFC83D]/40 flex items-center justify-center shrink-0"
>
<
div
className=
"w-11 h-11 flex items-center justify-center shrink-0"
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
}
}
>
<
div
className=
"w-full h-full bg-gradient-to-br from-[#FFC83D]/25 to-[#B44DFF]/15 flex items-center justify-center"
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
}
}
>
<
span
className=
"text-sm font-black text-[#FFC83D]"
>
<
span
className=
"text-sm font-black text-[#FFC83D]"
>
{
req
.
profile
.
display_name
?.[
0
]
||
req
.
profile
.
username
[
0
]
}
{
req
.
profile
.
display_name
?.[
0
]
||
req
.
profile
.
username
[
0
]
}
</
span
>
</
span
>
</
div
>
</
div
>
</
div
>
<
div
className=
"flex-1 min-w-0"
>
<
div
className=
"flex-1 min-w-0"
>
<
p
className=
"text-sm font-black truncate"
>
<
p
className=
"text-sm font-black truncate"
>
{
req
.
profile
.
display_name
||
req
.
profile
.
username
}
{
req
.
profile
.
display_name
||
req
.
profile
.
username
}
...
@@ -169,35 +199,62 @@ export function FriendsPage() {
...
@@ -169,35 +199,62 @@ export function FriendsPage() {
<
p
className=
"text-xs text-text-muted font-bold"
>
{
req
.
profile
.
elo_blitz
}
</
p
>
<
p
className=
"text-xs text-text-muted font-bold"
>
{
req
.
profile
.
elo_blitz
}
</
p
>
</
div
>
</
div
>
<
div
className=
"flex gap-2"
>
<
div
className=
"flex gap-2"
>
<
Button
size=
"sm"
variant=
"cyan"
onClick=
{
()
=>
acceptRequest
(
req
.
id
)
}
>
<
motion
.
button
whileTap=
{
{
scale
:
0.85
}
}
onClick=
{
()
=>
acceptRequest
(
req
.
id
)
}
className=
"btn-3d px-3.5 py-2 rounded-xl bg-[#00E5CC] text-background text-xs font-black"
>
قبول
قبول
</
Button
>
</
motion
.
button
>
<
Button
size=
"sm"
variant=
"coral"
onClick=
{
()
=>
rejectRequest
(
req
.
id
)
}
>
<
motion
.
button
whileTap=
{
{
scale
:
0.85
}
}
onClick=
{
()
=>
rejectRequest
(
req
.
id
)
}
className=
"btn-3d px-3.5 py-2 rounded-xl bg-[#FF5252] text-white text-xs font-black"
>
رفض
رفض
</
Button
>
</
motion
.
button
>
</
div
>
</
div
>
</
div
>
</
Card
>
))
}
))
}
</
div
>
</
div
>
</
motion
.
div
>
</
motion
.
div
>
)
}
)
}
{
/* Online Friends */
}
{
onlineFriends
.
length
>
0
&&
(
{
onlineFriends
.
length
>
0
&&
(
<
motion
.
div
className=
"flex flex-col gap-2"
variants=
{
item
}
>
<
motion
.
div
className=
"flex flex-col gap-2.5"
variants=
{
item
}
>
<
h2
className=
"text-sm font-black text-[#00E5CC]"
>
<
h2
className=
"text-sm font-black text-[#00E5CC] flex items-center gap-2"
>
<
div
className=
"w-1.5 h-4 rounded-full bg-[#00E5CC]"
/>
متصل (
{
onlineFriends
.
length
}
)
متصل (
{
onlineFriends
.
length
}
)
</
h2
>
</
h2
>
<
div
className=
"flex flex-col gap-2
"
>
<
div
className=
"flex flex-col gap-2.5
"
>
{
onlineFriends
.
map
((
friend
)
=>
(
{
onlineFriends
.
map
((
friend
)
=>
(
<
Card
key=
{
friend
.
id
}
className=
"!p-3.5 flex items-center gap-3"
>
<
div
key=
{
friend
.
id
}
className=
"game-panel !p-3 flex items-center gap-3 relative overflow-hidden"
>
<
div
className=
"relative"
>
{
/* Green accent stripe */
}
<
div
className=
"w-10 h-10 rounded-full bg-gradient-to-br from-[#00E5CC]/15 to-surface-3 border-3 border-border flex items-center justify-center"
>
<
div
className=
"absolute right-0 top-0 bottom-0 w-[4px] bg-[#4ADE80] rounded-r-full"
/>
<
div
className=
"relative mr-1"
>
<
div
className=
"w-11 h-11 flex items-center justify-center"
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
}
}
>
<
div
className=
"w-full h-full bg-gradient-to-br from-[#00E5CC]/15 to-surface-3 flex items-center justify-center"
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
}
}
>
<
span
className=
"text-sm font-black text-[#FFC83D]"
>
<
span
className=
"text-sm font-black text-[#FFC83D]"
>
{
friend
.
profile
.
display_name
?.[
0
]
||
friend
.
profile
.
username
[
0
]
}
{
friend
.
profile
.
display_name
?.[
0
]
||
friend
.
profile
.
username
[
0
]
}
</
span
>
</
span
>
</
div
>
</
div
>
<
div
className=
"absolute -bottom-0.5 -left-0.5 w-3.5 h-3.5 rounded-full bg-[#4ADE80] border-2 border-surface-1 animate-pulse"
/>
</
div
>
</
div
>
{
/* Online pulse ring */
}
<
div
className=
"absolute -bottom-0.5 -left-0.5 w-4 h-4 rounded-full bg-[#4ADE80] border-2 border-surface-1"
>
<
motion
.
div
className=
"absolute inset-0 rounded-full bg-[#4ADE80]"
animate=
{
{
scale
:
[
1
,
1.8
],
opacity
:
[
0.6
,
0
]
}
}
transition=
{
{
duration
:
1.5
,
repeat
:
Infinity
}
}
/>
</
div
>
</
div
>
<
div
className=
"flex-1 min-w-0"
>
<
div
className=
"flex-1 min-w-0"
>
<
p
className=
"text-sm font-black truncate"
>
<
p
className=
"text-sm font-black truncate"
>
{
friend
.
profile
.
display_name
||
friend
.
profile
.
username
}
{
friend
.
profile
.
display_name
||
friend
.
profile
.
username
}
...
@@ -205,34 +262,46 @@ export function FriendsPage() {
...
@@ -205,34 +262,46 @@ export function FriendsPage() {
<
p
className=
"text-xs text-text-muted font-bold"
>
{
friend
.
profile
.
elo_blitz
}
</
p
>
<
p
className=
"text-xs text-text-muted font-bold"
>
{
friend
.
profile
.
elo_blitz
}
</
p
>
</
div
>
</
div
>
<
motion
.
button
<
motion
.
button
className=
"p-2 rounded-xl bg-[#FF5252]/10 border-2 border-[#FF5252]/20
"
className=
"w-9 h-9 rounded-xl bg-[#FF5252]/10 border-2 border-[#FF5252]/25 flex items-center justify-center
"
whileTap=
{
{
scale
:
0.8
}
}
whileTap=
{
{
scale
:
0.8
}
}
onClick=
{
()
=>
removeFriend
(
friend
.
id
)
}
onClick=
{
()
=>
removeFriend
(
friend
.
id
)
}
>
>
<
UserX
size=
{
14
}
className=
"text-[#FF5252]"
/>
<
UserX
size=
{
14
}
className=
"text-[#FF5252]"
/>
</
motion
.
button
>
</
motion
.
button
>
</
Card
>
</
div
>
))
}
))
}
</
div
>
</
div
>
</
motion
.
div
>
</
motion
.
div
>
)
}
)
}
{
/* Offline Friends */
}
{
offlineFriends
.
length
>
0
&&
(
{
offlineFriends
.
length
>
0
&&
(
<
motion
.
div
className=
"flex flex-col gap-2"
variants=
{
item
}
>
<
motion
.
div
className=
"flex flex-col gap-2.5"
variants=
{
item
}
>
<
h2
className=
"text-sm font-black text-text-muted"
>
<
h2
className=
"text-sm font-black text-text-muted flex items-center gap-2"
>
<
div
className=
"w-1.5 h-4 rounded-full bg-text-muted/40"
/>
غير متصل (
{
offlineFriends
.
length
}
)
غير متصل (
{
offlineFriends
.
length
}
)
</
h2
>
</
h2
>
<
div
className=
"flex flex-col gap-2
"
>
<
div
className=
"flex flex-col gap-2.5
"
>
{
offlineFriends
.
map
((
friend
)
=>
(
{
offlineFriends
.
map
((
friend
)
=>
(
<
Card
key=
{
friend
.
id
}
className=
"!p-3.5 flex items-center gap-3 opacity-70"
>
<
div
key=
{
friend
.
id
}
className=
"game-panel !p-3 flex items-center gap-3 opacity-60 relative overflow-hidden"
>
<
div
className=
"relative"
>
{
/* Gray accent stripe */
}
<
div
className=
"w-10 h-10 rounded-full bg-surface-3 border-3 border-border flex items-center justify-center"
>
<
div
className=
"absolute right-0 top-0 bottom-0 w-[4px] bg-text-muted/30 rounded-r-full"
/>
<
div
className=
"relative mr-1"
>
<
div
className=
"w-11 h-11 flex items-center justify-center"
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
}
}
>
<
div
className=
"w-full h-full bg-surface-3 flex items-center justify-center"
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
}
}
>
<
span
className=
"text-sm font-black text-text-muted"
>
<
span
className=
"text-sm font-black text-text-muted"
>
{
friend
.
profile
.
display_name
?.[
0
]
||
friend
.
profile
.
username
[
0
]
}
{
friend
.
profile
.
display_name
?.[
0
]
||
friend
.
profile
.
username
[
0
]
}
</
span
>
</
span
>
</
div
>
</
div
>
<
div
className=
"absolute -bottom-0.5 -left-0.5 w-3.5 h-3.5 rounded-full bg-text-muted/40 border-2 border-surface-1"
/>
</
div
>
</
div
>
<
div
className=
"absolute -bottom-0.5 -left-0.5 w-4 h-4 rounded-full bg-text-muted/40 border-2 border-surface-1"
/>
</
div
>
<
div
className=
"flex-1 min-w-0"
>
<
div
className=
"flex-1 min-w-0"
>
<
p
className=
"text-sm font-bold truncate"
>
<
p
className=
"text-sm font-bold truncate"
>
{
friend
.
profile
.
display_name
||
friend
.
profile
.
username
}
{
friend
.
profile
.
display_name
||
friend
.
profile
.
username
}
...
@@ -242,40 +311,45 @@ export function FriendsPage() {
...
@@ -242,40 +311,45 @@ export function FriendsPage() {
</
p
>
</
p
>
</
div
>
</
div
>
<
motion
.
button
<
motion
.
button
className=
"p-2 rounded-xl bg-[#FF5252]/10 border-2 border-[#FF5252]/20
"
className=
"w-9 h-9 rounded-xl bg-[#FF5252]/10 border-2 border-[#FF5252]/20 flex items-center justify-center
"
whileTap=
{
{
scale
:
0.8
}
}
whileTap=
{
{
scale
:
0.8
}
}
onClick=
{
()
=>
removeFriend
(
friend
.
id
)
}
onClick=
{
()
=>
removeFriend
(
friend
.
id
)
}
>
>
<
UserX
size=
{
14
}
className=
"text-[#FF5252]"
/>
<
UserX
size=
{
14
}
className=
"text-[#FF5252]"
/>
</
motion
.
button
>
</
motion
.
button
>
</
Card
>
</
div
>
))
}
))
}
</
div
>
</
div
>
</
motion
.
div
>
</
motion
.
div
>
)
}
)
}
{
/* Empty State */
}
{
friends
.
length
===
0
&&
pendingReceived
.
length
===
0
&&
(
{
friends
.
length
===
0
&&
pendingReceived
.
length
===
0
&&
(
<
motion
.
div
<
motion
.
div
className=
"flex flex-col items-center justify-center py-16 gap-4
"
className=
"flex flex-col items-center justify-center py-16 gap-5
"
variants=
{
item
}
variants=
{
item
}
>
>
<
motion
.
div
<
motion
.
div
className=
"w-20 h-20 rounded-full bg-surface-2 border-3 border-border flex items-center justify-center"
className=
"w-24 h-24 flex items-center justify-center"
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
}
}
initial=
{
{
scale
:
0
,
rotate
:
-
15
}
}
initial=
{
{
scale
:
0
,
rotate
:
-
15
}
}
animate=
{
{
scale
:
1
,
rotate
:
0
}
}
animate=
{
{
scale
:
1
,
rotate
:
0
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
300
,
damping
:
18
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
300
,
damping
:
18
}
}
>
>
<
UserPlus
size=
{
32
}
className=
"text-text-muted"
/>
<
div
className=
"w-full h-full bg-gradient-to-br from-surface-2 to-surface-3 border-3 border-border flex items-center justify-center"
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
}
}
>
<
Shield
size=
{
36
}
className=
"text-text-muted"
/>
</
div
>
</
motion
.
div
>
</
motion
.
div
>
<
div
className=
"text-center"
>
<
div
className=
"text-center"
>
<
p
className=
"text-text-muted font-black text-base"
>
لا يوجد اصدقاء بعد
</
p
>
<
p
className=
"text-text-muted font-black text-base"
>
لا يوجد اصدقاء
</
p
>
<
p
className=
"text-text-muted text-xs font-bold mt-1
"
>
ابحث عن لاعبين لاضافتهم
</
p
>
<
p
className=
"text-text-muted text-xs font-bold mt-1.5
"
>
ابحث عن لاعبين لاضافتهم
</
p
>
</
div
>
</
div
>
</
motion
.
div
>
</
motion
.
div
>
)
}
)
}
</
motion
.
div
>
</
motion
.
div
>
)
}
)
}
</
div
>
</
PageTransition
>
</
PageTransition
>
)
)
}
}
src/pages/HomePage.tsx
View file @
4d37e0d3
...
@@ -4,7 +4,6 @@ import { Play, TrendingUp, Swords, Flame, Bot, Users, Lightbulb, Crown } from 'l
...
@@ -4,7 +4,6 @@ import { Play, TrendingUp, Swords, Flame, Bot, Users, Lightbulb, Crown } from 'l
import
{
useNavigate
}
from
'react-router-dom'
import
{
useNavigate
}
from
'react-router-dom'
import
{
useAuthStore
}
from
'../stores/authStore'
import
{
useAuthStore
}
from
'../stores/authStore'
import
{
PageTransition
}
from
'../components/layout/PageTransition'
import
{
PageTransition
}
from
'../components/layout/PageTransition'
import
{
Card
}
from
'../components/ui/Card'
import
{
DailyRewardModal
}
from
'../components/DailyRewardModal'
import
{
DailyRewardModal
}
from
'../components/DailyRewardModal'
import
{
useDailyReward
}
from
'../hooks/useDailyReward'
import
{
useDailyReward
}
from
'../hooks/useDailyReward'
import
{
playSound
}
from
'../lib/sounds'
import
{
playSound
}
from
'../lib/sounds'
...
@@ -22,13 +21,13 @@ const dailyTips = [
...
@@ -22,13 +21,13 @@ const dailyTips = [
const
stagger
=
{
const
stagger
=
{
hidden
:
{},
hidden
:
{},
show
:
{
show
:
{
transition
:
{
staggerChildren
:
0.0
6
},
transition
:
{
staggerChildren
:
0.0
8
},
},
},
}
}
const
fadeUp
=
{
const
fadeUp
=
{
hidden
:
{
opacity
:
0
,
y
:
2
0
},
hidden
:
{
opacity
:
0
,
y
:
2
4
},
show
:
{
opacity
:
1
,
y
:
0
,
transition
:
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
22
}
},
show
:
{
opacity
:
1
,
y
:
0
,
transition
:
{
type
:
'spring'
,
stiffness
:
400
,
damping
:
24
}
},
}
}
export
function
HomePage
()
{
export
function
HomePage
()
{
...
@@ -60,155 +59,266 @@ export function HomePage() {
...
@@ -60,155 +59,266 @@ export function HomePage() {
variants=
{
stagger
}
variants=
{
stagger
}
initial=
"hidden"
initial=
"hidden"
animate=
"show"
animate=
"show"
className=
"flex flex-col gap-
6
"
className=
"flex flex-col gap-
7
"
>
>
{
/* === 1. PLAYER NAMEPLATE — Angled Banner === */
}
{
profile
&&
(
{
profile
&&
(
<
motion
.
div
<
motion
.
div
variants=
{
fadeUp
}
className=
"relative"
>
variants=
{
fadeUp
}
<
div
className=
"flex items-center gap-4"
className=
"relative flex items-center gap-4 px-5 py-4"
style=
{
{
clipPath
:
'polygon(3% 0%, 100% 0%, 97% 50%, 100% 100%, 3% 100%, 0% 50%)'
,
background
:
'linear-gradient(135deg, rgba(255,200,60,0.12) 0%, rgba(180,77,255,0.12) 100%)'
,
}
}
>
{
/* Decorative left accent */
}
<
div
className=
"absolute right-0 top-0 bottom-0 w-[3px]"
style=
{
{
background
:
'linear-gradient(to bottom, #FFC83D, #B44DFF)'
}
}
/>
{
/* Hexagonal Avatar Shield */
}
<
div
className=
"relative flex-shrink-0"
>
<
div
className=
"w-14 h-14 flex items-center justify-center border-[3px] border-gold"
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
,
background
:
'linear-gradient(135deg, rgba(255,200,60,0.2), rgba(255,200,60,0.05))'
,
}
}
>
>
<
div
className=
"w-16 h-16 rounded-2xl bg-gradient-to-br from-gold/25 to-purple/25 border-3 border-gold/40 flex items-center justify-center"
>
<
span
className=
"text-xl font-black text-gold"
>
<
span
className=
"text-2xl font-black text-gold"
>
{
profile
.
display_name
?.
charAt
(
0
)
||
'L'
}
{
profile
.
display_name
?.
charAt
(
0
)
||
'L'
}
</
span
>
</
span
>
</
div
>
</
div
>
<
div
>
</
div
>
<
h2
className=
"text-2xl font-black"
>
اهلا،
{
profile
.
display_name
}
</
h2
>
{
/* Name + Level */
}
<
div
className=
"flex items-center gap-2 mt-1"
>
<
div
className=
"flex-1"
>
<
span
className=
"px-2.5 py-0.5 rounded-lg bg-gold/15 border-2 border-gold/30 text-[11px] font-bold text-gold"
>
<
h2
className=
"text-xl font-black text-text-primary leading-tight"
>
{
profile
.
display_name
}
</
h2
>
<
span
className=
"inline-block mt-1 px-2 py-0.5 rounded-md bg-purple/20 border border-purple/40 text-[10px] font-bold text-purple"
>
المستوى
{
profile
.
level
}
المستوى
{
profile
.
level
}
</
span
>
</
span
>
</
div
>
</
div
>
{
/* Decorative left accent (RTL mirror) */
}
<
div
className=
"absolute left-0 top-0 bottom-0 w-[3px]"
style=
{
{
background
:
'linear-gradient(to bottom, #B44DFF, #FFC83D)'
}
}
/>
</
div
>
</
div
>
</
motion
.
div
>
</
motion
.
div
>
)
}
)
}
{
/* === 2. PLAY BUTTON — The Hero Crest === */
}
<
motion
.
div
variants=
{
fadeUp
}
className=
"flex justify-center"
>
<
motion
.
div
variants=
{
fadeUp
}
className=
"flex justify-center"
>
<
motion
.
button
<
motion
.
button
onClick=
{
()
=>
{
onClick=
{
()
=>
{
playSound
(
'click'
)
playSound
(
'click'
)
navigate
(
'/play'
)
navigate
(
'/play'
)
}
}
}
}
className=
"relative w-[85%] py-10 rounded-[22px] bg-gradient-to-b from-gold-light to-gold overflow-hidden border-b-4 border-gold-muted shadow-2xl shadow-gold/40"
className=
"relative w-[85%] cursor-pointer"
whileTap=
{
{
scale
:
0.93
,
y
:
3
}
}
whileTap=
{
{
y
:
4
,
scale
:
0.97
}
}
whileHover=
{
{
scale
:
1.03
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
600
,
damping
:
20
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
600
,
damping
:
20
}
}
>
>
<
motion
.
div
{
/* Shadow layer (3D depth) */
}
className=
"absolute inset-0 rounded-[22px] border-[3px] border-gold/60 animate-pulse-glow"
<
div
className=
"absolute inset-0 top-[6px]"
style=
{
{
clipPath
:
'polygon(8% 0%, 92% 0%, 100% 50%, 92% 100%, 8% 100%, 0% 50%)'
,
background
:
'#9B6B1A'
,
}
}
/>
/>
{
/* Main body */
}
<
motion
.
div
className=
"relative overflow-hidden py-10"
style=
{
{
clipPath
:
'polygon(8% 0%, 92% 0%, 100% 50%, 92% 100%, 8% 100%, 0% 50%)'
,
background
:
'linear-gradient(180deg, #FFE066 0%, #FFC83D 60%, #C9972E 100%)'
,
borderBottom
:
'4px solid #9B6B1A'
,
}
}
animate=
{
{
scale
:
[
1
,
1.015
,
1
]
}
}
transition=
{
{
duration
:
2.2
,
repeat
:
Infinity
,
ease
:
'easeInOut'
}
}
>
{
/* Animated shimmer */
}
<
motion
.
div
<
motion
.
div
className=
"absolute inset-0 bg-gradient-to-r from-transparent via-white/25 to-transparent"
className=
"absolute inset-0"
animate=
{
{
x
:
[
'-200%'
,
'200%'
]
}
}
style=
{
{
transition=
{
{
duration
:
2.5
,
repeat
:
Infinity
,
ease
:
'linear'
}
}
background
:
'linear-gradient(105deg, transparent 30%, rgba(255,255,255,0.3) 50%, transparent 70%)'
,
}
}
animate=
{
{
x
:
[
'-100%'
,
'200%'
]
}
}
transition=
{
{
duration
:
2.8
,
repeat
:
Infinity
,
ease
:
'linear'
}
}
/>
/>
<
div
className=
"relative flex flex-col items-center gap-3"
>
{
/* Content */
}
<
div
className=
"relative flex flex-col items-center gap-2"
>
<
motion
.
div
<
motion
.
div
animate=
{
{
scale
:
[
1
,
1.12
,
1
]
}
}
animate=
{
{
scale
:
[
1
,
1.1
,
1
]
}
}
transition=
{
{
duration
:
1.8
,
repeat
:
Infinity
,
ease
:
'easeInOut'
}
}
transition=
{
{
duration
:
1.6
,
repeat
:
Infinity
,
ease
:
'easeInOut'
}
}
>
>
<
Play
size=
{
48
}
className=
"text-background"
fill=
"currentColor"
/>
<
Play
size=
{
52
}
className=
"text-background"
fill=
"currentColor"
/>
</
motion
.
div
>
</
motion
.
div
>
<
span
className=
"text-3xl font-black text-background uppercase tracking-wide"
>
العب الان
</
span
>
<
span
className=
"text-3xl font-black text-background tracking-wide"
>
العب الان
</
span
>
</
div
>
</
div
>
</
motion
.
div
>
</
motion
.
button
>
</
motion
.
button
>
</
motion
.
div
>
</
motion
.
div
>
{
/* === 3. STATS AS SHIELD BADGES === */
}
{
profile
&&
(
{
profile
&&
(
<
motion
.
div
variants=
{
fadeUp
}
className=
"grid grid-cols-3 gap-3"
>
<
motion
.
div
variants=
{
fadeUp
}
className=
"relative flex justify-center items-center gap-4 py-3"
>
<
StatCard
{
/* Decorative connecting line */
}
icon=
{
<
TrendingUp
size=
{
22
}
className=
"text-gold"
/>
}
<
div
className=
"absolute top-1/2 left-[15%] right-[15%] h-[2px] bg-gradient-to-r from-transparent via-border to-transparent -translate-y-1/2"
/>
<
StatBadge
icon=
{
<
TrendingUp
size=
{
18
}
className=
"text-gold"
/>
}
value=
{
profile
.
elo_blitz
}
value=
{
profile
.
elo_blitz
}
label=
"تقييم"
label=
"تقييم"
accent=
"gold"
gradient=
"linear-gradient(135deg, rgba(255,200,60,0.15), rgba(255,200,60,0.05))"
borderColor=
"#FFC83D"
/>
/>
<
Stat
Card
<
Stat
Badge
icon=
{
<
Swords
size=
{
22
}
className=
"text-cyan"
/>
}
icon=
{
<
Swords
size=
{
18
}
className=
"text-cyan"
/>
}
value=
{
profile
.
total_games_played
}
value=
{
profile
.
total_games_played
}
label=
"مباراة"
label=
"مباراة"
accent=
"cyan"
gradient=
"linear-gradient(135deg, rgba(0,229,204,0.15), rgba(0,229,204,0.05))"
borderColor=
"#00E5CC"
isCenter
/>
/>
<
Stat
Card
<
Stat
Badge
icon=
{
<
Flame
size=
{
22
}
className=
"text-coral"
/>
}
icon=
{
<
Flame
size=
{
18
}
className=
"text-coral"
/>
}
value=
{
profile
.
win_streak
}
value=
{
profile
.
win_streak
}
label=
"سلسلة فوز"
label=
"سلسلة فوز"
accent=
"coral"
gradient=
"linear-gradient(135deg, rgba(255,82,82,0.15), rgba(255,82,82,0.05))"
borderColor=
"#FF5252"
/>
/>
</
motion
.
div
>
</
motion
.
div
>
)
}
)
}
<
motion
.
div
variants=
{
fadeUp
}
className=
"grid grid-cols-2 gap-3"
>
{
/* === 4. QUICK ACTIONS — Chunky 3D Game Tiles === */
}
<
Card
<
motion
.
div
variants=
{
fadeUp
}
className=
"grid grid-cols-2 gap-4 px-2"
>
variant=
"default"
<
motion
.
button
className=
"flex flex-col items-center gap-3 !p-5"
className=
"relative cursor-pointer"
style=
{
{
transform
:
'rotate(-1deg)'
}
}
whileTap=
{
{
y
:
4
,
boxShadow
:
'none'
}
}
onClick=
{
()
=>
{
onClick=
{
()
=>
{
playSound
(
'click'
)
playSound
(
'click'
)
navigate
(
'/bot-select'
)
navigate
(
'/bot-select'
)
}
}
}
}
>
>
<
div
className=
"w-14 h-14 rounded-2xl bg-purple/15 border-3 border-purple/30 flex items-center justify-center"
>
<
div
className=
"relative flex flex-col items-center gap-3 p-5 rounded-2xl bg-surface-1 border-3 border-border"
<
Bot
size=
{
28
}
className=
"text-purple"
/>
style=
{
{
boxShadow
:
'0 6px 0 0 rgba(61,69,112,0.6)'
}
}
>
<
div
className=
"w-12 h-12 rounded-full flex items-center justify-center"
style=
{
{
background
:
'linear-gradient(135deg, rgba(180,77,255,0.25), rgba(180,77,255,0.08))'
}
}
>
<
Bot
size=
{
26
}
className=
"text-purple"
/>
</
div
>
</
div
>
<
div
className=
"text-center"
>
<
div
className=
"text-center"
>
<
p
className=
"text-sm font-black"
>
العب ضد روبوت
</
p
>
<
p
className=
"text-sm font-black text-text-primary"
>
العب ضد روبوت
</
p
>
<
p
className=
"text-[10px] text-text-muted mt-1"
>
تدريب وتحسين
</
p
>
<
p
className=
"text-[10px] text-text-muted mt-0.5"
>
تدريب وتحسين
</
p
>
</
div
>
</
div
>
</
div
>
</
Card
>
</
motion
.
button
>
<
Card
<
motion
.
button
variant=
"default"
className=
"relative cursor-pointer"
className=
"flex flex-col items-center gap-3 !p-5"
style=
{
{
transform
:
'rotate(1deg)'
}
}
whileTap=
{
{
y
:
4
,
boxShadow
:
'none'
}
}
onClick=
{
()
=>
{
onClick=
{
()
=>
{
playSound
(
'click'
)
playSound
(
'click'
)
navigate
(
'/friends'
)
navigate
(
'/friends'
)
}
}
}
}
>
>
<
div
className=
"w-14 h-14 rounded-2xl bg-gold/15 border-3 border-gold/30 flex items-center justify-center"
>
<
div
className=
"relative flex flex-col items-center gap-3 p-5 rounded-2xl bg-surface-1 border-3 border-border"
<
Users
size=
{
28
}
className=
"text-gold"
/>
style=
{
{
boxShadow
:
'0 6px 0 0 rgba(61,69,112,0.6)'
}
}
>
<
div
className=
"w-12 h-12 rounded-full flex items-center justify-center"
style=
{
{
background
:
'linear-gradient(135deg, rgba(255,200,60,0.25), rgba(255,200,60,0.08))'
}
}
>
<
Users
size=
{
26
}
className=
"text-gold"
/>
</
div
>
</
div
>
<
div
className=
"text-center"
>
<
div
className=
"text-center"
>
<
p
className=
"text-sm font-black"
>
تحدى صديق
</
p
>
<
p
className=
"text-sm font-black text-text-primary"
>
تحدى صديق
</
p
>
<
p
className=
"text-[10px] text-text-muted mt-1"
>
ارسل دعوة
</
p
>
<
p
className=
"text-[10px] text-text-muted mt-0.5"
>
ارسل دعوة
</
p
>
</
div
>
</
div
>
</
div
>
</
Card
>
</
motion
.
button
>
</
motion
.
div
>
</
motion
.
div
>
<
motion
.
div
variants=
{
fadeUp
}
>
{
/* === 5. DAILY TIP — Parchment/Scroll Style === */
}
<
Card
variant=
"gold"
className=
"relative overflow-hidden"
>
<
motion
.
div
variants=
{
fadeUp
}
className=
"relative overflow-hidden rounded-2xl border-2 border-gold/20 p-5"
<
div
className=
"absolute top-0 right-0 w-28 h-28 bg-gradient-to-bl from-gold/10 to-transparent rounded-bl-full"
/>
style=
{
{
<
div
className=
"flex items-start gap-4"
>
background
:
'linear-gradient(160deg, rgba(30,35,64,1) 0%, rgba(40,35,30,0.4) 100%)'
,
<
div
className=
"w-12 h-12 rounded-xl bg-gold/15 border-3 border-gold/30 flex items-center justify-center flex-shrink-0"
>
}
}
<
Lightbulb
size=
{
22
}
className=
"text-gold"
/>
>
{
/* Watermark crown */
}
<
div
className=
"absolute bottom-2 left-3 opacity-[0.06]"
>
<
Crown
size=
{
64
}
className=
"text-gold"
/>
</
div
>
{
/* Ribbon header */
}
<
div
className=
"inline-block mb-3"
>
<
div
className=
"px-4 py-1 text-[11px] font-black text-background"
style=
{
{
clipPath
:
'polygon(0% 0%, 92% 0%, 100% 50%, 92% 100%, 0% 100%, 4% 50%)'
,
background
:
'linear-gradient(90deg, #FFC83D, #FFE066)'
,
}
}
>
نصيحة اليوم
</
div
>
</
div
>
<
div
className=
"flex-1 pt-0.5"
>
<
div
className=
"flex items-center gap-2 mb-2"
>
<
h4
className=
"text-xs font-black text-gold uppercase"
>
نصيحة اليوم
</
h4
>
<
Crown
size=
{
12
}
className=
"text-gold/50"
/>
</
div
>
</
div
>
<
p
className=
"text-[12px] text-text-secondary leading-[1.8]"
>
{
todayTip
}
</
p
>
{
/* Tip content */
}
<
div
className=
"flex items-start gap-3"
>
<
div
className=
"w-9 h-9 flex-shrink-0 rounded-lg bg-gold/10 border border-gold/25 flex items-center justify-center"
>
<
Lightbulb
size=
{
18
}
className=
"text-gold"
/>
</
div
>
</
div
>
<
p
className=
"text-[12px] text-text-secondary leading-[2] pt-1.5"
>
{
todayTip
}
</
p
>
</
div
>
</
div
>
</
Card
>
</
motion
.
div
>
</
motion
.
div
>
</
motion
.
div
>
</
motion
.
div
>
</
PageTransition
>
</
PageTransition
>
)
)
}
}
function
StatCard
({
icon
,
value
,
label
,
accent
}:
{
icon
:
React
.
ReactNode
;
value
:
number
;
label
:
string
;
accent
:
string
})
{
/* --- Hexagonal Stat Badge --- */
const
borderMap
:
Record
<
string
,
string
>
=
{
function
StatBadge
({
cyan
:
'border-t-cyan'
,
icon
,
gold
:
'border-t-gold'
,
value
,
coral
:
'border-t-coral'
,
label
,
}
gradient
,
borderColor
,
isCenter
=
false
,
}:
{
icon
:
React
.
ReactNode
value
:
number
label
:
string
gradient
:
string
borderColor
:
string
isCenter
?:
boolean
})
{
return
(
return
(
<
div
<
div
className=
{
`flex flex-col items-center gap-2 p-4 rounded-2xl bg-surface-1 border-3 border-border ${borderMap[accent] || ''}`
}
className=
"relative flex flex-col items-center justify-center z-10"
style=
{
{
borderTopWidth
:
'4px'
}
}
style=
{
{
transform
:
isCenter
?
'scale(1.08)'
:
'scale(1)'
}
}
>
<
div
className=
"w-20 h-20 flex flex-col items-center justify-center border-[2.5px]"
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
,
background
:
gradient
,
borderColor
:
borderColor
,
}
}
>
>
{
icon
}
{
icon
}
<
span
className=
"text-2xl font-black"
>
{
value
}
</
span
>
<
span
className=
"text-lg font-black text-text-primary mt-0.5"
>
{
value
}
</
span
>
<
span
className=
"text-[10px] text-text-muted font-bold"
>
{
label
}
</
span
>
</
div
>
<
span
className=
"text-[9px] text-text-muted font-bold mt-1"
>
{
label
}
</
span
>
</
div
>
</
div
>
)
)
}
}
src/pages/LeaderboardPage.tsx
View file @
4d37e0d3
import
{
useState
}
from
'react'
import
{
useState
}
from
'react'
import
{
motion
}
from
'framer-motion'
import
{
motion
}
from
'framer-motion'
import
{
Crown
,
Medal
,
Trophy
}
from
'lucide-react'
import
{
Crown
,
Medal
,
Trophy
,
Zap
,
Clock
,
Timer
,
Hourglass
}
from
'lucide-react'
import
{
PageTransition
}
from
'../components/layout/PageTransition'
import
{
PageTransition
}
from
'../components/layout/PageTransition'
import
{
useAuthStore
}
from
'../stores/authStore'
import
{
useAuthStore
}
from
'../stores/authStore'
import
{
useLeaderboard
}
from
'../hooks/useLeaderboard'
import
{
useLeaderboard
}
from
'../hooks/useLeaderboard'
...
@@ -8,11 +8,11 @@ import { useLeaderboard } from '../hooks/useLeaderboard'
...
@@ -8,11 +8,11 @@ import { useLeaderboard } from '../hooks/useLeaderboard'
type
TimeControlType
=
'bullet'
|
'blitz'
|
'rapid'
|
'classical'
type
TimeControlType
=
'bullet'
|
'blitz'
|
'rapid'
|
'classical'
type
Period
=
'weekly'
|
'monthly'
|
'all_time'
type
Period
=
'weekly'
|
'monthly'
|
'all_time'
const
TIME_CONTROLS
:
{
label
:
string
;
value
:
TimeControlType
}[]
=
[
const
TIME_CONTROLS
:
{
label
:
string
;
value
:
TimeControlType
;
icon
:
React
.
ReactNode
}[]
=
[
{
label
:
'رصاصة'
,
value
:
'bullet'
},
{
label
:
'رصاصة'
,
value
:
'bullet'
,
icon
:
<
Zap
size=
{
14
}
/>
},
{
label
:
'خاطف'
,
value
:
'blitz'
},
{
label
:
'خاطف'
,
value
:
'blitz'
,
icon
:
<
Timer
size=
{
14
}
/>
},
{
label
:
'سريع'
,
value
:
'rapid'
},
{
label
:
'سريع'
,
value
:
'rapid'
,
icon
:
<
Clock
size=
{
14
}
/>
},
{
label
:
'كلاسيكي'
,
value
:
'classical'
},
{
label
:
'كلاسيكي'
,
value
:
'classical'
,
icon
:
<
Hourglass
size=
{
14
}
/>
},
]
]
const
PERIODS
:
{
label
:
string
;
value
:
Period
}[]
=
[
const
PERIODS
:
{
label
:
string
;
value
:
Period
}[]
=
[
...
@@ -21,23 +21,45 @@ const PERIODS: { label: string; value: Period }[] = [
...
@@ -21,23 +21,45 @@ const PERIODS: { label: string; value: Period }[] = [
{
label
:
'الكل'
,
value
:
'all_time'
},
{
label
:
'الكل'
,
value
:
'all_time'
},
]
]
function
Avatar
({
name
,
url
,
size
=
44
}:
{
name
:
string
;
url
?:
string
|
null
;
size
?:
number
})
{
const
HEX_CLIP
=
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
if
(
url
)
{
function
HexAvatar
({
name
,
url
,
size
=
44
}:
{
name
:
string
;
url
?:
string
|
null
;
size
?:
number
})
{
return
(
return
(
<
img
<
div
src=
{
url
}
className=
"relative flex items-center justify-center"
alt=
{
name
}
className=
"rounded-full object-cover border-2 border-border"
style=
{
{
width
:
size
,
height
:
size
}
}
style=
{
{
width
:
size
,
height
:
size
}
}
>
{
/* Gold hex border */
}
<
div
className=
"absolute inset-0"
style=
{
{
clipPath
:
HEX_CLIP
,
background
:
'linear-gradient(135deg, #FFC83D, #B44DFF)'
,
}
}
/>
/>
)
{
/* Inner content */
}
}
return
(
<
div
<
div
className=
"rounded-full bg-gradient-to-br from-surface-3 to-surface-2 flex items-center justify-center font-black text-text-secondary border-2 border-border"
className=
"flex items-center justify-center"
style=
{
{
width
:
size
,
height
:
size
,
fontSize
:
size
*
0.38
}
}
style=
{
{
width
:
size
-
4
,
height
:
size
-
4
,
clipPath
:
HEX_CLIP
,
background
:
url
?
'transparent'
:
'linear-gradient(135deg, var(--color-surface-2), var(--color-surface-3))'
,
}
}
>
>
{
url
?
(
<
img
src=
{
url
}
alt=
{
name
}
className=
"w-full h-full object-cover"
style=
{
{
clipPath
:
HEX_CLIP
}
}
/>
)
:
(
<
span
className=
"font-black text-text-secondary"
style=
{
{
fontSize
:
size
*
0.35
}
}
>
{
name
?.
charAt
(
0
)
||
'?'
}
{
name
?.
charAt
(
0
)
||
'?'
}
</
span
>
)
}
</
div
>
</
div
>
</
div
>
)
)
}
}
...
@@ -53,47 +75,97 @@ export function LeaderboardPage() {
...
@@ -53,47 +75,97 @@ export function LeaderboardPage() {
return
(
return
(
<
PageTransition
className=
"flex flex-col gap-5"
>
<
PageTransition
className=
"flex flex-col gap-5"
>
<
div
className=
"flex items-center gap-2.5"
>
{
/* === PAGE HEADER === */
}
<
Trophy
size=
{
24
}
className=
"text-[#FFC83D]"
/>
<
motion
.
div
<
h1
className=
"text-2xl font-black"
>
لوحة المتصدرين
</
h1
>
className=
"relative flex items-center justify-center py-3"
</
div
>
style=
{
{
clipPath
:
'polygon(5% 0%, 95% 0%, 100% 100%, 0% 100%)'
,
background
:
'linear-gradient(to bottom, rgba(255,200,60,0.15), transparent)'
,
borderBottom
:
'2px solid rgba(255,200,60,0.2)'
,
}
}
initial=
{
{
opacity
:
0
,
y
:
-
10
}
}
animate=
{
{
opacity
:
1
,
y
:
0
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
300
,
damping
:
25
}
}
>
<
Trophy
size=
{
28
}
className=
"text-[#FFC83D] ml-2.5"
/>
<
h1
className=
"text-2xl font-black tracking-tight"
>
لوحة المتصدرين
</
h1
>
</
motion
.
div
>
<
div
className=
"flex gap-2 overflow-x-auto no-scrollbar pb-1"
>
{
/* === FILTER TABS (Trapezoidal) === */
}
{
TIME_CONTROLS
.
map
((
tc
)
=>
(
<
motion
.
div
className=
"flex gap-2 overflow-x-auto no-scrollbar pb-1"
initial=
{
{
opacity
:
0
,
x
:
-
10
}
}
animate=
{
{
opacity
:
1
,
x
:
0
}
}
transition=
{
{
delay
:
0.1
}
}
>
{
TIME_CONTROLS
.
map
((
tc
)
=>
{
const
isActive
=
timeControl
===
tc
.
value
return
(
<
motion
.
button
<
motion
.
button
key=
{
tc
.
value
}
key=
{
tc
.
value
}
whileTap=
{
{
scale
:
0.93
}
}
whileTap=
{
{
scale
:
0.93
}
}
onClick=
{
()
=>
setTimeControl
(
tc
.
value
)
}
onClick=
{
()
=>
setTimeControl
(
tc
.
value
)
}
className=
{
`px-4 py-2 rounded-xl text-xs font-bold whitespace-nowrap border-2 transition-colors ${
className=
"relative flex items-center gap-1.5 px-4 py-2.5 text-xs font-black whitespace-nowrap transition-all"
timeControl === tc.value
style=
{
{
? 'bg-[#FFC83D] border-[#FFC83D] text-background font-black'
clipPath
:
'polygon(8% 0%, 92% 0%, 100% 100%, 0% 100%)'
,
: 'bg-surface-2 text-text-muted border-border hover:border-[#FFC83D]/40'
background
:
isActive
}`
}
?
'linear-gradient(to bottom, #FFC83D, #E0A800)'
:
'var(--color-surface-2)'
,
color
:
isActive
?
'#0B0E1A'
:
'var(--color-text-muted)'
,
boxShadow
:
isActive
?
'0 4px 12px rgba(255,200,60,0.35), inset 0 1px 0 rgba(255,255,255,0.2)'
:
'inset 0 2px 4px rgba(0,0,0,0.3)'
,
minWidth
:
72
,
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
20
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
20
}
}
>
>
{
tc
.
icon
}
{
tc
.
label
}
{
tc
.
label
}
{
isActive
&&
(
<
motion
.
div
className=
"absolute -bottom-1.5 left-1/2 -translate-x-1/2 w-0 h-0"
style=
{
{
borderLeft
:
'5px solid transparent'
,
borderRight
:
'5px solid transparent'
,
borderTop
:
'5px solid #FFC83D'
,
}
}
layoutId=
"tabIndicator"
/>
)
}
</
motion
.
button
>
</
motion
.
button
>
))
}
)
</
div
>
})
}
</
motion
.
div
>
<
div
className=
"flex gap-2 overflow-x-auto no-scrollbar"
>
{
/* === PERIOD SELECTOR === */
}
{
PERIODS
.
map
((
p
)
=>
(
<
motion
.
div
className=
"flex gap-2"
initial=
{
{
opacity
:
0
}
}
animate=
{
{
opacity
:
1
}
}
transition=
{
{
delay
:
0.15
}
}
>
{
PERIODS
.
map
((
p
)
=>
{
const
isActive
=
period
===
p
.
value
return
(
<
motion
.
button
<
motion
.
button
key=
{
p
.
value
}
key=
{
p
.
value
}
whileTap=
{
{
scale
:
0.93
}
}
whileTap=
{
{
scale
:
0.93
}
}
onClick=
{
()
=>
setPeriod
(
p
.
value
)
}
onClick=
{
()
=>
setPeriod
(
p
.
value
)
}
className=
{
`px-3.5 py-1.5 rounded-xl text-[11px] font-bold whitespace-nowrap border-2 transition-colors ${
className=
"px-4 py-1.5 rounded-full text-[11px] font-bold whitespace-nowrap transition-all"
period === p.value
style=
{
{
? 'bg-[#B44DFF]/15 text-[#B44DFF] border-[#B44DFF]/50 font-black'
background
:
isActive
?
'rgba(180,77,255,0.15)'
:
'var(--color-surface-1)'
,
: 'bg-surface-1 text-text-muted border-border hover:border-[#B44DFF]/30'
color
:
isActive
?
'#B44DFF'
:
'var(--color-text-muted)'
,
}`
}
border
:
isActive
?
'2px solid rgba(180,77,255,0.5)'
:
'2px solid var(--color-border)'
,
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
20
}
}
boxShadow
:
isActive
?
'none'
:
'inset 0 1px 3px rgba(0,0,0,0.2)'
,
}
}
>
>
{
p
.
label
}
{
p
.
label
}
</
motion
.
button
>
</
motion
.
button
>
))
}
)
</
div
>
})
}
</
motion
.
div
>
{
/* === CONTENT === */
}
{
loading
?
(
{
loading
?
(
<
div
className=
"flex flex-col items-center py-16"
>
<
div
className=
"flex flex-col items-center py-16"
>
<
motion
.
div
<
motion
.
div
...
@@ -105,7 +177,12 @@ export function LeaderboardPage() {
...
@@ -105,7 +177,12 @@ export function LeaderboardPage() {
)
:
entries
.
length
===
0
?
(
)
:
entries
.
length
===
0
?
(
<
div
className=
"flex flex-col items-center justify-center py-16 gap-4"
>
<
div
className=
"flex flex-col items-center justify-center py-16 gap-4"
>
<
motion
.
div
<
motion
.
div
className=
"w-20 h-20 rounded-2xl bg-surface-2 border-3 border-[#FFC83D]/30 flex items-center justify-center"
className=
"w-20 h-20 flex items-center justify-center"
style=
{
{
clipPath
:
HEX_CLIP
,
background
:
'linear-gradient(135deg, rgba(255,200,60,0.1), var(--color-surface-2))'
,
border
:
'3px solid rgba(255,200,60,0.3)'
,
}
}
initial=
{
{
scale
:
0
,
rotate
:
-
10
}
}
initial=
{
{
scale
:
0
,
rotate
:
-
10
}
}
animate=
{
{
scale
:
1
,
rotate
:
0
}
}
animate=
{
{
scale
:
1
,
rotate
:
0
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
300
,
damping
:
18
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
300
,
damping
:
18
}
}
...
@@ -117,41 +194,108 @@ export function LeaderboardPage() {
...
@@ -117,41 +194,108 @@ export function LeaderboardPage() {
</
div
>
</
div
>
)
:
(
)
:
(
<>
<>
{
/* === THE PODIUM === */
}
{
top3
.
length
>
0
&&
<
Podium
entries=
{
top3
}
currentUserId=
{
user
?.
id
}
/>
}
{
top3
.
length
>
0
&&
<
Podium
entries=
{
top3
}
currentUserId=
{
user
?.
id
}
/>
}
<
div
className=
"flex flex-col gap-2.5"
>
{
/* === PLAYER LIST === */
}
{
rest
.
map
((
entry
,
i
)
=>
(
{
rest
.
length
>
0
&&
(
<
div
className=
"flex flex-col gap-0"
>
{
rest
.
map
((
entry
,
i
)
=>
{
const
isMe
=
entry
.
player_id
===
user
?.
id
const
rankColor
=
i
<
7
?
getRankColor
(
entry
.
rank
)
:
'var(--color-text-muted)'
return
(
<
div
key=
{
entry
.
player_id
}
>
<
motion
.
div
<
motion
.
div
key=
{
entry
.
player_id
}
initial=
{
{
opacity
:
0
,
x
:
-
12
}
}
initial=
{
{
opacity
:
0
,
x
:
-
12
}
}
animate=
{
{
opacity
:
1
,
x
:
0
}
}
animate=
{
{
opacity
:
1
,
x
:
0
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
400
,
damping
:
24
,
delay
:
0.3
+
i
*
0.04
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
400
,
damping
:
24
,
delay
:
0.3
+
i
*
0.03
}
}
className=
{
`flex items-center gap-3 p-3.5 rounded-2xl bg-surface-1 border-3 ${
className=
"flex items-center gap-3 py-3 px-3"
entry.player_id === user?.id
style=
{
{
? 'border-[#FFC83D]/60 bg-[#FFC83D]/10'
borderRadius
:
14
,
: 'border-border'
border
:
isMe
?
'2px solid rgba(255,200,60,0.5)'
:
'2px solid transparent'
,
}`
}
background
:
isMe
?
'rgba(255,200,60,0.06)'
:
'transparent'
,
>
boxShadow
:
isMe
?
'0 0 16px rgba(255,200,60,0.12)'
:
'none'
,
<
span
className=
"w-8 text-center text-sm font-black text-text-muted"
>
}
}
{
entry
.
rank
}
>
</
span
>
{
/* Rank shield */
}
<
Avatar
name=
{
entry
.
display_name
}
url=
{
entry
.
avatar_url
}
size=
{
38
}
/>
{
entry
.
rank
<=
10
?
(
<
div
className=
"flex items-center justify-center flex-shrink-0"
style=
{
{
width
:
24
,
height
:
28
,
clipPath
:
'polygon(50% 0%, 100% 20%, 100% 80%, 50% 100%, 0% 80%, 0% 20%)'
,
background
:
`linear-gradient(to bottom, ${rankColor}30, ${rankColor}10)`
,
border
:
`2px solid ${rankColor}`
,
}
}
>
<
span
className=
"text-[10px] font-black"
style=
{
{
color
:
rankColor
}
}
>
{
entry
.
rank
}
</
span
>
</
div
>
)
:
(
<
div
className=
"flex items-center justify-center flex-shrink-0 rounded-full"
style=
{
{
width
:
24
,
height
:
24
,
background
:
'var(--color-surface-3)'
,
border
:
'2px solid var(--color-border)'
,
}
}
>
<
span
className=
"text-[10px] font-bold text-text-muted"
>
{
entry
.
rank
}
</
span
>
</
div
>
)
}
{
/* Hex avatar */
}
<
HexAvatar
name=
{
entry
.
display_name
}
url=
{
entry
.
avatar_url
}
size=
{
36
}
/>
{
/* Name + games */
}
<
div
className=
"flex-1 min-w-0"
>
<
div
className=
"flex-1 min-w-0"
>
<
p
className=
"text-sm font-black truncate"
>
{
entry
.
display_name
}
</
p
>
<
p
className=
"text-sm font-black truncate"
>
{
entry
.
display_name
}
</
p
>
<
p
className=
"text-[10px] text-text-muted font-bold"
>
<
p
className=
"text-[10px] text-text-muted font-bold"
>
{
entry
.
games_played
}
مباراة
</
p
>
{
entry
.
games_played
}
مباراة
</
div
>
</
p
>
{
/* Rating capsule */
}
<
div
className=
"px-3 py-1 rounded-full flex-shrink-0"
style=
{
{
background
:
`linear-gradient(135deg, ${rankColor}15, ${rankColor}08)`
,
border
:
`2px solid ${rankColor}40`
,
boxShadow
:
`0 0 8px ${rankColor}10`
,
}
}
>
<
span
className=
"text-xs font-black"
style=
{
{
color
:
rankColor
}
}
>
{
entry
.
rating
}
</
span
>
</
div
>
</
div
>
<
span
className=
"text-sm font-black text-[#FFC83D]"
>
{
entry
.
rating
}
</
span
>
</
motion
.
div
>
</
motion
.
div
>
))
}
{
/* Decorative separator */
}
{
i
<
rest
.
length
-
1
&&
(
<
div
className=
"mx-8 h-[1px]"
style=
{
{
background
:
'linear-gradient(to left, transparent, var(--color-border), transparent)'
,
}
}
/>
)
}
</
div
>
)
})
}
</
div
>
</
div
>
)
}
</>
</>
)
}
)
}
</
PageTransition
>
</
PageTransition
>
)
)
}
}
function
getRankColor
(
rank
:
number
):
string
{
if
(
rank
<=
3
)
return
'#FFC83D'
if
(
rank
<=
5
)
return
'#00E5CC'
if
(
rank
<=
7
)
return
'#B44DFF'
if
(
rank
<=
10
)
return
'#4D8BFF'
return
'#6E748C'
}
function
Podium
({
function
Podium
({
entries
,
entries
,
currentUserId
,
currentUserId
,
...
@@ -159,84 +303,135 @@ function Podium({
...
@@ -159,84 +303,135 @@ function Podium({
entries
:
{
rank
:
number
;
player_id
:
string
;
rating
:
number
;
games_played
:
number
;
display_name
:
string
;
avatar_url
?:
string
|
null
}[]
entries
:
{
rank
:
number
;
player_id
:
string
;
rating
:
number
;
games_played
:
number
;
display_name
:
string
;
avatar_url
?:
string
|
null
}[]
currentUserId
?:
string
currentUserId
?:
string
})
{
})
{
// Order: 2nd | 1st | 3rd (RTL: right to left visually)
const
ordered
=
[
entries
[
1
],
entries
[
0
],
entries
[
2
]].
filter
(
Boolean
)
const
ordered
=
[
entries
[
1
],
entries
[
0
],
entries
[
2
]].
filter
(
Boolean
)
const
podiumConfig
=
[
const
podiumConfig
=
[
{
{
color
:
'#C0C0C0'
,
color
:
'#C0C0C0'
,
height
:
'h-24'
,
podiumHeight
:
90
,
avatarSize
:
52
,
avatarSize
:
64
,
rankIcon
:
<
Medal
size=
{
16
}
className=
"text-[#C0C0C0]"
/>,
rankIcon
:
<
Medal
size=
{
20
}
className=
"text-[#C0C0C0]"
/>,
label
:
'2'
,
},
},
{
{
color
:
'#FFC83D'
,
color
:
'#FFC83D'
,
height
:
'h-32'
,
podiumHeight
:
120
,
avatarSize
:
68
,
avatarSize
:
80
,
rankIcon
:
<
Crown
size=
{
22
}
className=
"text-[#FFC83D] fill-[#FFC83D]/20"
/>,
rankIcon
:
<
Crown
size=
{
26
}
className=
"text-[#FFC83D] animate-float"
/>,
label
:
'1'
,
},
},
{
{
color
:
'#CD7F32'
,
color
:
'#CD7F32'
,
height
:
'h-20'
,
podiumHeight
:
70
,
avatarSize
:
48
,
avatarSize
:
64
,
rankIcon
:
<
Medal
size=
{
16
}
className=
"text-[#CD7F32]"
/>,
rankIcon
:
<
Medal
size=
{
20
}
className=
"text-[#CD7F32]"
/>,
label
:
'3'
,
},
},
]
]
return
(
return
(
<
div
className=
"flex items-end justify-center gap-3 py-4"
>
<
motion
.
div
className=
"relative flex items-end justify-center gap-2 pt-8 pb-2"
initial=
{
{
opacity
:
0
}
}
animate=
{
{
opacity
:
1
}
}
transition=
{
{
delay
:
0.2
}
}
>
{
/* Spotlight radial glow */
}
<
div
className=
"absolute inset-0 pointer-events-none"
style=
{
{
background
:
'radial-gradient(ellipse at 50% 80%, rgba(255,200,60,0.08) 0%, transparent 60%)'
,
}
}
/>
{
ordered
.
map
((
entry
,
i
)
=>
{
{
ordered
.
map
((
entry
,
i
)
=>
{
if
(
!
entry
)
return
null
if
(
!
entry
)
return
null
const
config
=
podiumConfig
[
i
]
const
config
=
podiumConfig
[
i
]
const
isMe
=
entry
.
player_id
===
currentUserId
const
isMe
=
entry
.
player_id
===
currentUserId
const
isFirst
=
i
===
1
return
(
return
(
<
motion
.
div
<
motion
.
div
key=
{
entry
.
player_id
}
key=
{
entry
.
player_id
}
className=
"flex flex-col items-center gap-2"
className=
"relative flex flex-col items-center"
initial=
{
{
scale
:
0
,
opacity
:
0
,
y
:
30
}
}
style=
{
{
flex
:
1
,
maxWidth
:
isFirst
?
130
:
110
}
}
initial=
{
{
scale
:
0
,
opacity
:
0
,
y
:
40
}
}
animate=
{
{
scale
:
1
,
opacity
:
1
,
y
:
0
}
}
animate=
{
{
scale
:
1
,
opacity
:
1
,
y
:
0
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
350
,
damping
:
20
,
delay
:
0.1
+
i
*
0.12
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
350
,
damping
:
20
,
delay
:
0.1
5
+
i
*
0.12
}
}
>
>
{
/* Crown / Medal above avatar */
}
<
motion
.
div
<
motion
.
div
className=
"mb-1"
initial=
{
{
scale
:
0
,
rotate
:
-
20
}
}
initial=
{
{
scale
:
0
,
rotate
:
-
20
}
}
animate=
{
{
scale
:
1
,
rotate
:
0
}
}
animate=
{
{
scale
:
1
,
rotate
:
0
}
}
transition=
{
{
delay
:
0.
4
+
i
*
0.1
,
type
:
'spring'
,
stiffness
:
400
,
damping
:
15
}
}
transition=
{
{
delay
:
0.
5
+
i
*
0.1
,
type
:
'spring'
,
stiffness
:
400
,
damping
:
15
}
}
>
>
{
isFirst
?
(
<
div
className=
"relative"
>
{
config
.
rankIcon
}
{
config
.
rankIcon
}
{
/* Gold glow for first place */
}
<
div
className=
"absolute inset-0 animate-pulse-glow rounded-full"
style=
{
{
filter
:
'blur(6px)'
,
background
:
'rgba(255,200,60,0.3)'
}
}
/>
</
div
>
)
:
(
config
.
rankIcon
)
}
</
motion
.
div
>
</
motion
.
div
>
{
/* Hex Avatar */
}
<
div
className=
{
`relative ${isMe ? 'ring-2 ring-[#FFC83D]/50 rounded-full' : ''}`
}
>
<
HexAvatar
name=
{
entry
.
display_name
}
url=
{
entry
.
avatar_url
}
size=
{
config
.
avatarSize
}
/>
</
div
>
{
/* Name */
}
<
p
className=
"text-xs font-black truncate max-w-[80px] text-center mt-1.5"
>
{
entry
.
display_name
}
</
p
>
{
/* Rating capsule */
}
<
div
<
div
className=
{
`rounded-full border-3 flex items-center justify-center font-black ${isMe ? 'ring-3 ring-[#FFC83D]/40' : ''}`
}
className=
"mt-1 px-3 py-0.5 rounded-full"
style=
{
{
style=
{
{
width
:
config
.
avatarSize
,
background
:
`linear-gradient(135deg, ${config.color}25, ${config.color}10)`
,
height
:
config
.
avatarSize
,
border
:
`2px solid ${config.color}60`
,
borderColor
:
config
.
color
,
backgroundColor
:
`${config.color}15`
,
color
:
config
.
color
,
fontSize
:
config
.
avatarSize
*
0.35
,
}
}
}
}
>
>
{
entry
.
display_name
?.
charAt
(
0
)
||
'?'
}
<
span
className=
"text-sm font-black"
style=
{
{
color
:
config
.
color
}
}
>
{
entry
.
rating
}
</
span
>
</
div
>
<
div
className=
"text-center"
>
<
p
className=
"text-xs font-black truncate max-w-[75px]"
>
{
entry
.
display_name
}
</
p
>
<
p
className=
"text-base font-black"
style=
{
{
color
:
config
.
color
}
}
>
{
entry
.
rating
}
</
p
>
<
p
className=
"text-[9px] text-text-muted font-bold"
>
{
entry
.
games_played
}
مباراة
</
p
>
</
div
>
</
div
>
{
/* 3D Podium step */
}
<
div
<
div
className=
{
`w-full ${config.height} rounded-t-xl border-3 border-b-0`
}
className=
"w-full mt-2 relative overflow-hidden"
style=
{
{
style=
{
{
borderColor
:
config
.
color
,
height
:
config
.
podiumHeight
,
background
:
`linear-gradient(to top, ${config.color}20, transparent)`
,
borderRadius
:
'12px 12px 0 0'
,
border
:
`3px solid ${config.color}`
,
borderBottom
:
`6px solid ${config.color}80`
,
background
:
`linear-gradient(to bottom, ${config.color}20 0%, ${config.color}08 50%, ${config.color}02 100%)`
,
boxShadow
:
`inset 0 2px 8px ${config.color}15, 0 4px 12px rgba(0,0,0,0.3)`
,
}
}
>
{
/* Inner gradient lit from top */
}
<
div
className=
"absolute inset-0"
style=
{
{
background
:
`linear-gradient(to bottom, ${config.color}12, transparent 60%)`
,
}
}
}
}
/>
/>
{
/* Rank number centered */
}
<
div
className=
"absolute inset-0 flex items-center justify-center"
>
<
span
className=
"text-3xl font-black opacity-20"
style=
{
{
color
:
config
.
color
}
}
>
{
config
.
label
}
</
span
>
</
div
>
</
div
>
</
motion
.
div
>
</
motion
.
div
>
)
)
})
}
})
}
</
div
>
</
motion
.
div
>
)
)
}
}
src/pages/MatchmakingPage.tsx
View file @
4d37e0d3
import
{
useEffect
}
from
'react'
import
{
useEffect
}
from
'react'
import
{
motion
,
AnimatePresence
}
from
'framer-motion'
import
{
motion
,
AnimatePresence
}
from
'framer-motion'
import
{
useNavigate
,
useSearchParams
}
from
'react-router-dom'
import
{
useNavigate
,
useSearchParams
}
from
'react-router-dom'
import
{
Button
}
from
'../components/ui/Button'
import
{
useMatchmaking
}
from
'../hooks/useMatchmaking'
import
{
useMatchmaking
}
from
'../hooks/useMatchmaking'
import
{
useMatchStore
}
from
'../stores/matchStore'
import
{
useMatchStore
}
from
'../stores/matchStore'
import
type
{
TIME_CONTROLS
}
from
'../lib/constants'
import
type
{
TIME_CONTROLS
}
from
'../lib/constants'
...
@@ -36,7 +35,9 @@ export function MatchmakingPage() {
...
@@ -36,7 +35,9 @@ export function MatchmakingPage() {
}
}
return
(
return
(
<
div
className=
"flex-1 flex flex-col items-center justify-center px-6 py-12 relative overflow-hidden min-h-dvh bg-background"
>
<
div
className=
"flex-1 flex flex-col items-center justify-center px-6 py-12 relative overflow-hidden min-h-dvh"
style=
{
{
background
:
'radial-gradient(ellipse at center, rgba(255,200,61,0.08) 0%, transparent 60%), var(--color-background)'
}
}
>
<
AnimatePresence
mode=
"wait"
>
<
AnimatePresence
mode=
"wait"
>
{
matchFound
?
(
{
matchFound
?
(
<
motion
.
div
<
motion
.
div
...
@@ -46,15 +47,63 @@ export function MatchmakingPage() {
...
@@ -46,15 +47,63 @@ export function MatchmakingPage() {
animate=
{
{
scale
:
1
,
opacity
:
1
}
}
animate=
{
{
scale
:
1
,
opacity
:
1
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
300
,
damping
:
15
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
300
,
damping
:
15
}
}
>
>
{
/* VS Badge - hex/shield shape with gold border */
}
<
div
className=
"relative"
>
<
motion
.
div
<
motion
.
div
className=
"w-28 h-28 rounded-full bg-gold/20 border-3 border-gold flex items-center justify-center animate-pulse-glow"
className=
"w-32 h-32 flex items-center justify-center"
animate=
{
{
scale
:
[
1
,
1.15
,
1
]
}
}
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
}
}
transition=
{
{
duration
:
0.6
,
repeat
:
2
}
}
animate=
{
{
scale
:
[
1
,
1.08
,
1
]
}
}
transition=
{
{
duration
:
1.2
,
repeat
:
Infinity
}
}
>
<
div
className=
"w-full h-full bg-gradient-to-br from-[#FFC83D]/30 to-[#FFC83D]/10 border-3 border-[#FFC83D] flex items-center justify-center"
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
,
boxShadow
:
'0 0 30px rgba(255,200,61,0.4)'
,
}
}
>
>
<
span
className=
"text-5xl font-black text-gold"
>
VS
</
span
>
<
span
className=
"text-5xl font-black text-[#FFC83D]"
>
VS
</
span
>
</
div
>
</
motion
.
div
>
{
/* Pulsing glow ring */
}
<
motion
.
div
className=
"absolute inset-[-8px]"
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
}
}
animate=
{
{
opacity
:
[
0.3
,
0.7
,
0.3
],
scale
:
[
0.95
,
1.05
,
0.95
]
}
}
transition=
{
{
duration
:
1.5
,
repeat
:
Infinity
}
}
>
<
div
className=
"w-full h-full border-2 border-[#FFC83D]/40"
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
}
}
/>
</
motion
.
div
>
</
motion
.
div
>
{
/* Sparkle particles */
}
{
[...
Array
(
8
)].
map
((
_
,
i
)
=>
(
<
motion
.
div
key=
{
i
}
className=
"absolute w-2 h-2 rounded-full bg-[#FFC83D]"
style=
{
{
top
:
'50%'
,
left
:
'50%'
,
}
}
animate=
{
{
x
:
[
0
,
Math
.
cos
((
i
*
Math
.
PI
*
2
)
/
8
)
*
80
],
y
:
[
0
,
Math
.
sin
((
i
*
Math
.
PI
*
2
)
/
8
)
*
80
],
opacity
:
[
1
,
0
],
scale
:
[
1
,
0.3
],
}
}
transition=
{
{
duration
:
1.2
,
repeat
:
Infinity
,
delay
:
i
*
0.15
,
ease
:
'easeOut'
,
}
}
/>
))
}
</
div
>
<
motion
.
h2
<
motion
.
h2
className=
"mt-
6 text-2xl font-black text-gold
"
className=
"mt-
8 text-2xl font-black text-[#FFC83D]
"
initial=
{
{
opacity
:
0
,
y
:
10
}
}
initial=
{
{
opacity
:
0
,
y
:
10
}
}
animate=
{
{
opacity
:
1
,
y
:
0
}
}
animate=
{
{
opacity
:
1
,
y
:
0
}
}
transition=
{
{
delay
:
0.2
}
}
transition=
{
{
delay
:
0.2
}
}
...
@@ -68,31 +117,58 @@ export function MatchmakingPage() {
...
@@ -68,31 +117,58 @@ export function MatchmakingPage() {
className=
"flex flex-col items-center"
className=
"flex flex-col items-center"
exit=
{
{
opacity
:
0
,
scale
:
0.8
}
}
exit=
{
{
opacity
:
0
,
scale
:
0.8
}
}
>
>
<
div
className=
"relative w-44 h-44 flex items-center justify-center"
>
{
/* Hexagonal radar area */
}
<
div
className=
"relative w-48 h-48 flex items-center justify-center"
>
{
/* Concentric hexagonal rings pulsing outward */
}
{
[
0
,
1
,
2
].
map
((
i
)
=>
(
{
[
0
,
1
,
2
].
map
((
i
)
=>
(
<
motion
.
div
<
motion
.
div
key=
{
i
}
key=
{
i
}
className=
"absolute inset-0
rounded-full border-3 border-gold/30
"
className=
"absolute inset-0
flex items-center justify-center
"
initial=
{
{
scale
:
0.
5
,
opacity
:
0.8
}
}
initial=
{
{
scale
:
0.
4
,
opacity
:
0.8
}
}
animate=
{
{
scale
:
2.5
,
opacity
:
0
}
}
animate=
{
{
scale
:
[
0.5
+
i
*
0.2
,
1.4
+
i
*
0.3
],
opacity
:
[
0.6
,
0
]
}
}
transition=
{
{
transition=
{
{
duration
:
2.5
,
duration
:
2.5
,
repeat
:
Infinity
,
repeat
:
Infinity
,
delay
:
i
*
0.8
,
delay
:
i
*
0.8
,
ease
:
'easeOut'
,
ease
:
'easeOut'
,
}
}
}
}
>
<
div
className=
"w-full h-full border-2 border-[#FFC83D]/30"
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
}
}
/>
/>
</
motion
.
div
>
))
}
))
}
{
/* Rotating sweep line */
}
<
motion
.
div
className=
"absolute inset-0 flex items-center justify-center"
animate=
{
{
rotate
:
360
}
}
transition=
{
{
duration
:
3
,
repeat
:
Infinity
,
ease
:
'linear'
}
}
>
<
div
className=
"w-[2px] h-1/2 origin-bottom bg-gradient-to-t from-[#FFC83D]/60 to-transparent absolute top-0"
/>
</
motion
.
div
>
{
/* Static outer hex border */
}
<
div
className=
"absolute inset-2 border-3 border-[#FFC83D]/20"
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
}
}
/>
{
/* Center "?" in shield shape */
}
<
motion
.
div
<
motion
.
div
className=
"w-24 h-24 rounded-full bg-gradient-to-br from-gold/25 to-surface-2 border-3 border-gold/50 flex items-center justify-center shadow-lg"
className=
"w-20 h-20 flex items-center justify-center z-10"
animate=
{
{
scale
:
[
1
,
1.08
,
1
],
rotate
:
[
0
,
3
,
-
3
,
0
]
}
}
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
}
}
animate=
{
{
scale
:
[
1
,
1.06
,
1
],
rotate
:
[
0
,
2
,
-
2
,
0
]
}
}
transition=
{
{
duration
:
2
,
repeat
:
Infinity
,
ease
:
'easeInOut'
}
}
transition=
{
{
duration
:
2
,
repeat
:
Infinity
,
ease
:
'easeInOut'
}
}
>
>
<
span
className=
"text-4xl font-black text-gold"
>
?
</
span
>
<
div
className=
"w-full h-full bg-gradient-to-br from-[#FFC83D]/25 to-surface-2 border-3 border-[#FFC83D]/50 flex items-center justify-center"
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
}
}
>
<
span
className=
"text-4xl font-black text-[#FFC83D]"
>
?
</
span
>
</
div
>
</
motion
.
div
>
</
motion
.
div
>
</
div
>
</
div
>
{
/* Title */
}
<
motion
.
h2
<
motion
.
h2
className=
"mt-8 text-xl font-black"
className=
"mt-8 text-xl font-black"
initial=
{
{
opacity
:
0
}
}
initial=
{
{
opacity
:
0
}
}
...
@@ -102,17 +178,21 @@ export function MatchmakingPage() {
...
@@ -102,17 +178,21 @@ export function MatchmakingPage() {
جاري البحث عن خصم
جاري البحث عن خصم
</
motion
.
h2
>
</
motion
.
h2
>
{
/* Timer in a game-panel mini-bar */
}
<
motion
.
div
<
motion
.
div
className=
"mt-
3 text-2xl font-black font-mono text-gold tabular-nums
"
className=
"mt-
4 game-panel !py-2 !px-6 inline-flex items-center justify-center
"
initial=
{
{
opacity
:
0
}
}
initial=
{
{
opacity
:
0
}
}
animate=
{
{
opacity
:
1
}
}
animate=
{
{
opacity
:
1
}
}
transition=
{
{
delay
:
0.4
}
}
transition=
{
{
delay
:
0.4
}
}
>
>
<
span
className=
"text-2xl font-black font-mono text-[#FFC83D] tabular-nums"
>
{
formatElapsed
(
elapsed
)
}
{
formatElapsed
(
elapsed
)
}
</
span
>
</
motion
.
div
>
</
motion
.
div
>
{
/* Bouncing dots */
}
<
motion
.
div
<
motion
.
div
className=
"mt-
3 flex gap-1.5
"
className=
"mt-
4 flex gap-2
"
initial=
{
{
opacity
:
0
}
}
initial=
{
{
opacity
:
0
}
}
animate=
{
{
opacity
:
1
}
}
animate=
{
{
opacity
:
1
}
}
transition=
{
{
delay
:
0.5
}
}
transition=
{
{
delay
:
0.5
}
}
...
@@ -120,22 +200,28 @@ export function MatchmakingPage() {
...
@@ -120,22 +200,28 @@ export function MatchmakingPage() {
{
[
0
,
1
,
2
].
map
((
i
)
=>
(
{
[
0
,
1
,
2
].
map
((
i
)
=>
(
<
motion
.
span
<
motion
.
span
key=
{
i
}
key=
{
i
}
className=
"w-3 h-3 rounded-full bg-
gold
"
className=
"w-3 h-3 rounded-full bg-
[#FFC83D]
"
animate=
{
{
opacity
:
[
0.2
,
1
,
0.2
],
scale
:
[
0.8
,
1.2
,
0.8
]
}
}
animate=
{
{
y
:
[
0
,
-
8
,
0
],
opacity
:
[
0.4
,
1
,
0.4
]
}
}
transition=
{
{
duration
:
1
.2
,
repeat
:
Infinity
,
delay
:
i
*
0.3
}
}
transition=
{
{
duration
:
1
,
repeat
:
Infinity
,
delay
:
i
*
0.2
}
}
/>
/>
))
}
))
}
</
motion
.
div
>
</
motion
.
div
>
{
/* Cancel - 3D ghost button */
}
<
motion
.
div
<
motion
.
div
className=
"mt-12"
className=
"mt-12"
initial=
{
{
opacity
:
0
}
}
initial=
{
{
opacity
:
0
}
}
animate=
{
{
opacity
:
1
}
}
animate=
{
{
opacity
:
1
}
}
transition=
{
{
delay
:
1
}
}
transition=
{
{
delay
:
1
}
}
>
>
<
Button
variant=
"ghost"
onClick=
{
handleCancel
}
size=
"md"
>
<
motion
.
button
whileTap=
{
{
scale
:
0.93
}
}
onClick=
{
handleCancel
}
className=
"btn-3d px-8 py-3 rounded-xl bg-surface-2 border-3 border-border text-text-muted text-sm font-black"
style=
{
{
boxShadow
:
'0 4px 0 rgba(0,0,0,0.3)'
}
}
>
الغاء
الغاء
</
B
utton
>
</
motion
.
b
utton
>
</
motion
.
div
>
</
motion
.
div
>
</
motion
.
div
>
</
motion
.
div
>
)
}
)
}
...
...
src/pages/PlayPage.tsx
View file @
4d37e0d3
import
{
useState
}
from
'react'
import
{
useState
}
from
'react'
import
{
motion
}
from
'framer-motion'
import
{
motion
}
from
'framer-motion'
import
{
Zap
,
Timer
,
Clock
,
Hourglass
,
Lock
,
Cpu
,
Crown
}
from
'lucide-react'
import
{
Zap
,
Timer
,
Clock
,
Hourglass
,
Lock
,
Cpu
,
Crown
,
Swords
}
from
'lucide-react'
import
{
useNavigate
}
from
'react-router-dom'
import
{
useNavigate
}
from
'react-router-dom'
import
{
PageTransition
}
from
'../components/layout/PageTransition'
import
{
PageTransition
}
from
'../components/layout/PageTransition'
import
{
Card
}
from
'../components/ui/Card'
import
{
Button
}
from
'../components/ui/Button'
import
{
GAMES
,
TIME_CONTROLS
}
from
'../lib/constants'
import
{
GAMES
,
TIME_CONTROLS
}
from
'../lib/constants'
import
{
playSound
}
from
'../lib/sounds'
import
{
playSound
}
from
'../lib/sounds'
...
@@ -15,23 +13,16 @@ const CATEGORIES = [
...
@@ -15,23 +13,16 @@ const CATEGORIES = [
{
key
:
'classical'
,
label
:
'كلاسيكي'
,
icon
:
Hourglass
},
{
key
:
'classical'
,
label
:
'كلاسيكي'
,
icon
:
Hourglass
},
]
as
const
]
as
const
const
GAME_ICONS
:
Record
<
string
,
string
>
=
{
backgammon
:
'⚀'
,
dominoes
:
'🂡'
,
ludo
:
'⚄'
,
trivia
:
'?'
,
}
const
stagger
=
{
const
stagger
=
{
hidden
:
{},
hidden
:
{},
show
:
{
show
:
{
transition
:
{
staggerChildren
:
0.0
6
},
transition
:
{
staggerChildren
:
0.0
7
},
},
},
}
}
const
fadeUp
=
{
const
fadeUp
=
{
hidden
:
{
opacity
:
0
,
y
:
2
0
},
hidden
:
{
opacity
:
0
,
y
:
2
4
},
show
:
{
opacity
:
1
,
y
:
0
,
transition
:
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
22
}
},
show
:
{
opacity
:
1
,
y
:
0
,
transition
:
{
type
:
'spring'
,
stiffness
:
400
,
damping
:
24
}
},
}
}
export
function
PlayPage
()
{
export
function
PlayPage
()
{
...
@@ -46,76 +37,160 @@ export function PlayPage() {
...
@@ -46,76 +37,160 @@ export function PlayPage() {
)?.[
1
].
category
)?.[
1
].
category
return
(
return
(
<
PageTransition
>
<
PageTransition
className=
"!py-6"
>
<
motion
.
div
<
motion
.
div
variants=
{
stagger
}
variants=
{
stagger
}
initial=
"hidden"
initial=
"hidden"
animate=
"show"
animate=
"show"
className=
"flex flex-col gap-6"
className=
"flex flex-col gap-6"
>
>
<
motion
.
h1
variants=
{
fadeUp
}
className=
"text-2xl font-black"
>
{
/* === 1. PAGE HEADER — Ribbon Banner === */
}
اختر اللعبة
<
motion
.
div
variants=
{
fadeUp
}
className=
"flex justify-center"
>
</
motion
.
h1
>
<
div
className=
"relative flex items-center gap-3"
>
<
Swords
size=
{
18
}
className=
"text-gold/60"
style=
{
{
transform
:
'scaleX(-1)'
}
}
/>
<
div
className=
"px-7 py-2.5"
style=
{
{
clipPath
:
'polygon(6% 0%, 94% 0%, 100% 50%, 94% 100%, 6% 100%, 0% 50%)'
,
background
:
'linear-gradient(90deg, rgba(255,200,60,0.2), rgba(255,200,60,0.08))'
,
}
}
>
<
h1
className=
"text-xl font-black text-text-primary"
>
اختر اللعبة
</
h1
>
</
div
>
<
Swords
size=
{
18
}
className=
"text-gold/60"
/>
</
div
>
</
motion
.
div
>
{
/* === 2. CHESS ARENA CARD — Featured Game === */
}
<
motion
.
div
variants=
{
fadeUp
}
className=
"relative"
>
{
/* Double-border panel */
}
<
div
className=
"relative rounded-2xl border-[3px] border-gold/50 overflow-hidden"
style=
{
{
boxShadow
:
'0 0 24px rgba(255,200,60,0.15), inset 0 0 30px rgba(255,200,60,0.03)'
,
background
:
'linear-gradient(170deg, var(--color-surface-1) 0%, rgba(30,35,64,0.95) 100%)'
,
}
}
>
{
/* Chess grid pattern (subtle) */
}
<
div
className=
"absolute inset-0 opacity-[0.03]"
style=
{
{
backgroundImage
:
'linear-gradient(rgba(255,255,255,0.5) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.5) 1px, transparent 1px)'
,
backgroundSize
:
'28px 28px'
,
}
}
/>
{
/* FEATURED corner ribbon */
}
<
div
className=
"absolute top-0 left-0 z-10"
>
<
div
className=
"px-3 py-1 text-[8px] font-black text-background tracking-wider"
style=
{
{
background
:
'linear-gradient(135deg, #FFC83D, #FF8C42)'
,
transform
:
'rotate(-0deg)'
,
borderBottomRightRadius
:
'8px'
,
}
}
>
FEATURED
</
div
>
</
div
>
<
motion
.
div
variants=
{
fadeUp
}
>
{
/* Content */
}
<
Card
variant=
"gold"
className=
"relative !p-6"
>
<
div
className=
"relative p-6 flex items-center gap-5"
>
<
div
className=
"flex items-center gap-5"
>
{
/* Crown with radial glow */
}
<
div
className=
"w-18 h-18 min-w-[72px] min-h-[72px] rounded-2xl bg-gold/15 border-3 border-gold/40 flex items-center justify-center"
>
<
div
className=
"relative flex-shrink-0"
>
<
Crown
size=
{
38
}
className=
"text-gold"
/>
<
div
className=
"absolute inset-0 rounded-full blur-xl opacity-30"
style=
{
{
background
:
'radial-gradient(circle, #FFC83D, transparent 70%)'
}
}
/>
<
div
className=
"relative w-[68px] h-[68px] rounded-2xl flex items-center justify-center border-[3px] border-gold/40"
style=
{
{
background
:
'linear-gradient(135deg, rgba(255,200,60,0.2), rgba(255,200,60,0.05))'
}
}
>
<
Crown
size=
{
36
}
className=
"text-gold"
/>
</
div
>
</
div
>
</
div
>
<
div
className=
"flex-1"
>
<
div
className=
"flex-1"
>
<
span
className=
"text-xl font-black"
>
{
chessGame
.
nameAr
}
</
span
>
<
span
className=
"text-xl font-black
text-text-primary
"
>
{
chessGame
.
nameAr
}
</
span
>
<
p
className=
"text-xs text-text-secondary mt-
2
leading-relaxed"
>
<
p
className=
"text-xs text-text-secondary mt-
1.5
leading-relaxed"
>
العب شطرنج اونلاين ضد لاعبين حقيقيين
العب شطرنج اونلاين ضد لاعبين حقيقيين
</
p
>
</
p
>
</
div
>
{
/* Online indicator */
}
</
div
>
<
div
className=
"flex items-center gap-2 mt-2.5"
>
<
motion
.
div
<
motion
.
div
className=
"absolute top-4 left-4 w-3 h-3 rounded-full bg-green-500 border-2 border-green-400/50
"
className=
"w-2.5 h-2.5 rounded-full bg-green
"
animate=
{
{
opacity
:
[
1
,
0.4
,
1
],
scale
:
[
1
,
1.2
,
1
]
}
}
animate=
{
{
opacity
:
[
1
,
0.4
,
1
],
scale
:
[
1
,
1.3
,
1
]
}
}
transition=
{
{
duration
:
2
,
repeat
:
Infinity
}
}
transition=
{
{
duration
:
1.8
,
repeat
:
Infinity
}
}
/>
/>
</
Card
>
<
span
className=
"text-[11px] font-bold text-green"
>
اونلاين
</
span
>
</
div
>
</
div
>
</
div
>
{
/* Gold bottom accent */
}
<
div
className=
"h-[3px] w-full bg-gradient-to-r from-transparent via-gold to-transparent"
/>
</
div
>
</
motion
.
div
>
</
motion
.
div
>
<
motion
.
div
variants=
{
fadeUp
}
className=
"grid grid-cols-2 gap-3"
>
{
/* === 3. OTHER GAMES — Compact Locked Tiles === */
}
<
motion
.
div
variants=
{
fadeUp
}
className=
"grid grid-cols-2 gap-2.5"
>
{
otherGames
.
map
((
game
)
=>
(
{
otherGames
.
map
((
game
)
=>
(
<
div
<
div
key=
{
game
.
key
}
key=
{
game
.
key
}
className=
"relative rounded-2xl overflow-hidden border-3 border-border bg-surface-1 opacity-50 p-5 flex flex-col items-center gap-3"
className=
"relative h-[56px] rounded-xl overflow-hidden border-2 border-border/40 flex items-center justify-center"
style=
{
{
background
:
'var(--color-surface-1)'
,
boxShadow
:
'inset 0 3px 8px rgba(0,0,0,0.4)'
,
}
}
>
>
<
div
className=
"w-12 h-12 rounded-xl bg-surface-3/80 border-2 border-border flex items-center justify-center"
>
{
/* Frosted overlay */
}
<
span
className=
"text-2xl font-bold text-text-muted"
>
<
div
className=
"absolute inset-0 bg-background/40 backdrop-blur-[1px]"
/>
{
GAME_ICONS
[
game
.
key
]
||
''
}
{
/* Lock badge */
}
</
span
>
<
div
className=
"relative z-10 flex items-center gap-2"
>
<
div
className=
"w-6 h-6 flex items-center justify-center"
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
,
background
:
'rgba(61,69,112,0.6)'
,
}
}
>
<
Lock
size=
{
10
}
className=
"text-text-muted"
/>
</
div
>
</
div
>
<
span
className=
"text-sm font-black text-text-secondary"
>
{
game
.
nameAr
}
</
span
>
<
div
>
<
div
className=
"absolute inset-0 flex items-center justify-center bg-background/60 backdrop-blur-[2px]"
>
<
span
className=
"text-[11px] font-bold text-text-muted block"
>
{
game
.
nameAr
}
</
span
>
<
div
className=
"flex items-center gap-1.5 px-3.5 py-2 rounded-full bg-surface-2/90 border-2 border-border"
>
<
span
className=
"text-[9px] text-text-muted/60"
>
قريبا
</
span
>
<
Lock
size=
{
13
}
className=
"text-text-muted"
/>
<
span
className=
"text-[11px] text-text-muted font-bold"
>
قريبا
</
span
>
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
))
}
))
}
</
motion
.
div
>
</
motion
.
div
>
{
/* === 4. TIME CONTROL SECTION === */
}
<
motion
.
div
variants=
{
fadeUp
}
className=
"space-y-4"
>
<
motion
.
div
variants=
{
fadeUp
}
className=
"space-y-4"
>
<
h2
className=
"text-lg font-black"
>
نظام الوقت
</
h2
>
{
/* Section ribbon header */
}
<
div
className=
"flex justify-center"
>
<
div
className=
"px-5 py-1.5 text-[11px] font-black text-gold"
style=
{
{
clipPath
:
'polygon(4% 0%, 96% 0%, 100% 50%, 96% 100%, 4% 100%, 0% 50%)'
,
background
:
'linear-gradient(90deg, rgba(255,200,60,0.12), rgba(255,200,60,0.05))'
,
border
:
'1.5px solid rgba(255,200,60,0.25)'
,
}
}
>
نظام الوقت
</
div
>
</
div
>
<
div
className=
"flex gap-2 overflow-x-auto scrollbar-hide pb-1"
>
{
/* Category Tabs — Trapezoidal */
}
<
div
className=
"flex gap-2 justify-center overflow-x-auto scrollbar-hide pb-1"
>
{
CATEGORIES
.
map
((
cat
)
=>
{
{
CATEGORIES
.
map
((
cat
)
=>
{
const
isActive
=
activeCategory
===
cat
.
key
const
isActive
=
activeCategory
===
cat
.
key
const
Icon
=
cat
.
icon
const
Icon
=
cat
.
icon
return
(
return
(
<
motion
.
button
<
motion
.
button
key=
{
cat
.
key
}
key=
{
cat
.
key
}
className=
{
`flex items-center gap-2 px-4 py-2.5 rounded-full text-xs font-black border-2 whitespace-nowrap transition-all ${
className=
"relative cursor-pointer"
isActive
whileTap=
{
{
scale
:
0.92
}
}
? 'bg-gold/20 border-gold text-gold shadow-md shadow-gold/15'
: 'bg-surface-2 border-border text-text-muted'
}`
}
whileTap=
{
{
scale
:
0.9
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
20
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
20
}
}
onClick=
{
()
=>
{
onClick=
{
()
=>
{
playSound
(
'click'
)
playSound
(
'click'
)
...
@@ -123,60 +198,128 @@ export function PlayPage() {
...
@@ -123,60 +198,128 @@ export function PlayPage() {
if
(
first
)
setSelectedTC
(
first
[
0
])
if
(
first
)
setSelectedTC
(
first
[
0
])
}
}
}
}
>
>
<
Icon
size=
{
14
}
/>
<
div
className=
{
`flex items-center gap-1.5 px-4 py-2.5 text-[11px] font-black whitespace-nowrap transition-all ${
isActive
? 'text-background'
: 'text-text-muted'
}`
}
style=
{
{
clipPath
:
'polygon(8% 0%, 92% 0%, 100% 100%, 0% 100%)'
,
background
:
isActive
?
'linear-gradient(180deg, #FFE066, #FFC83D)'
:
'var(--color-surface-2)'
,
boxShadow
:
isActive
?
'0 4px 12px rgba(255,200,60,0.25)'
:
'inset 0 2px 4px rgba(0,0,0,0.3)'
,
}
}
>
<
Icon
size=
{
13
}
/>
{
cat
.
label
}
{
cat
.
label
}
</
div
>
</
motion
.
button
>
</
motion
.
button
>
)
)
})
}
})
}
</
div
>
</
div
>
{
/* Time Options Grid */
}
<
div
className=
"grid grid-cols-3 gap-2.5"
>
<
div
className=
"grid grid-cols-3 gap-2.5"
>
{
Object
.
entries
(
TIME_CONTROLS
)
{
Object
.
entries
(
TIME_CONTROLS
)
.
filter
(([,
v
])
=>
v
.
category
===
activeCategory
)
.
filter
(([,
v
])
=>
v
.
category
===
activeCategory
)
.
map
(([
key
,
tc
])
=>
(
.
map
(([
key
,
tc
])
=>
{
const
isSelected
=
selectedTC
===
key
return
(
<
motion
.
button
<
motion
.
button
key=
{
key
}
key=
{
key
}
className=
{
`py-4 rounded-xl text-center font-black text-sm border-3 transition-all ${
className=
"relative cursor-pointer"
selectedTC === key
? 'bg-gold/20 border-gold text-gold shadow-md shadow-gold/15 scale-105'
: 'bg-surface-2 border-border text-text-secondary hover:border-border/80'
}`
}
whileTap=
{
{
scale
:
0.9
}
}
whileTap=
{
{
scale
:
0.9
}
}
animate=
{
isSelected
?
{
y
:
-
2
}
:
{
y
:
0
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
20
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
20
}
}
onClick=
{
()
=>
{
onClick=
{
()
=>
{
playSound
(
'click'
)
playSound
(
'click'
)
setSelectedTC
(
key
)
setSelectedTC
(
key
)
}
}
}
}
>
<
div
className=
{
`py-4 px-2 text-center font-black text-sm transition-all rounded-xl border-[3px] ${
isSelected
? 'border-gold text-gold'
: 'border-border/60 text-text-secondary'
}`
}
style=
{
{
background
:
isSelected
?
'linear-gradient(180deg, rgba(255,200,60,0.15), rgba(255,200,60,0.04))'
:
'var(--color-surface-1)'
,
boxShadow
:
isSelected
?
'0 0 16px rgba(255,200,60,0.2), 0 4px 0 0 rgba(201,151,46,0.4)'
:
'inset 0 2px 6px rgba(0,0,0,0.3)'
,
}
}
>
>
{
tc
.
labelAr
}
{
tc
.
labelAr
}
</
div
>
</
motion
.
button
>
</
motion
.
button
>
))
}
)
})
}
</
div
>
</
div
>
</
motion
.
div
>
</
motion
.
div
>
<
motion
.
div
variants=
{
fadeUp
}
className=
"flex flex-col items-center gap-3 mt-2 pb-4"
>
{
/* === 5. ACTION BUTTONS === */
}
<
Button
<
motion
.
div
variants=
{
fadeUp
}
className=
"flex flex-col items-center gap-4 pt-3 pb-4"
>
{
/* Primary: Find opponent — massive 3D gold */
}
<
motion
.
button
className=
"relative w-[80%] cursor-pointer"
whileTap=
{
{
y
:
4
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
600
,
damping
:
20
}
}
onClick=
{
()
=>
{
onClick=
{
()
=>
{
playSound
(
'click'
)
playSound
(
'click'
)
navigate
(
`/matchmaking?tc=${selectedTC}&game=chess`
)
navigate
(
`/matchmaking?tc=${selectedTC}&game=chess`
)
}
}
}
}
className=
"w-[80%]"
size=
"lg"
>
>
البحث عن خصم
<
div
</
Button
>
className=
"relative flex items-center justify-center gap-3 py-5 rounded-2xl font-black text-lg text-background overflow-hidden"
<
Button
style=
{
{
background
:
'linear-gradient(180deg, #FFE066 0%, #FFC83D 70%, #C9972E 100%)'
,
boxShadow
:
'0 6px 0 0 #9B6B1A, 0 8px 24px rgba(255,200,60,0.3)'
,
}
}
>
{
/* Animated glow */
}
<
motion
.
div
className=
"absolute inset-0 rounded-2xl"
animate=
{
{
boxShadow
:
[
'0 0 10px rgba(255,200,60,0.2)'
,
'0 0 25px rgba(255,200,60,0.4)'
,
'0 0 10px rgba(255,200,60,0.2)'
,
],
}
}
transition=
{
{
duration
:
2
,
repeat
:
Infinity
}
}
/>
<
Swords
size=
{
22
}
className=
"text-background"
/>
<
span
>
البحث عن خصم
</
span
>
</
div
>
</
motion
.
button
>
{
/* Secondary: Play vs bot — purple 3D */
}
<
motion
.
button
className=
"relative w-[72%] cursor-pointer"
whileTap=
{
{
y
:
3
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
600
,
damping
:
20
}
}
onClick=
{
()
=>
{
onClick=
{
()
=>
{
playSound
(
'click'
)
playSound
(
'click'
)
navigate
(
'/bot-select'
)
navigate
(
'/bot-select'
)
}
}
}
}
variant=
"ghost"
>
className=
"w-[80%]"
<
div
size=
"md"
className=
"flex items-center justify-center gap-3 py-4 rounded-2xl font-black text-sm text-text-primary border-2 border-purple/40"
style=
{
{
background
:
'linear-gradient(180deg, rgba(180,77,255,0.15) 0%, rgba(180,77,255,0.05) 100%)'
,
boxShadow
:
'0 5px 0 0 rgba(120,40,180,0.4)'
,
}
}
>
>
<
Cpu
size=
{
18
}
className=
"text-purple"
/>
<
Cpu
size=
{
18
}
className=
"text-purple"
/>
العب ضد الروبوت
<
span
>
العب ضد الروبوت
</
span
>
</
Button
>
</
div
>
</
motion
.
button
>
</
motion
.
div
>
</
motion
.
div
>
</
motion
.
div
>
</
motion
.
div
>
</
PageTransition
>
</
PageTransition
>
...
...
src/pages/ProfilePage.tsx
View file @
4d37e0d3
import
{
useState
}
from
'react'
import
{
useState
}
from
'react'
import
{
motion
,
AnimatePresence
}
from
'framer-motion'
import
{
motion
,
AnimatePresence
}
from
'framer-motion'
import
{
Trophy
,
Target
,
Flame
,
TrendingUp
,
Pencil
,
X
,
LogOut
}
from
'lucide-react'
import
{
Trophy
,
Target
,
Flame
,
TrendingUp
,
Pencil
,
X
,
LogOut
,
Star
,
Shield
}
from
'lucide-react'
import
{
useAuthStore
}
from
'../stores/authStore'
import
{
useAuthStore
}
from
'../stores/authStore'
import
{
useNotificationStore
}
from
'../stores/notificationStore'
import
{
useNotificationStore
}
from
'../stores/notificationStore'
import
{
PageTransition
}
from
'../components/layout/PageTransition'
import
{
PageTransition
}
from
'../components/layout/PageTransition'
import
{
Card
}
from
'../components/ui/Card'
import
{
Button
}
from
'../components/ui/Button'
import
{
Button
}
from
'../components/ui/Button'
import
{
supabase
}
from
'../lib/supabase'
import
{
supabase
}
from
'../lib/supabase'
const
stagger
=
{
const
stagger
=
{
hidden
:
{
opacity
:
0
},
hidden
:
{
opacity
:
0
},
show
:
{
opacity
:
1
,
transition
:
{
staggerChildren
:
0.0
6
}
},
show
:
{
opacity
:
1
,
transition
:
{
staggerChildren
:
0.0
8
}
},
}
}
const
item
=
{
const
item
=
{
hidden
:
{
opacity
:
0
,
y
:
1
2
,
scale
:
0.95
},
hidden
:
{
opacity
:
0
,
y
:
1
6
,
scale
:
0.93
},
show
:
{
opacity
:
1
,
y
:
0
,
scale
:
1
,
transition
:
{
type
:
'spring'
,
stiffness
:
40
0
,
damping
:
22
}
},
show
:
{
opacity
:
1
,
y
:
0
,
scale
:
1
,
transition
:
{
type
:
'spring'
,
stiffness
:
38
0
,
damping
:
22
}
},
}
}
const
HEX_CLIP
=
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
export
function
ProfilePage
()
{
export
function
ProfilePage
()
{
const
{
user
,
profile
,
setProfile
}
=
useAuthStore
()
const
{
user
,
profile
,
setProfile
}
=
useAuthStore
()
const
{
showToast
}
=
useNotificationStore
()
const
{
showToast
}
=
useNotificationStore
()
...
@@ -74,116 +75,272 @@ export function ProfilePage() {
...
@@ -74,116 +75,272 @@ export function ProfilePage() {
const
xpForNextLevel
=
500
const
xpForNextLevel
=
500
const
currentXpInLevel
=
profile
.
xp
%
xpForNextLevel
const
currentXpInLevel
=
profile
.
xp
%
xpForNextLevel
const
xpRemaining
=
xpForNextLevel
-
currentXpInLevel
const
xpPercent
=
Math
.
min
((
currentXpInLevel
/
xpForNextLevel
)
*
100
,
100
)
const
xpPercent
=
Math
.
min
((
currentXpInLevel
/
xpForNextLevel
)
*
100
,
100
)
const
ratings
=
[
const
ratings
=
[
{
label
:
'رصاصة'
,
value
:
profile
.
elo_bullet
,
color
:
'border-t-[#FF5252]'
},
{
label
:
'رصاصة'
,
value
:
profile
.
elo_bullet
,
color
:
'#FF5252'
,
borderColor
:
'border-[#FF5252]'
},
{
label
:
'خاطف'
,
value
:
profile
.
elo_blitz
,
color
:
'border-t-[#FFC83D]'
},
{
label
:
'خاطف'
,
value
:
profile
.
elo_blitz
,
color
:
'#FFC83D'
,
borderColor
:
'border-[#FFC83D]'
},
{
label
:
'سريع'
,
value
:
profile
.
elo_rapid
,
color
:
'border-t-[#00E5CC]'
},
{
label
:
'سريع'
,
value
:
profile
.
elo_rapid
,
color
:
'#00E5CC'
,
borderColor
:
'border-[#00E5CC]'
},
{
label
:
'كلاسيكي'
,
value
:
profile
.
elo_classical
,
color
:
'border-t-[#B44DFF]'
},
{
label
:
'كلاسيكي'
,
value
:
profile
.
elo_classical
,
color
:
'#B44DFF'
,
borderColor
:
'border-[#B44DFF]'
},
]
const
stats
=
[
{
icon
:
<
TrendingUp
size=
{
20
}
className=
"text-[#B44DFF]"
/>,
value
:
profile
.
total_games_played
,
label
:
'مباريات'
,
ringColor
:
'#B44DFF'
},
{
icon
:
<
Trophy
size=
{
20
}
className=
"text-[#FFC83D]"
/>,
value
:
profile
.
total_wins
,
label
:
'انتصارات'
,
ringColor
:
'#FFC83D'
},
{
icon
:
<
Target
size=
{
20
}
className=
"text-[#00E5CC]"
/>,
value
:
`
${
winRate
}
%`
,
label
:
'نسبة الفوز'
,
ringColor
:
'#00E5CC'
},
{
icon
:
<
Flame
size=
{
20
}
className=
"text-[#FF5252]"
/>,
value
:
profile
.
best_win_streak
,
label
:
'افضل سلسلة'
,
ringColor
:
'#FF5252'
},
]
]
return
(
return
(
<
PageTransition
>
<
PageTransition
>
<
motion
.
div
<
motion
.
div
className=
"flex flex-col gap-
5 py-
6"
className=
"flex flex-col gap-6"
variants=
{
stagger
}
variants=
{
stagger
}
initial=
"hidden"
initial=
"hidden"
animate=
"show"
animate=
"show"
>
>
<
motion
.
div
className=
"flex flex-col items-center gap-3"
variants=
{
item
}
>
{
/* === PLAYER CARD / NAMEPLATE === */
}
<
motion
.
div
variants=
{
item
}
>
<
div
className=
"relative overflow-hidden rounded-[20px] p-5"
style=
{
{
border
:
'3px solid var(--color-border)'
,
background
:
'linear-gradient(135deg, rgba(255,200,60,0.05) 0%, rgba(180,77,255,0.05) 100%)'
,
boxShadow
:
'inset 0 2px 4px rgba(255,255,255,0.04), inset 0 -2px 6px rgba(0,0,0,0.3), 0 6px 20px rgba(0,0,0,0.4)'
,
}
}
>
{
/* Decorative corner stars */
}
<
Star
size=
{
24
}
className=
"absolute top-3 left-3 text-[#FFC83D] opacity-[0.08]"
/>
<
Star
size=
{
18
}
className=
"absolute top-5 left-10 text-[#FFC83D] opacity-[0.06]"
/>
<
Star
size=
{
20
}
className=
"absolute bottom-4 right-4 text-[#B44DFF] opacity-[0.07]"
/>
<
Star
size=
{
14
}
className=
"absolute bottom-6 right-10 text-[#B44DFF] opacity-[0.05]"
/>
{
/* Subtle background pattern */
}
<
div
className=
"absolute inset-0 opacity-[0.02]"
style=
{
{
backgroundImage
:
'repeating-linear-gradient(45deg, transparent, transparent 10px, rgba(255,200,60,0.5) 10px, rgba(255,200,60,0.5) 11px)'
,
}
}
/>
<
div
className=
"relative flex flex-col items-center gap-3"
>
{
/* Hexagonal Avatar */
}
<
div
className=
"relative"
>
<
div
className=
"relative"
>
<
motion
.
div
<
motion
.
div
className=
"w-[88px] h-[88px] rounded-full bg-gradient-to-br from-[#FFC83D]/30 to-[#B44DFF]/20 border-3 border-[#FFC83D] flex items-center justify-center shadow-lg shadow-[#FFC83D]/20"
className=
"flex items-center justify-center"
initial=
{
{
scale
:
0
,
rotate
:
-
20
}
}
style=
{
{
width
:
80
,
height
:
80
,
clipPath
:
HEX_CLIP
,
background
:
'linear-gradient(135deg, rgba(255,200,60,0.25), rgba(180,77,255,0.15))'
,
border
:
'none'
,
}
}
initial=
{
{
scale
:
0
,
rotate
:
-
30
}
}
animate=
{
{
scale
:
1
,
rotate
:
0
}
}
animate=
{
{
scale
:
1
,
rotate
:
0
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
300
,
damping
:
18
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
300
,
damping
:
18
}
}
>
>
<
span
className=
"text-4xl font-black text-[#FFC83D]"
>
<
div
className=
"flex items-center justify-center"
style=
{
{
width
:
74
,
height
:
74
,
clipPath
:
HEX_CLIP
,
background
:
'linear-gradient(135deg, var(--color-surface-2), var(--color-surface-3))'
,
}
}
>
<
span
className=
"text-3xl font-black text-[#FFC83D]"
>
{
profile
.
display_name
?.
charAt
(
0
)
||
'?'
}
{
profile
.
display_name
?.
charAt
(
0
)
||
'?'
}
</
span
>
</
span
>
</
div
>
</
motion
.
div
>
</
motion
.
div
>
{
/* Gold border hex overlay */
}
<
div
className=
"absolute inset-0 pointer-events-none"
style=
{
{
clipPath
:
HEX_CLIP
,
border
:
'3px solid #FFC83D'
,
width
:
80
,
height
:
80
,
}
}
/>
{
/* Level badge */
}
<
motion
.
div
<
motion
.
div
className=
"absolute -bottom-1 -right-1 w-7 h-7 rounded-full bg-gradient-to-b from-[#FFC83D] to-[#E0A800] border-3 border-background flex items-center justify-center"
className=
"absolute -bottom-2 left-1/2 -translate-x-1/2 w-7 h-7 rounded-full flex items-center justify-center"
style=
{
{
background
:
'linear-gradient(to bottom, #FFE066, #FFC83D)'
,
border
:
'2px solid var(--color-background)'
,
boxShadow
:
'0 2px 8px rgba(255,200,60,0.4)'
,
}
}
initial=
{
{
scale
:
0
}
}
initial=
{
{
scale
:
0
}
}
animate=
{
{
scale
:
1
}
}
animate=
{
{
scale
:
1
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
20
,
delay
:
0.2
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
20
,
delay
:
0.2
}
}
>
>
<
span
className=
"text-[10px] font-black text-background
"
>
{
profile
.
level
}
</
span
>
<
span
className=
"text-[10px] font-black text-[#0B0E1A]
"
>
{
profile
.
level
}
</
span
>
</
motion
.
div
>
</
motion
.
div
>
</
div
>
</
div
>
<
div
className=
"text-center"
>
<
h1
className=
"text-xl font-black"
>
{
profile
.
display_name
}
</
h1
>
{
/* Player name + username */
}
<
div
className=
"text-center mt-1"
>
<
h1
className=
"text-2xl font-black tracking-tight"
>
{
profile
.
display_name
}
</
h1
>
<
p
className=
"text-sm text-text-muted font-bold"
>
@
{
profile
.
username
}
</
p
>
<
p
className=
"text-sm text-text-muted font-bold"
>
@
{
profile
.
username
}
</
p
>
{
profile
.
bio
&&
(
{
profile
.
bio
&&
(
<
p
className=
"text-xs text-text-secondary mt-1 max-w-[220px] mx-auto line-clamp-2
"
>
{
profile
.
bio
}
</
p
>
<
p
className=
"text-xs text-text-secondary mt-1.5 max-w-[240px] mx-auto line-clamp-2 leading-relaxed
"
>
{
profile
.
bio
}
</
p
>
)
}
)
}
</
div
>
</
div
>
</
motion
.
div
>
<
motion
.
div
variants=
{
item
}
>
{
/* XP Progress bar */
}
<
Card
variant=
"gold"
className=
"!p-4"
>
<
div
className=
"w-full mt-2"
>
<
div
className=
"flex items-center justify-between mb-2"
>
<
div
className=
"flex items-center justify-between mb-1.5"
>
<
span
className=
"text-lg font-black"
>
المستوى
{
profile
.
level
}
</
span
>
<
span
className=
"text-xs font-black text-text-secondary"
>
المستوى
{
profile
.
level
}
</
span
>
<
span
className=
"text-sm font-black text-[#FFC83D] px-3 py-1 rounded-full bg-[#FFC83D]/15 border-2 border-[#FFC83D]/30"
>
<
span
className=
"text-xs font-black text-[#FFC83D]"
>
XP
{
currentXpInLevel
}
</
span
>
{
profile
.
xp
}
XP
</
span
>
</
div
>
</
div
>
<
div
className=
"w-full h-4 rounded-full bg-surface-3 border-2 border-border overflow-hidden"
>
<
div
className=
"w-full overflow-hidden"
style=
{
{
height
:
16
,
borderRadius
:
10
,
border
:
'2px solid var(--color-border)'
,
background
:
'var(--color-surface-3)'
,
boxShadow
:
'inset 0 2px 4px rgba(0,0,0,0.4)'
,
}
}
>
<
motion
.
div
<
motion
.
div
className=
"h-full rounded-full bg-gradient-to-l from-[#FFC83D] to-[#FFE082]"
className=
"h-full relative"
style=
{
{
borderRadius
:
8
,
background
:
'linear-gradient(to left, #FFC83D, #FFE066)'
,
backgroundImage
:
'repeating-linear-gradient(90deg, transparent, transparent 8px, rgba(255,255,255,0.1) 8px, rgba(255,255,255,0.1) 9px)'
,
}
}
initial=
{
{
width
:
0
}
}
initial=
{
{
width
:
0
}
}
animate=
{
{
width
:
`${xpPercent}%`
}
}
animate=
{
{
width
:
`${xpPercent}%`
}
}
transition=
{
{
duration
:
1.2
,
ease
:
'easeOut'
,
delay
:
0.3
}
}
transition=
{
{
duration
:
1.2
,
ease
:
'easeOut'
,
delay
:
0.4
}
}
/>
/>
</
div
>
</
div
>
<
p
className=
"text-xs text-text-muted font-bold mt-2"
>
{
xpRemaining
}
XP للمستوى التالي
</
p
>
</
div
>
</
Card
>
</
div
>
</
div
>
</
motion
.
div
>
</
motion
.
div
>
<
motion
.
div
variants=
{
item
}
>
{
/* === RATINGS SECTION === */
}
<
motion
.
div
variants=
{
item
}
className=
"flex flex-col gap-3"
>
{
/* Section header */
}
<
div
className=
"flex items-center gap-3"
>
<
h2
className=
"text-lg font-black"
>
التقييمات
</
h2
>
<
div
className=
"flex-1 h-[2px]"
style=
{
{
background
:
'linear-gradient(to left, transparent, rgba(255,200,60,0.4))'
}
}
/>
</
div
>
{
/* 2x2 Rating grid */
}
<
div
className=
"grid grid-cols-2 gap-3"
>
<
div
className=
"grid grid-cols-2 gap-3"
>
{
ratings
.
map
((
rating
,
i
)
=>
(
{
ratings
.
map
((
rating
,
i
)
=>
(
<
motion
.
div
<
motion
.
div
key=
{
rating
.
label
}
key=
{
rating
.
label
}
className=
{
`p-4 rounded-2xl bg-surface-1 border-3 border-border border-t-4 ${rating.color} flex flex-col items-center gap-1`
}
className=
"relative flex flex-col items-center justify-center overflow-hidden"
initial=
{
{
opacity
:
0
,
y
:
14
}
}
style=
{
{
animate=
{
{
opacity
:
1
,
y
:
0
}
}
height
:
90
,
transition=
{
{
type
:
'spring'
,
stiffness
:
400
,
damping
:
22
,
delay
:
0.15
+
i
*
0.06
}
}
clipPath
:
HEX_CLIP
,
border
:
`3px solid ${rating.color}`
,
background
:
`linear-gradient(to bottom, ${rating.color}10, var(--color-surface-1))`
,
boxShadow
:
`inset 0 2px 6px rgba(0,0,0,0.3), 0 0 12px ${rating.color}15`
,
}
}
initial=
{
{
opacity
:
0
,
scale
:
0.8
}
}
animate=
{
{
opacity
:
1
,
scale
:
1
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
400
,
damping
:
22
,
delay
:
0.2
+
i
*
0.07
}
}
>
{
/* Inner hex to create border effect */
}
<
div
className=
"absolute inset-0 flex flex-col items-center justify-center"
style=
{
{
borderTop
:
`4px solid ${rating.color}`
,
}
}
>
>
<
span
className=
"text-2xl font-black"
>
{
rating
.
value
}
</
span
>
<
span
className=
"text-2xl font-black mt-2"
>
{
rating
.
value
}
</
span
>
<
span
className=
"text-xs font-bold text-text-muted"
>
{
rating
.
label
}
</
span
>
<
span
className=
"text-[10px] font-bold text-text-muted"
>
{
rating
.
label
}
</
span
>
</
div
>
</
motion
.
div
>
</
motion
.
div
>
))
}
))
}
</
div
>
</
div
>
</
motion
.
div
>
</
motion
.
div
>
<
motion
.
div
variants=
{
item
}
>
{
/* === STATS SECTION === */
}
<
div
className=
"grid grid-cols-4 gap-2"
>
<
motion
.
div
variants=
{
item
}
className=
"flex flex-col gap-3"
>
<
StatBox
icon=
{
<
TrendingUp
size=
{
18
}
className=
"text-[#B44DFF]"
/>
}
value=
{
profile
.
total_games_played
}
label=
"مباريات"
/>
{
/* Section header */
}
<
StatBox
icon=
{
<
Trophy
size=
{
18
}
className=
"text-[#FFC83D]"
/>
}
value=
{
profile
.
total_wins
}
label=
"انتصارات"
/>
<
div
className=
"flex items-center gap-3"
>
<
StatBox
icon=
{
<
Target
size=
{
18
}
className=
"text-[#00E5CC]"
/>
}
value=
{
`${winRate}%`
}
label=
"نسبة الفوز"
/>
<
h2
className=
"text-lg font-black"
>
الاحصائيات
</
h2
>
<
StatBox
icon=
{
<
Flame
size=
{
18
}
className=
"text-[#FF5252]"
/>
}
value=
{
profile
.
best_win_streak
}
label=
"افضل سلسلة"
/>
<
div
className=
"flex-1 h-[2px]"
style=
{
{
background
:
'linear-gradient(to left, transparent, rgba(0,229,204,0.4))'
}
}
/>
</
div
>
{
/* 4 circular medal items */
}
<
div
className=
"flex items-center justify-between gap-2"
>
{
stats
.
map
((
stat
,
i
)
=>
(
<
motion
.
div
key=
{
stat
.
label
}
className=
"flex flex-col items-center gap-1.5"
initial=
{
{
opacity
:
0
,
y
:
12
}
}
animate=
{
{
opacity
:
1
,
y
:
0
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
400
,
damping
:
22
,
delay
:
0.25
+
i
*
0.07
}
}
>
{
/* Circular badge with ring */
}
<
div
className=
"relative flex items-center justify-center"
style=
{
{
width
:
64
,
height
:
64
,
borderRadius
:
'50%'
,
border
:
`3px solid ${stat.ringColor}`
,
background
:
`linear-gradient(135deg, ${stat.ringColor}12, var(--color-surface-2))`
,
boxShadow
:
`inset 0 2px 6px rgba(0,0,0,0.3), 0 0 8px ${stat.ringColor}15`
,
}
}
>
<
div
className=
"flex flex-col items-center gap-0.5"
>
{
stat
.
icon
}
<
span
className=
"text-xs font-black mt-0.5"
>
{
stat
.
value
}
</
span
>
</
div
>
</
div
>
<
span
className=
"text-[9px] text-text-muted font-bold text-center leading-tight"
>
{
stat
.
label
}
</
span
>
</
motion
.
div
>
))
}
</
div
>
</
div
>
</
motion
.
div
>
</
motion
.
div
>
<
motion
.
div
className=
"flex flex-col items-center gap-3 mt-2"
variants=
{
item
}
>
{
/* === ACTION BUTTONS === */
}
<
Button
variant=
"ghost"
onClick=
{
openEdit
}
className=
"w-[80%]"
>
<
motion
.
div
className=
"flex flex-col items-center gap-3 mt-1"
variants=
{
item
}
>
<
Pencil
size=
{
16
}
/>
{
/* Edit profile button */
}
تعديل الملف الشخصي
</
Button
>
<
motion
.
button
<
motion
.
button
className=
"text-sm font-bold text-[#FF5252]/70 flex items-center gap-1.5 py-2"
className=
"w-[75%] flex items-center justify-center gap-2.5 py-3.5 rounded-2xl font-black text-sm"
whileTap=
{
{
scale
:
0.9
}
}
style=
{
{
border
:
'3px solid var(--color-border)'
,
background
:
'var(--color-surface-2)'
,
boxShadow
:
'inset 0 1px 0 rgba(255,255,255,0.04), inset 0 -2px 4px rgba(0,0,0,0.2), 0 4px 12px rgba(0,0,0,0.3)'
,
}
}
whileTap=
{
{
scale
:
0.94
,
y
:
2
}
}
whileHover=
{
{
scale
:
1.02
,
borderColor
:
'rgba(255,200,60,0.5)'
}
}
onClick=
{
openEdit
}
>
<
Pencil
size=
{
16
}
className=
"text-[#FFC83D]"
/>
<
span
>
تعديل الملف الشخصي
</
span
>
</
motion
.
button
>
{
/* Logout button */
}
<
motion
.
button
className=
"w-[60%] flex items-center justify-center gap-2 py-2.5 rounded-xl font-bold text-xs"
style=
{
{
border
:
'2px solid rgba(255,82,82,0.4)'
,
background
:
'rgba(255,82,82,0.08)'
,
color
:
'#FF5252'
,
boxShadow
:
'0 2px 8px rgba(255,82,82,0.15)'
,
}
}
whileTap=
{
{
scale
:
0.92
,
y
:
1
}
}
onClick=
{
handleLogout
}
onClick=
{
handleLogout
}
>
>
<
LogOut
size=
{
14
}
/>
<
LogOut
size=
{
14
}
/>
تسجيل الخروج
<
span
>
تسجيل الخروج
</
span
>
</
motion
.
button
>
</
motion
.
button
>
</
motion
.
div
>
</
motion
.
div
>
</
motion
.
div
>
</
motion
.
div
>
{
/* === EDIT MODAL === */
}
<
AnimatePresence
>
<
AnimatePresence
>
{
editOpen
&&
(
{
editOpen
&&
(
<
motion
.
div
<
motion
.
div
...
@@ -200,31 +357,58 @@ export function ProfilePage() {
...
@@ -200,31 +357,58 @@ export function ProfilePage() {
exit=
{
{
opacity
:
0
}
}
exit=
{
{
opacity
:
0
}
}
/>
/>
<
motion
.
div
<
motion
.
div
className=
"relative w-full max-w-[360px] rounded-2xl bg-surface-1 border-3 border-border p-6 flex flex-col gap-5"
className=
"relative w-full max-w-[360px] flex flex-col gap-5 overflow-hidden"
style=
{
{
borderRadius
:
20
,
border
:
'3px solid var(--color-border)'
,
background
:
'var(--color-surface-1)'
,
boxShadow
:
'inset 0 2px 4px rgba(255,255,255,0.03), inset 0 -2px 8px rgba(0,0,0,0.3), 0 16px 48px rgba(0,0,0,0.6)'
,
}
}
initial=
{
{
scale
:
0.85
,
opacity
:
0
,
y
:
30
}
}
initial=
{
{
scale
:
0.85
,
opacity
:
0
,
y
:
30
}
}
animate=
{
{
scale
:
1
,
opacity
:
1
,
y
:
0
}
}
animate=
{
{
scale
:
1
,
opacity
:
1
,
y
:
0
}
}
exit=
{
{
scale
:
0.85
,
opacity
:
0
,
y
:
30
}
}
exit=
{
{
scale
:
0.85
,
opacity
:
0
,
y
:
30
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
400
,
damping
:
22
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
400
,
damping
:
22
}
}
>
>
<
div
className=
"flex items-center justify-between"
>
{
/* Ribbon banner header */
}
<
h2
className=
"text-lg font-black"
>
تعديل الملف الشخصي
</
h2
>
<
div
className=
"relative flex items-center justify-center py-3"
style=
{
{
background
:
'linear-gradient(to bottom, rgba(255,200,60,0.12), transparent)'
,
borderBottom
:
'2px solid var(--color-border)'
,
clipPath
:
'polygon(0 0, 100% 0, 95% 100%, 5% 100%)'
,
}
}
>
<
Shield
size=
{
18
}
className=
"text-[#FFC83D] ml-2"
/>
<
h2
className=
"text-lg font-black"
>
تعديل الملف
</
h2
>
</
div
>
{
/* Close button */
}
<
motion
.
button
<
motion
.
button
whileTap=
{
{
scale
:
0.85
}
}
whileTap=
{
{
scale
:
0.85
}
}
onClick=
{
()
=>
setEditOpen
(
false
)
}
onClick=
{
()
=>
setEditOpen
(
false
)
}
className=
"p-2 rounded-xl bg-surface-3 border-2 border-border"
className=
"absolute top-3 left-3 p-2 rounded-xl"
style=
{
{
background
:
'var(--color-surface-3)'
,
border
:
'2px solid var(--color-border)'
,
}
}
>
>
<
X
size=
{
16
}
className=
"text-text-muted"
/>
<
X
size=
{
14
}
className=
"text-text-muted"
/>
</
motion
.
button
>
</
motion
.
button
>
</
div
>
<
div
className=
"flex flex-col gap-4"
>
{
/* Form */
}
<
div
className=
"flex flex-col gap-4 px-5 pb-5"
>
<
div
className=
"flex flex-col gap-1.5"
>
<
div
className=
"flex flex-col gap-1.5"
>
<
label
className=
"text-xs font-black text-text-secondary"
>
الاسم
</
label
>
<
label
className=
"text-xs font-black text-text-secondary"
>
الاسم
</
label
>
<
input
<
input
type=
"text"
type=
"text"
value=
{
editForm
.
display_name
}
value=
{
editForm
.
display_name
}
onChange=
{
(
e
)
=>
setEditForm
({
...
editForm
,
display_name
:
e
.
target
.
value
})
}
onChange=
{
(
e
)
=>
setEditForm
({
...
editForm
,
display_name
:
e
.
target
.
value
})
}
className=
"px-4 py-3 rounded-xl bg-surface-3 border-3 border-border focus:border-[#FFC83D]/60 text-sm font-bold outline-none transition-colors"
className=
"px-4 py-3 rounded-xl text-sm font-bold outline-none transition-colors"
style=
{
{
background
:
'var(--color-surface-3)'
,
border
:
'3px solid var(--color-border)'
,
boxShadow
:
'inset 0 2px 4px rgba(0,0,0,0.3)'
,
}
}
maxLength=
{
30
}
maxLength=
{
30
}
dir=
"rtl"
dir=
"rtl"
/>
/>
...
@@ -234,21 +418,26 @@ export function ProfilePage() {
...
@@ -234,21 +418,26 @@ export function ProfilePage() {
<
textarea
<
textarea
value=
{
editForm
.
bio
}
value=
{
editForm
.
bio
}
onChange=
{
(
e
)
=>
setEditForm
({
...
editForm
,
bio
:
e
.
target
.
value
})
}
onChange=
{
(
e
)
=>
setEditForm
({
...
editForm
,
bio
:
e
.
target
.
value
})
}
className=
"px-4 py-3 rounded-xl bg-surface-3 border-3 border-border focus:border-[#FFC83D]/60 text-sm font-bold outline-none transition-colors resize-none h-20"
className=
"px-4 py-3 rounded-xl text-sm font-bold outline-none transition-colors resize-none h-20"
style=
{
{
background
:
'var(--color-surface-3)'
,
border
:
'3px solid var(--color-border)'
,
boxShadow
:
'inset 0 2px 4px rgba(0,0,0,0.3)'
,
}
}
maxLength=
{
150
}
maxLength=
{
150
}
dir=
"rtl"
dir=
"rtl"
/>
/>
</
div
>
</
div
>
</
div
>
<
Button
<
Button
onClick=
{
handleSave
}
onClick=
{
handleSave
}
loading=
{
saving
}
loading=
{
saving
}
disabled=
{
!
editForm
.
display_name
.
trim
()
}
disabled=
{
!
editForm
.
display_name
.
trim
()
}
className=
"w-[80%] mx-auto
"
className=
"w-[80%] mx-auto mt-1
"
>
>
حفظ التعديلات
حفظ التعديلات
</
Button
>
</
Button
>
</
div
>
</
motion
.
div
>
</
motion
.
div
>
</
motion
.
div
>
</
motion
.
div
>
)
}
)
}
...
@@ -256,13 +445,3 @@ export function ProfilePage() {
...
@@ -256,13 +445,3 @@ export function ProfilePage() {
</
PageTransition
>
</
PageTransition
>
)
)
}
}
function
StatBox
({
icon
,
value
,
label
}:
{
icon
:
React
.
ReactNode
;
value
:
number
|
string
;
label
:
string
})
{
return
(
<
div
className=
"flex flex-col items-center gap-1.5 p-3 rounded-2xl bg-surface-1 border-3 border-border"
>
{
icon
}
<
span
className=
"text-base font-black"
>
{
value
}
</
span
>
<
span
className=
"text-[9px] text-text-muted text-center leading-tight font-bold"
>
{
label
}
</
span
>
</
div
>
)
}
src/pages/SettingsPage.tsx
View file @
4d37e0d3
import
{
Volume2
,
VolumeX
,
Info
,
LogOut
,
Trash2
,
Bug
}
from
'lucide-react'
import
{
Volume2
,
VolumeX
,
Info
,
LogOut
,
Trash2
,
Bug
,
Settings
}
from
'lucide-react'
import
{
motion
}
from
'framer-motion'
import
{
motion
}
from
'framer-motion'
import
{
useNavigate
}
from
'react-router-dom'
import
{
useNavigate
}
from
'react-router-dom'
import
{
PageTransition
}
from
'../components/layout/PageTransition'
import
{
PageTransition
}
from
'../components/layout/PageTransition'
import
{
Card
}
from
'../components/ui/Card'
import
{
useUIStore
}
from
'../stores/uiStore'
import
{
useUIStore
}
from
'../stores/uiStore'
import
{
supabase
}
from
'../lib/supabase'
import
{
supabase
}
from
'../lib/supabase'
...
@@ -17,49 +16,83 @@ export function SettingsPage() {
...
@@ -17,49 +16,83 @@ export function SettingsPage() {
return
(
return
(
<
PageTransition
>
<
PageTransition
>
<
h1
className=
"text-2xl font-black"
>
الاعدادات
</
h1
>
{
/* Header */
}
<
div
className=
"flex items-center gap-3"
>
<
Settings
size=
{
22
}
className=
"text-[#FFC83D]"
/>
<
h1
className=
"text-2xl font-black text-text-primary"
>
الاعدادات
</
h1
>
</
div
>
<
div
className=
"h-[3px] rounded-full bg-gradient-to-l from-[#FFC83D] via-[#FFC83D]/40 to-transparent"
/>
<
div
className=
"flex flex-col gap-4"
>
<
div
className=
"flex flex-col gap-3"
>
<
Card
className=
"flex items-center justify-between"
>
{
/* Sound toggle */
}
<
div
className=
"game-panel !p-4 flex items-center justify-between"
>
<
div
className=
"flex items-center gap-3"
>
<
div
className=
"flex items-center gap-3"
>
{
soundEnabled
?
<
Volume2
size=
{
20
}
className=
"text-cyan"
/>
:
<
VolumeX
size=
{
20
}
className=
"text-text-muted"
/>
}
{
soundEnabled
?
(
<
span
className=
"text-sm font-bold"
>
الاصوات
</
span
>
<
div
className=
"w-10 h-10 rounded-xl bg-[#00E5CC]/15 border-2 border-[#00E5CC]/30 flex items-center justify-center"
>
<
Volume2
size=
{
18
}
className=
"text-[#00E5CC]"
/>
</
div
>
</
div
>
)
:
(
<
div
className=
"w-10 h-10 rounded-xl bg-surface-3 border-2 border-border flex items-center justify-center"
>
<
VolumeX
size=
{
18
}
className=
"text-text-muted"
/>
</
div
>
)
}
<
div
>
<
span
className=
"text-sm font-black block"
>
الاصوات
</
span
>
<
span
className=
"text-[11px] text-text-muted font-bold"
>
تشغيل المؤثرات الصوتية
</
span
>
</
div
>
</
div
>
{
/* Game-style toggle */
}
<
motion
.
button
<
motion
.
button
className=
{
`w-14 h-7 rounded-full p-1 border-2 ${soundEnabled ? 'bg-cyan/20 border-cyan' : 'bg-surface-3 border-border'}`
}
className=
{
`w-[48px] h-[26px] rounded-full p-[3px] border-3 transition-colors ${
soundEnabled
? 'bg-[#00E5CC]/20 border-[#00E5CC]'
: 'bg-surface-3 border-border'
}`
}
style=
{
{
boxShadow
:
soundEnabled
?
'0 0 8px rgba(0,229,204,0.3)'
:
'inset 0 2px 4px rgba(0,0,0,0.3)'
}
}
onClick=
{
()
=>
setSoundEnabled
(
!
soundEnabled
)
}
onClick=
{
()
=>
setSoundEnabled
(
!
soundEnabled
)
}
whileTap=
{
{
scale
:
0.9
}
}
whileTap=
{
{
scale
:
0.9
}
}
>
>
<
motion
.
div
<
motion
.
div
className=
{
`w-5 h-5 rounded-full ${soundEnabled ? 'bg-cyan' : 'bg-text-muted'}`
}
className=
{
`w-[18px] h-[18px] rounded-full shadow-md ${
animate=
{
{
x
:
soundEnabled
?
0
:
28
}
}
soundEnabled ? 'bg-[#00E5CC]' : 'bg-text-muted'
}`
}
animate=
{
{
x
:
soundEnabled
?
0
:
20
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
30
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
30
}
}
/>
/>
</
motion
.
button
>
</
motion
.
button
>
</
Card
>
</
div
>
<
Card
className=
"flex items-center gap-3"
>
{
/* Report bug */
}
<
Info
size=
{
20
}
className=
"text-gold"
/>
<
div
className=
"game-panel !p-4 flex items-center gap-3"
>
<
div
className=
"w-10 h-10 rounded-xl bg-[#B44DFF]/15 border-2 border-[#B44DFF]/30 flex items-center justify-center"
>
<
Bug
size=
{
18
}
className=
"text-[#B44DFF]"
/>
</
div
>
<
div
>
<
div
>
<
p
className=
"text-sm font-bold"
>
EL3AB Player
</
p
>
<
p
className=
"text-sm font-black"
>
الابلاغ عن مشكلة
</
p
>
<
p
className=
"text-xs text-text-muted font-semibold"
>
الاصدار 1.0.0
</
p
>
<
p
className=
"text-[11px] text-text-muted font-bold"
>
ساعدنا في تحسين التطبيق
</
p
>
</
div
>
</
div
>
</
div
>
</
Card
>
<
Card
className=
"flex items-center gap-3"
>
{
/* Version info - recessed panel */
}
<
Bug
size=
{
20
}
className=
"text-purple"
/>
<
div
className=
"game-panel !p-4 flex items-center gap-3"
style=
{
{
boxShadow
:
'inset 0 3px 8px rgba(0,0,0,0.3), inset 0 1px 2px rgba(0,0,0,0.2)'
}
}
>
<
div
className=
"w-10 h-10 rounded-xl bg-[#FFC83D]/15 border-2 border-[#FFC83D]/30 flex items-center justify-center"
>
<
Info
size=
{
18
}
className=
"text-[#FFC83D]"
/>
</
div
>
<
div
>
<
div
>
<
p
className=
"text-sm font-bold"
>
الابلاغ عن مشكلة
</
p
>
<
p
className=
"text-sm font-bold"
>
EL3AB Player
</
p
>
<
p
className=
"text-xs text-text-muted font-semibold"
>
ساعدنا في تحسين التطبيق
</
p
>
<
p
className=
"text-[11px] text-text-muted font-semibold"
>
الاصدار 1.0.0
</
p
>
</
div
>
</
div
>
</
div
>
</
Card
>
</
div
>
</
div
>
<
div
className=
"flex flex-col gap-3 mt-4"
>
{
/* Logout and Delete */
}
<
div
className=
"flex flex-col gap-3 mt-4 items-center"
>
<
motion
.
button
<
motion
.
button
whileTap=
{
{
scale
:
0.9
5
}
}
whileTap=
{
{
scale
:
0.9
3
}
}
onClick=
{
handleLogout
}
onClick=
{
handleLogout
}
className=
"
flex items-center justify-center gap-2 w-[80%] mx-auto px-6 py-3 rounded-2xl bg-coral/10 border-2 border-coral/30 text-coral font-bold
text-sm"
className=
"
btn-3d flex items-center justify-center gap-2.5 w-[70%] px-6 py-3.5 rounded-2xl bg-[#FF5252] text-white font-black
text-sm"
>
>
<
LogOut
size=
{
16
}
/>
<
LogOut
size=
{
16
}
/>
<
span
>
تسجيل الخروج
</
span
>
<
span
>
تسجيل الخروج
</
span
>
...
@@ -67,11 +100,11 @@ export function SettingsPage() {
...
@@ -67,11 +100,11 @@ export function SettingsPage() {
<
motion
.
button
<
motion
.
button
disabled
disabled
className=
"flex items-center justify-center gap-2 w-[
80%] mx-auto px-6 py-3 rounded-2xl bg-surface-2 border-2 border-border text-text-muted font-bold text-sm opacity-5
0 cursor-not-allowed"
className=
"flex items-center justify-center gap-2 w-[
70%] px-6 py-3 rounded-2xl bg-surface-2 border-2 border-border text-text-muted font-bold text-sm opacity-4
0 cursor-not-allowed"
>
>
<
Trash2
size=
{
16
}
/>
<
Trash2
size=
{
16
}
/>
<
span
>
حذف الحساب
</
span
>
<
span
>
حذف الحساب
</
span
>
<
span
className=
"text-[10px] bg-surface-3 px-2 py-0.5 rounded-full"
>
قريبا
</
span
>
<
span
className=
"text-[10px] bg-surface-3 px-2 py-0.5 rounded-full
mr-1
"
>
قريبا
</
span
>
</
motion
.
button
>
</
motion
.
button
>
</
div
>
</
div
>
</
PageTransition
>
</
PageTransition
>
...
...
src/pages/ShopPage.tsx
View file @
4d37e0d3
import
{
useState
}
from
'react'
import
{
useState
}
from
'react'
import
{
motion
,
AnimatePresence
}
from
'framer-motion'
import
{
motion
,
AnimatePresence
}
from
'framer-motion'
import
{
Coins
,
Gem
,
ShoppingBag
,
Sparkles
,
Check
,
X
}
from
'lucide-react'
import
{
Coins
,
Gem
,
ShoppingBag
,
Sparkles
,
Check
,
X
,
Shield
}
from
'lucide-react'
import
{
PageTransition
}
from
'../components/layout/PageTransition'
import
{
PageTransition
}
from
'../components/layout/PageTransition'
import
{
Button
}
from
'../components/ui/Button'
import
{
useAuthStore
}
from
'../stores/authStore'
import
{
useAuthStore
}
from
'../stores/authStore'
import
{
useShop
,
type
Cosmetic
}
from
'../hooks/useShop'
import
{
useShop
,
type
Cosmetic
}
from
'../hooks/useShop'
import
{
RARITY_COLORS
}
from
'../lib/constants'
import
{
RARITY_COLORS
}
from
'../lib/constants'
...
@@ -26,6 +25,14 @@ const RARITY_LABELS: Record<string, string> = {
...
@@ -26,6 +25,14 @@ const RARITY_LABELS: Record<string, string> = {
legendary
:
'اسطوري'
,
legendary
:
'اسطوري'
,
}
}
const
RARITY_GLOW
:
Record
<
string
,
string
>
=
{
common
:
'none'
,
uncommon
:
'0 0 8px rgba(77,139,255,0.3)'
,
rare
:
'0 0 12px rgba(180,77,255,0.4)'
,
epic
:
'0 0 16px rgba(255,82,82,0.4)'
,
legendary
:
'0 0 20px rgba(255,200,61,0.5), 0 0 40px rgba(255,200,61,0.2)'
,
}
export
function
ShopPage
()
{
export
function
ShopPage
()
{
const
{
profile
}
=
useAuthStore
()
const
{
profile
}
=
useAuthStore
()
const
[
filter
,
setFilter
]
=
useState
<
FilterType
>
(
'all'
)
const
[
filter
,
setFilter
]
=
useState
<
FilterType
>
(
'all'
)
...
@@ -58,33 +65,41 @@ export function ShopPage() {
...
@@ -58,33 +65,41 @@ export function ShopPage() {
return
(
return
(
<
PageTransition
className=
"flex flex-col gap-5"
>
<
PageTransition
className=
"flex flex-col gap-5"
>
{
/* Header */
}
<
div
className=
"flex items-center justify-between"
>
<
div
className=
"flex items-center justify-between"
>
<
div
className=
"flex items-center gap-2.5"
>
<
div
className=
"flex items-center gap-2.5"
>
<
ShoppingBag
size=
{
2
4
}
className=
"text-[#FFC83D]"
/>
<
ShoppingBag
size=
{
2
2
}
className=
"text-[#FFC83D]"
/>
<
h1
className=
"text-2xl font-black"
>
المتجر
</
h1
>
<
h1
className=
"text-2xl font-black"
>
المتجر
</
h1
>
</
div
>
</
div
>
{
/* Currency capsules */
}
<
div
className=
"flex items-center gap-2"
>
<
div
className=
"flex items-center gap-2"
>
<
div
className=
"flex items-center gap-1.5 border-3 border-[#FFC83D]/50 rounded-full px-3 py-1.5 bg-[#FFC83D]/10"
>
<
div
className=
"flex items-center gap-1.5 border-3 border-[#FFC83D]/50 rounded-full px-3 py-1.5 bg-[#FFC83D]/10"
style=
{
{
boxShadow
:
'inset 0 2px 4px rgba(0,0,0,0.2)'
}
}
>
<
Coins
size=
{
14
}
className=
"text-[#FFC83D]"
/>
<
Coins
size=
{
14
}
className=
"text-[#FFC83D]"
/>
<
span
className=
"text-sm font-black text-[#FFC83D]"
>
{
profile
?.
coins
||
0
}
</
span
>
<
span
className=
"text-sm font-black text-[#FFC83D]"
>
{
profile
?.
coins
||
0
}
</
span
>
</
div
>
</
div
>
<
div
className=
"flex items-center gap-1.5 border-3 border-[#B44DFF]/50 rounded-full px-3 py-1.5 bg-[#B44DFF]/10"
>
<
div
className=
"flex items-center gap-1.5 border-3 border-[#B44DFF]/50 rounded-full px-3 py-1.5 bg-[#B44DFF]/10"
style=
{
{
boxShadow
:
'inset 0 2px 4px rgba(0,0,0,0.2)'
}
}
>
<
Gem
size=
{
14
}
className=
"text-[#B44DFF]"
/>
<
Gem
size=
{
14
}
className=
"text-[#B44DFF]"
/>
<
span
className=
"text-sm font-black text-[#B44DFF]"
>
{
profile
?.
gems
||
0
}
</
span
>
<
span
className=
"text-sm font-black text-[#B44DFF]"
>
{
profile
?.
gems
||
0
}
</
span
>
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
<
div
className=
"flex gap-2 overflow-x-auto pb-1 -mx-1 px-1 scrollbar-hide"
>
{
/* Category filter - thick bordered tabs */
}
<
div
className=
"flex gap-2 overflow-x-auto pb-1 no-scrollbar"
>
{
FILTER_OPTIONS
.
map
((
opt
)
=>
(
{
FILTER_OPTIONS
.
map
((
opt
)
=>
(
<
motion
.
button
<
motion
.
button
key=
{
opt
.
key
}
key=
{
opt
.
key
}
onClick=
{
()
=>
setFilter
(
opt
.
key
)
}
onClick=
{
()
=>
setFilter
(
opt
.
key
)
}
className=
{
`px-4 py-2
rounded-xl text-xs font-bold whitespace-nowrap border-2 transition-colors
${
className=
{
`px-4 py-2
.5 rounded-xl text-xs font-black whitespace-nowrap border-3 transition-all
${
filter === opt.key
filter === opt.key
? 'bg-
[#FF8C42] border-[#FF8C42] text-white
'
? 'bg-
gradient-to-b from-[#FF8C42] to-[#E06820] border-[#FF8C42] text-white shadow-[0_3px_0_#A84510]
'
: 'bg-surface-2 border-border text-text-muted
hover:border-[#FF8C42]/40
'
: 'bg-surface-2 border-border text-text-muted'
}`
}
}`
}
style=
{
filter
!==
opt
.
key
?
{
boxShadow
:
'inset 0 2px 4px rgba(0,0,0,0.3)'
}
:
{}
}
whileTap=
{
{
scale
:
0.93
}
}
whileTap=
{
{
scale
:
0.93
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
20
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
20
}
}
>
>
...
@@ -102,16 +117,21 @@ export function ShopPage() {
...
@@ -102,16 +117,21 @@ export function ShopPage() {
/>
/>
</
div
>
</
div
>
)
:
items
.
length
===
0
?
(
)
:
items
.
length
===
0
?
(
<
div
className=
"flex-1 flex flex-col items-center justify-center py-16 gap-
4
"
>
<
div
className=
"flex-1 flex flex-col items-center justify-center py-16 gap-
5
"
>
<
motion
.
div
<
motion
.
div
className=
"w-20 h-20 rounded-2xl bg-surface-2 border-3 border-border flex items-center justify-center"
className=
"w-24 h-24 flex items-center justify-center"
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
}
}
initial=
{
{
scale
:
0
,
rotate
:
-
10
}
}
initial=
{
{
scale
:
0
,
rotate
:
-
10
}
}
animate=
{
{
scale
:
1
,
rotate
:
0
}
}
animate=
{
{
scale
:
1
,
rotate
:
0
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
300
,
damping
:
18
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
300
,
damping
:
18
}
}
>
>
<
ShoppingBag
size=
{
32
}
className=
"text-text-muted"
/>
<
div
className=
"w-full h-full bg-gradient-to-br from-surface-2 to-surface-3 flex items-center justify-center"
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
}
}
>
<
ShoppingBag
size=
{
36
}
className=
"text-text-muted"
/>
</
div
>
</
motion
.
div
>
</
motion
.
div
>
<
p
className=
"text-text-muted text-sm font-b
old"
>
لا توجد عناصر متاحة
</
p
>
<
p
className=
"text-text-muted text-sm font-b
lack"
>
لا توجد عناصر
</
p
>
</
div
>
</
div
>
)
:
(
)
:
(
<
div
className=
"grid grid-cols-2 gap-4"
>
<
div
className=
"grid grid-cols-2 gap-4"
>
...
@@ -120,37 +140,48 @@ export function ShopPage() {
...
@@ -120,37 +140,48 @@ export function ShopPage() {
const
isEquipped
=
equippedIds
.
includes
(
item
.
id
)
const
isEquipped
=
equippedIds
.
includes
(
item
.
id
)
const
rarityColor
=
RARITY_COLORS
[
item
.
rarity
]
const
rarityColor
=
RARITY_COLORS
[
item
.
rarity
]
const
isLegendary
=
item
.
rarity
===
'legendary'
const
isLegendary
=
item
.
rarity
===
'legendary'
const
rotation
=
index
%
2
===
0
?
'-1deg'
:
'1deg'
return
(
return
(
<
motion
.
button
<
motion
.
button
key=
{
item
.
id
}
key=
{
item
.
id
}
onClick=
{
()
=>
setSelectedItem
(
item
)
}
onClick=
{
()
=>
setSelectedItem
(
item
)
}
className=
{
`relative flex flex-col items-center gap-2 p-4 rounded-2xl bg-surface-1 border-3 overflow-hidden text-center ${isLegendary ? 'animate-pulse-glow' : ''}`
}
className=
"relative flex flex-col items-center gap-2 p-4 rounded-2xl bg-surface-1 border-3 overflow-hidden text-center"
style=
{
{
borderColor
:
rarityColor
}
}
style=
{
{
borderColor
:
rarityColor
,
transform
:
`rotate(${rotation})`
,
boxShadow
:
RARITY_GLOW
[
item
.
rarity
],
}
}
initial=
{
{
opacity
:
0
,
y
:
16
,
scale
:
0.9
}
}
initial=
{
{
opacity
:
0
,
y
:
16
,
scale
:
0.9
}
}
animate=
{
{
opacity
:
1
,
y
:
0
,
scale
:
1
}
}
animate=
{
{
opacity
:
1
,
y
:
0
,
scale
:
1
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
400
,
damping
:
22
,
delay
:
index
*
0.06
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
400
,
damping
:
22
,
delay
:
index
*
0.06
}
}
whileHover=
{
{
y
:
-
6
,
scale
:
1.03
,
boxShadow
:
`0 12px 30px ${rarityColor}30`
}
}
whileHover=
{
{
y
:
-
6
,
scale
:
1.03
,
rotate
:
0
}
}
whileTap=
{
{
scale
:
0.95
}
}
whileTap=
{
{
scale
:
0.95
}
}
>
>
{
/* Legendary shimmer overlay */
}
{
isLegendary
&&
(
{
isLegendary
&&
(
<
motion
.
div
<
div
className=
"absolute inset-0 rounded-2xl pointer-events-none animate-shimmer"
className=
"absolute inset-0 rounded-2xl pointer-events-none"
style=
{
{
background
:
'linear-gradient(110deg, transparent 25%, rgba(255,200,61,0.1) 50%, transparent 75%)'
,
backgroundSize
:
'200% 100%'
}
}
style=
{
{
boxShadow
:
`inset 0 0 24px ${rarityColor}25, 0 0 20px ${rarityColor}20`
}
}
animate=
{
{
opacity
:
[
0.4
,
1
,
0.4
]
}
}
transition=
{
{
duration
:
2
,
repeat
:
Infinity
}
}
/>
/>
)
}
)
}
{
/* Owned stamp - shield overlay */
}
{
isOwned
&&
(
{
isOwned
&&
(
<
div
className=
"absolute top-2 left-2 z-10 flex items-center gap-1 bg-green-500/20 border-2 border-green-500/50 rounded-lg px-2 py-0.5"
>
<
div
className=
"absolute top-2 left-2 z-10 w-10 h-10 flex items-center justify-center"
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
}
}
>
<
div
className=
"w-full h-full bg-green-500/20 border border-green-500/50 flex items-center justify-center flex-col"
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
}
}
>
<
Check
size=
{
10
}
className=
"text-green-400"
/>
<
Check
size=
{
10
}
className=
"text-green-400"
/>
<
span
className=
"text-[9px] font-black text-green-400
"
>
<
span
className=
"text-[7px] font-black text-green-400 leading-none mt-0.5
"
>
{
isEquipped
?
'مفعّل'
:
'مملوك'
}
{
isEquipped
?
'مفعّل'
:
'مملوك'
}
</
span
>
</
span
>
</
div
>
</
div
>
</
div
>
)
}
)
}
{
/* Preview */
}
<
div
<
div
className=
"w-full aspect-square rounded-xl bg-surface-2 border-2 flex items-center justify-center"
className=
"w-full aspect-square rounded-xl bg-surface-2 border-2 flex items-center justify-center"
style=
{
{
borderColor
:
`${rarityColor}40`
}
}
style=
{
{
borderColor
:
`${rarityColor}40`
}
}
...
@@ -169,16 +200,16 @@ export function ShopPage() {
...
@@ -169,16 +200,16 @@ export function ShopPage() {
)
}
)
}
</
div
>
</
div
>
<
div
className=
"flex items-center gap-1.5"
>
{
/* Name - ribbon style */
}
<
div
<
div
className=
"w-[110%] -mx-[5%] py-1.5 bg-surface-2 border-t-2 border-b-2"
className=
"w-2.5 h-2.5 rounded-full border-2"
style=
{
{
borderColor
:
`${rarityColor}30`
}
}
style=
{
{
backgroundColor
:
rarityColor
,
borderColor
:
`${rarityColor}80`
}
}
>
/>
<
span
className=
"text-xs font-black text-text-primary truncate block px-2"
>
<
span
className=
"text-xs font-black text-text-primary truncate max-w-[90px]"
>
{
item
.
name_ar
}
{
item
.
name_ar
}
</
span
>
</
span
>
</
div
>
</
div
>
{
/* Price ribbon */
}
{
!
isOwned
&&
(
{
!
isOwned
&&
(
<
div
className=
"flex items-center gap-1.5 bg-surface-2 border-2 border-border rounded-lg px-2.5 py-1"
>
<
div
className=
"flex items-center gap-1.5 bg-surface-2 border-2 border-border rounded-lg px-2.5 py-1"
>
{
item
.
price_gems
?
(
{
item
.
price_gems
?
(
...
@@ -200,6 +231,7 @@ export function ShopPage() {
...
@@ -200,6 +231,7 @@ export function ShopPage() {
</
div
>
</
div
>
)
}
)
}
{
/* Purchase Modal */
}
<
AnimatePresence
>
<
AnimatePresence
>
{
selectedItem
&&
(
{
selectedItem
&&
(
<
motion
.
div
<
motion
.
div
...
@@ -210,16 +242,20 @@ export function ShopPage() {
...
@@ -210,16 +242,20 @@ export function ShopPage() {
onClick=
{
()
=>
!
purchasing
&&
setSelectedItem
(
null
)
}
onClick=
{
()
=>
!
purchasing
&&
setSelectedItem
(
null
)
}
>
>
<
motion
.
div
<
motion
.
div
className=
"w-full max-w-[340px]
rounded-3xl bg-surface-1 border-3 border-border p-6 flex flex-col items-center gap-4
relative overflow-hidden"
className=
"w-full max-w-[340px]
game-panel !p-0 flex flex-col items-center
relative overflow-hidden"
initial=
{
{
scale
:
0.7
,
opacity
:
0
,
rotate
:
-
3
}
}
initial=
{
{
scale
:
0.7
,
opacity
:
0
,
rotate
:
-
3
}
}
animate=
{
{
scale
:
1
,
opacity
:
1
,
rotate
:
0
}
}
animate=
{
{
scale
:
1
,
opacity
:
1
,
rotate
:
0
}
}
exit=
{
{
scale
:
0.7
,
opacity
:
0
,
rotate
:
3
}
}
exit=
{
{
scale
:
0.7
,
opacity
:
0
,
rotate
:
3
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
350
,
damping
:
22
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
350
,
damping
:
22
}
}
onClick=
{
(
e
)
=>
e
.
stopPropagation
()
}
onClick=
{
(
e
)
=>
e
.
stopPropagation
()
}
>
>
{
/* Rarity-colored header */
}
<
div
className=
"w-full h-2 rounded-t-2xl"
style=
{
{
backgroundColor
:
RARITY_COLORS
[
selectedItem
.
rarity
]
}
}
/>
<
div
className=
"p-6 flex flex-col items-center gap-4 w-full"
>
<
button
<
button
onClick=
{
()
=>
!
purchasing
&&
setSelectedItem
(
null
)
}
onClick=
{
()
=>
!
purchasing
&&
setSelectedItem
(
null
)
}
className=
"absolute top-4 left-4 w-9 h-9 rounded-full bg-surface-2 border-2 border-border flex items-center justify-center hover:border-coral
"
className=
"absolute top-5 left-5 w-9 h-9 rounded-full bg-surface-2 border-2 border-border flex items-center justify-center hover:border-[#FF5252]/50
"
>
>
<
X
size=
{
16
}
className=
"text-text-muted"
/>
<
X
size=
{
16
}
className=
"text-text-muted"
/>
</
button
>
</
button
>
...
@@ -243,7 +279,7 @@ export function ShopPage() {
...
@@ -243,7 +279,7 @@ export function ShopPage() {
)
:
(
)
:
(
<>
<>
<
div
<
div
className=
"w-full aspect-[4/3] rounded-2xl bg-surface-2 border-2 flex items-center justify-center mt-6
"
className=
"w-full aspect-[4/3] rounded-2xl bg-surface-2 border-2 flex items-center justify-center mt-4
"
style=
{
{
borderColor
:
`${RARITY_COLORS[selectedItem.rarity]}40`
}
}
style=
{
{
borderColor
:
`${RARITY_COLORS[selectedItem.rarity]}40`
}
}
>
>
{
selectedItem
.
preview_url
?
(
{
selectedItem
.
preview_url
?
(
...
@@ -293,20 +329,21 @@ export function ShopPage() {
...
@@ -293,20 +329,21 @@ export function ShopPage() {
<
span
className=
"text-sm font-black text-green-400"
>
مفعّل حاليا
</
span
>
<
span
className=
"text-sm font-black text-green-400"
>
مفعّل حاليا
</
span
>
</
div
>
</
div
>
)
:
(
)
:
(
<
Button
<
motion
.
button
variant=
"cyan"
whileTap=
{
{
scale
:
0.93
}
}
size=
"md"
onClick=
{
handleEquip
}
onClick=
{
handleEquip
}
loading
=
{
purchasing
}
disabled
=
{
purchasing
}
className=
"w-full
"
className=
"btn-3d w-full py-3 rounded-xl bg-[#00E5CC] text-background text-sm font-black disabled:opacity-50
"
>
>
تفعيل
{
purchasing
?
'جاري...'
:
'تفعيل'
}
</
B
utton
>
</
motion
.
b
utton
>
)
}
)
}
</
div
>
</
div
>
)
:
(
)
:
(
<
div
className=
"flex flex-col gap-3 w-[70%] mx-auto"
>
<
div
className=
"flex flex-col gap-3 w-[70%] mx-auto"
>
<
div
className=
"flex items-center justify-center gap-2 bg-surface-2 rounded-xl px-4 py-3 border-2 border-border"
>
<
div
className=
"flex items-center justify-center gap-2 bg-surface-2 rounded-xl px-4 py-3 border-2 border-border"
style=
{
{
boxShadow
:
'inset 0 2px 4px rgba(0,0,0,0.2)'
}
}
>
{
selectedItem
.
price_gems
?
(
{
selectedItem
.
price_gems
?
(
<>
<>
<
Gem
size=
{
18
}
className=
"text-[#B44DFF]"
/>
<
Gem
size=
{
18
}
className=
"text-[#B44DFF]"
/>
...
@@ -319,20 +356,19 @@ export function ShopPage() {
...
@@ -319,20 +356,19 @@ export function ShopPage() {
</>
</>
)
}
)
}
</
div
>
</
div
>
<
Button
<
motion
.
button
variant=
"gold"
whileTap=
{
{
scale
:
0.93
}
}
size=
"md"
onClick=
{
handlePurchase
}
onClick=
{
handlePurchase
}
loading=
{
purchasing
}
disabled=
{
disabled=
{
selectedItem
.
price_gems
purchasing
||
(
selectedItem
.
price_gems
?
(
profile
?.
gems
||
0
)
<
(
selectedItem
.
price_gems
||
0
)
?
(
profile
?.
gems
||
0
)
<
(
selectedItem
.
price_gems
||
0
)
:
(
profile
?.
coins
||
0
)
<
(
selectedItem
.
price_coins
||
0
)
:
(
profile
?.
coins
||
0
)
<
(
selectedItem
.
price_coins
||
0
)
)
}
}
className=
"w-full
"
className=
"btn-3d w-full py-3 rounded-xl bg-gradient-to-b from-[#FFC83D] to-[#E5A800] text-background text-sm font-black disabled:opacity-50 disabled:grayscale
"
>
>
شراء
{
purchasing
?
'جاري...'
:
'شراء'
}
</
B
utton
>
</
motion
.
b
utton
>
{
(
selectedItem
.
price_gems
{
(
selectedItem
.
price_gems
?
(
profile
?.
gems
||
0
)
<
(
selectedItem
.
price_gems
||
0
)
?
(
profile
?.
gems
||
0
)
<
(
selectedItem
.
price_gems
||
0
)
:
(
profile
?.
coins
||
0
)
<
(
selectedItem
.
price_coins
||
0
))
&&
(
:
(
profile
?.
coins
||
0
)
<
(
selectedItem
.
price_coins
||
0
))
&&
(
...
@@ -342,6 +378,7 @@ export function ShopPage() {
...
@@ -342,6 +378,7 @@ export function ShopPage() {
)
}
)
}
</>
</>
)
}
)
}
</
div
>
</
motion
.
div
>
</
motion
.
div
>
</
motion
.
div
>
</
motion
.
div
>
)
}
)
}
...
...
src/pages/TournamentsPage.tsx
View file @
4d37e0d3
import
{
useState
}
from
'react'
import
{
useState
}
from
'react'
import
{
motion
,
AnimatePresence
}
from
'framer-motion'
import
{
motion
,
AnimatePresence
}
from
'framer-motion'
import
{
Trophy
,
Users
,
Clock
,
Coins
,
Gem
}
from
'lucide-react'
import
{
Trophy
,
Users
,
Clock
,
Coins
,
Gem
,
Shield
}
from
'lucide-react'
import
{
PageTransition
}
from
'../components/layout/PageTransition'
import
{
PageTransition
}
from
'../components/layout/PageTransition'
import
{
Card
}
from
'../components/ui/Card'
import
{
Button
}
from
'../components/ui/Button'
import
{
useAuthStore
}
from
'../stores/authStore'
import
{
useAuthStore
}
from
'../stores/authStore'
import
{
useTournaments
}
from
'../hooks/useTournaments'
import
{
useTournaments
}
from
'../hooks/useTournaments'
...
@@ -33,21 +31,11 @@ const TIME_CONTROL_LABELS: Record<string, string> = {
...
@@ -33,21 +31,11 @@ const TIME_CONTROL_LABELS: Record<string, string> = {
classical
:
'كلاسيكي'
,
classical
:
'كلاسيكي'
,
}
}
function
StatusBadge
({
status
}:
{
status
:
string
})
{
const
STATUS_CONFIG
:
Record
<
string
,
{
label
:
string
;
color
:
string
;
bg
:
string
}
>
=
{
const
config
:
Record
<
string
,
{
label
:
string
;
classes
:
string
}
>
=
{
registration
:
{
label
:
'مفتوحة'
,
color
:
'#00E5CC'
,
bg
:
'rgba(0,229,204,0.15)'
},
registration
:
{
label
:
'مفتوحة'
,
classes
:
'bg-[#00E5CC]/15 text-[#00E5CC] border-[#00E5CC]/50'
},
in_progress
:
{
label
:
'جارية'
,
color
:
'#FFC83D'
,
bg
:
'rgba(255,200,61,0.15)'
},
in_progress
:
{
label
:
'جارية'
,
classes
:
'bg-[#FFC83D]/15 text-[#FFC83D] border-[#FFC83D]/50 animate-pulse'
},
completed
:
{
label
:
'منتهية'
,
color
:
'#6E748C'
,
bg
:
'rgba(110,116,140,0.15)'
},
completed
:
{
label
:
'منتهية'
,
classes
:
'bg-surface-3 text-text-muted border-border'
},
cancelled
:
{
label
:
'ملغاة'
,
color
:
'#FF5252'
,
bg
:
'rgba(255,82,82,0.15)'
},
cancelled
:
{
label
:
'ملغاة'
,
classes
:
'bg-[#FF5252]/15 text-[#FF5252] border-[#FF5252]/50'
},
}
const
c
=
config
[
status
]
??
config
.
completed
return
(
<
span
className=
{
`px-3 py-1 rounded-full text-[10px] font-black border-2 ${c.classes}`
}
>
{
c
.
label
}
</
span
>
)
}
}
export
function
TournamentsPage
()
{
export
function
TournamentsPage
()
{
...
@@ -60,22 +48,32 @@ export function TournamentsPage() {
...
@@ -60,22 +48,32 @@ export function TournamentsPage() {
return
(
return
(
<
PageTransition
className=
"flex flex-col gap-5"
>
<
PageTransition
className=
"flex flex-col gap-5"
>
<
div
className=
"flex items-center gap-2.5"
>
{
/* Header - Banner shape */
}
<
Trophy
size=
{
24
}
className=
"text-[#FFC83D]"
/>
<
div
className=
"flex items-center gap-3"
>
<
h1
className=
"text-2xl font-black"
>
البطولات
</
h1
>
<
div
className=
"w-12 h-12 flex items-center justify-center rounded-xl bg-[#FFC83D]/15 border-3 border-[#FFC83D]/40"
style=
{
{
clipPath
:
'polygon(10% 0%, 90% 0%, 100% 50%, 90% 100%, 10% 100%, 0% 50%)'
}
}
>
<
Trophy
size=
{
22
}
className=
"text-[#FFC83D]"
/>
</
div
>
<
div
>
<
h1
className=
"text-2xl font-black text-text-primary"
>
البطولات
</
h1
>
<
div
className=
"h-[3px] mt-1 w-16 rounded-full bg-gradient-to-l from-[#FFC83D] to-transparent"
/>
</
div
>
</
div
>
</
div
>
{
/* Filter tabs - thick bordered buttons */
}
<
div
className=
"flex gap-2 overflow-x-auto no-scrollbar pb-1"
>
<
div
className=
"flex gap-2 overflow-x-auto no-scrollbar pb-1"
>
{
FILTERS
.
map
((
f
)
=>
(
{
FILTERS
.
map
((
f
)
=>
(
<
motion
.
button
<
motion
.
button
key=
{
f
.
label
}
key=
{
f
.
label
}
whileTap=
{
{
scale
:
0.93
}
}
whileTap=
{
{
scale
:
0.93
}
}
onClick=
{
()
=>
setActiveFilter
(
f
.
value
)
}
onClick=
{
()
=>
setActiveFilter
(
f
.
value
)
}
className=
{
`px-
4 py-2 rounded-xl text-xs font-bold whitespace-nowrap border-2 transition-colors
${
className=
{
`px-
5 py-2.5 rounded-xl text-xs font-black whitespace-nowrap border-3 transition-all
${
activeFilter === f.value
activeFilter === f.value
? 'bg-
[#00E5CC] border-[#00E5CC] text-background font-black
'
? 'bg-
gradient-to-b from-[#FFC83D] to-[#E5A800] border-[#FFC83D] text-background shadow-[0_4px_0_#B8860B]
'
: 'bg-surface-2 text-text-muted border-border
hover:border-[#00E5CC]/40
'
: 'bg-surface-2 text-text-muted border-border'
}`
}
}`
}
style=
{
activeFilter
!==
f
.
value
?
{
boxShadow
:
'inset 0 2px 4px rgba(0,0,0,0.3)'
}
:
{}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
20
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
20
}
}
>
>
{
f
.
label
}
{
f
.
label
}
...
@@ -86,28 +84,37 @@ export function TournamentsPage() {
...
@@ -86,28 +84,37 @@ export function TournamentsPage() {
{
loading
?
(
{
loading
?
(
<
div
className=
"flex flex-col gap-4"
>
<
div
className=
"flex flex-col gap-4"
>
{
[
1
,
2
,
3
].
map
((
i
)
=>
(
{
[
1
,
2
,
3
].
map
((
i
)
=>
(
<
div
key=
{
i
}
className=
"
rounded-2xl bg-surface-1 border-3 border-border p-5 animate-pulse
"
>
<
div
key=
{
i
}
className=
"
game-panel animate-pulse !p-5
"
>
<
div
className=
"w-36 h-5 rounded bg-surface-3 mb-3"
/>
<
div
className=
"w-36 h-5 rounded bg-surface-3 mb-3"
/>
<
div
className=
"w-24 h-3 rounded bg-surface-3"
/>
<
div
className=
"w-24 h-3 rounded bg-surface-3"
/>
</
div
>
</
div
>
))
}
))
}
</
div
>
</
div
>
)
:
tournaments
.
length
===
0
?
(
)
:
tournaments
.
length
===
0
?
(
<
div
className=
"flex flex-col items-center justify-center py-16 gap-
4
"
>
<
div
className=
"flex flex-col items-center justify-center py-16 gap-
5
"
>
<
motion
.
div
<
motion
.
div
className=
"w-20 h-20 rounded-2xl bg-surface-2 border-3 border-[#FFC83D]/30 flex items-center justify-center"
className=
"w-24 h-24 flex items-center justify-center"
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
}
}
initial=
{
{
scale
:
0
,
rotate
:
-
10
}
}
initial=
{
{
scale
:
0
,
rotate
:
-
10
}
}
animate=
{
{
scale
:
1
,
rotate
:
0
}
}
animate=
{
{
scale
:
1
,
rotate
:
0
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
300
,
damping
:
18
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
300
,
damping
:
18
}
}
>
>
<
Trophy
size=
{
32
}
className=
"text-[#FFC83D]"
/>
<
div
className=
"w-full h-full bg-gradient-to-br from-surface-2 to-surface-3 flex items-center justify-center"
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
}
}
>
<
Trophy
size=
{
36
}
className=
"text-[#FFC83D]/60"
/>
</
div
>
</
motion
.
div
>
</
motion
.
div
>
<
p
className=
"text-text-muted text-sm font-b
old
"
>
لا توجد بطولات
</
p
>
<
p
className=
"text-text-muted text-sm font-b
lack
"
>
لا توجد بطولات
</
p
>
</
div
>
</
div
>
)
:
(
)
:
(
<
div
className=
"flex flex-col gap-4"
>
<
div
className=
"flex flex-col gap-4"
>
<
AnimatePresence
>
<
AnimatePresence
>
{
tournaments
.
map
((
t
,
i
)
=>
(
{
tournaments
.
map
((
t
,
i
)
=>
{
const
statusCfg
=
STATUS_CONFIG
[
t
.
status
]
??
STATUS_CONFIG
.
completed
const
playerPercent
=
Math
.
min
((
t
.
registrations_count
/
t
.
max_players
)
*
100
,
100
)
return
(
<
motion
.
div
<
motion
.
div
key=
{
t
.
id
}
key=
{
t
.
id
}
initial=
{
{
opacity
:
0
,
y
:
20
,
scale
:
0.95
}
}
initial=
{
{
opacity
:
0
,
y
:
20
,
scale
:
0.95
}
}
...
@@ -115,76 +122,111 @@ export function TournamentsPage() {
...
@@ -115,76 +122,111 @@ export function TournamentsPage() {
exit=
{
{
opacity
:
0
,
y
:
-
16
}
}
exit=
{
{
opacity
:
0
,
y
:
-
16
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
400
,
damping
:
24
,
delay
:
i
*
0.06
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
400
,
damping
:
24
,
delay
:
i
*
0.06
}
}
>
>
<
Card
className=
"flex flex-col gap-3 relative"
>
<
div
className=
"game-panel relative overflow-hidden !p-5"
>
<
div
className=
"absolute top-4 left-4"
>
{
/* Status ribbon - diagonal corner badge */
}
<
StatusBadge
status=
{
t
.
status
}
/>
<
div
className=
"absolute top-0 left-0 w-28 h-7 flex items-center justify-center"
style=
{
{
background
:
statusCfg
.
bg
,
borderBottom
:
`2px solid ${statusCfg.color}`
,
borderRight
:
`2px solid ${statusCfg.color}`
,
borderBottomRightRadius
:
'12px'
,
}
}
>
<
span
className=
"text-[10px] font-black"
style=
{
{
color
:
statusCfg
.
color
}
}
>
{
statusCfg
.
label
}
</
span
>
</
div
>
</
div
>
<
h3
className=
"text-base font-black leading-tight pr-0 pl-20"
>
{
/* Tournament name */
}
<
h3
className=
"text-lg font-black leading-tight mt-5 mb-3"
>
{
t
.
name_ar
||
t
.
name
}
{
t
.
name_ar
||
t
.
name
}
</
h3
>
</
h3
>
<
div
className=
"flex items-center gap-4 text-xs text-text-muted font-bold"
>
{
/* Info row */
}
<
span
className=
"flex items-center gap-1.5 bg-surface-2 border-2 border-border rounded-lg px-2 py-1"
>
<
div
className=
"flex items-center gap-3 text-xs text-text-muted font-bold mb-3"
>
<
span
className=
"flex items-center gap-1.5 bg-surface-2 border-2 border-border rounded-lg px-2.5 py-1"
>
{
FORMAT_LABELS
[
t
.
format
]
??
t
.
format
}
{
FORMAT_LABELS
[
t
.
format
]
??
t
.
format
}
</
span
>
</
span
>
<
span
className=
"flex items-center gap-1"
>
<
span
className=
"flex items-center gap-1"
>
<
Clock
size=
{
12
}
/>
<
Clock
size=
{
12
}
/>
{
TIME_CONTROL_LABELS
[
t
.
time_control
]
??
t
.
time_control
}
{
TIME_CONTROL_LABELS
[
t
.
time_control
]
??
t
.
time_control
}
</
span
>
</
span
>
<
span
className=
"flex items-center gap-1"
>
<
Users
size=
{
12
}
/>
{
t
.
registrations_count
}
/
{
t
.
max_players
}
</
span
>
</
div
>
</
div
>
{
/* Prize display - treasure row */
}
{
(
t
.
prize_pool_coins
>
0
||
t
.
prize_pool_gems
>
0
)
&&
(
{
(
t
.
prize_pool_coins
>
0
||
t
.
prize_pool_gems
>
0
)
&&
(
<
div
className=
"flex items-center gap-3"
>
<
div
className=
"flex items-center gap-3 mb-3 p-2.5 rounded-xl bg-surface-2/50 border-2 border-[#FFC83D]/20"
style=
{
{
boxShadow
:
'inset 0 2px 6px rgba(0,0,0,0.2)'
}
}
>
{
t
.
prize_pool_coins
>
0
&&
(
{
t
.
prize_pool_coins
>
0
&&
(
<
span
className=
"flex items-center gap-1.5 text-[#FFC83D] font-black text-sm"
>
<
span
className=
"flex items-center gap-1.5 text-[#FFC83D] font-black text-sm"
>
<
Coins
size=
{
14
}
className=
"text-[#FFC83D]"
/>
<
Coins
size=
{
16
}
className=
"text-[#FFC83D]"
/>
{
t
.
prize_pool_coins
.
toLocaleString
(
'ar-EG'
)
}
{
t
.
prize_pool_coins
.
toLocaleString
(
'ar-EG'
)
}
</
span
>
</
span
>
)
}
)
}
{
t
.
prize_pool_gems
>
0
&&
(
{
t
.
prize_pool_gems
>
0
&&
(
<
span
className=
"flex items-center gap-1.5 text-[#B44DFF] font-black text-sm"
>
<
span
className=
"flex items-center gap-1.5 text-[#B44DFF] font-black text-sm"
>
<
Gem
size=
{
14
}
className=
"text-[#B44DFF]"
/>
<
Gem
size=
{
16
}
className=
"text-[#B44DFF]"
/>
{
t
.
prize_pool_gems
.
toLocaleString
(
'ar-EG'
)
}
{
t
.
prize_pool_gems
.
toLocaleString
(
'ar-EG'
)
}
</
span
>
</
span
>
)
}
)
}
</
div
>
</
div
>
)
}
)
}
{
/* Player count - game progress bar */
}
<
div
className=
"flex items-center gap-2.5 mb-3"
>
<
Users
size=
{
13
}
className=
"text-text-muted shrink-0"
/>
<
div
className=
"flex-1 h-3 rounded-full bg-surface-3 border-2 border-border overflow-hidden"
style=
{
{
boxShadow
:
'inset 0 2px 4px rgba(0,0,0,0.3)'
}
}
>
<
motion
.
div
className=
"h-full rounded-full"
style=
{
{
background
:
'linear-gradient(90deg, #00E5CC, #00B8A3)'
,
boxShadow
:
'inset 0 -1px 2px rgba(0,0,0,0.2), 0 0 6px rgba(0,229,204,0.3)'
,
}
}
initial=
{
{
width
:
0
}
}
animate=
{
{
width
:
`${playerPercent}%`
}
}
transition=
{
{
duration
:
0.8
,
ease
:
'easeOut'
}
}
/>
</
div
>
<
span
className=
"text-[11px] font-black text-text-muted whitespace-nowrap"
>
{
t
.
registrations_count
}
/
{
t
.
max_players
}
</
span
>
</
div
>
{
/* Register button */
}
{
t
.
status
===
'registration'
&&
user
&&
(
{
t
.
status
===
'registration'
&&
user
&&
(
<
div
className=
"flex justify-center mt-1
"
>
<
div
className=
"flex justify-center mt-2
"
>
{
isRegistered
(
t
.
id
)
?
(
{
isRegistered
(
t
.
id
)
?
(
<
div
className=
"flex items-center gap-2
"
>
<
div
className=
"flex items-center gap-2.5
"
>
<
span
className=
"px-4 py-2 rounded-xl bg-[#FFC83D]/15 border-2 border-[#FFC83D]/50 text-[#FFC83D] text-xs font-black"
>
<
span
className=
"px-4 py-2 rounded-xl bg-[#FFC83D]/15 border-2 border-[#FFC83D]/50 text-[#FFC83D] text-xs font-black"
>
مسجل
مسجل
</
span
>
</
span
>
<
motion
.
button
<
motion
.
button
whileTap=
{
{
scale
:
0.93
}
}
whileTap=
{
{
scale
:
0.93
}
}
onClick=
{
()
=>
unregister
(
t
.
id
)
}
onClick=
{
()
=>
unregister
(
t
.
id
)
}
className=
"px-3 py-2 rounded-xl bg-[#FF5252]/10 border-2 border-[#FF5252]/30 text-[#FF5252] text-xs font-bold
"
className=
"btn-3d px-3.5 py-2 rounded-xl bg-[#FF5252] text-white text-xs font-black
"
>
>
إلغاء
إلغاء
</
motion
.
button
>
</
motion
.
button
>
</
div
>
</
div
>
)
:
(
)
:
(
<
Button
<
motion
.
button
variant=
"cyan"
whileTap=
{
{
scale
:
0.93
}
}
size=
"sm"
onClick=
{
()
=>
register
(
t
.
id
)
}
onClick=
{
()
=>
register
(
t
.
id
)
}
className=
"w-[80%] mx-auto
"
className=
"btn-3d w-[75%] py-3 rounded-xl bg-gradient-to-b from-[#FFC83D] to-[#E5A800] text-background text-sm font-black
"
>
>
سجل
سجل الان
</
B
utton
>
</
motion
.
b
utton
>
)
}
)
}
</
div
>
</
div
>
)
}
)
}
</
Card
>
</
div
>
</
motion
.
div
>
</
motion
.
div
>
))
}
)
})
}
</
AnimatePresence
>
</
AnimatePresence
>
</
div
>
</
div
>
)
}
)
}
...
...
test-results/.last-run.json
0 → 100644
View file @
4d37e0d3
{
"status"
:
"failed"
,
"failedTests"
:
[]
}
\ No newline at end of file
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment