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
Hide 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
{
Header
}
from
'./Header'
import
{
BottomNav
}
from
'./BottomNav'
import
{
DecorativeBackground
}
from
'./DecorativeBackground'
import
{
ToastContainer
}
from
'../ui/ToastContainer'
import
{
usePresence
}
from
'../../hooks/usePresence'
import
{
useNotifications
}
from
'../../hooks/useNotifications'
...
...
@@ -10,12 +11,15 @@ export function AppShell() {
useNotifications
()
return
(
<
div
className=
"flex flex-col min-h-dvh bg-background"
>
<
Header
/>
<
main
className=
"flex-1 pb-28 overflow-y-auto"
>
<
Outlet
/>
</
main
>
<
BottomNav
/>
<
div
className=
"relative flex flex-col min-h-dvh overflow-hidden"
>
<
DecorativeBackground
/>
<
div
className=
"relative z-10 flex flex-col min-h-dvh"
>
<
Header
/>
<
main
className=
"flex-1 pb-28 overflow-y-auto"
>
<
Outlet
/>
</
main
>
<
BottomNav
/>
</
div
>
<
ToastContainer
/>
</
div
>
)
...
...
src/components/layout/BottomNav.tsx
View file @
4d37e0d3
...
...
@@ -10,52 +10,119 @@ const NAV_ITEMS = [
{
path
:
'/profile'
,
icon
:
User
,
label
:
'حسابي'
},
]
const
CENTER_INDEX
=
2
export
function
BottomNav
()
{
const
location
=
useLocation
()
const
navigate
=
useNavigate
()
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=
"flex items-center justify-around px-2 py-2 bg-surface-2 border-3 border-border rounded-2xl shadow-2xl shadow-black/50"
>
{
NAV_ITEMS
.
map
((
item
)
=>
{
const
isActive
=
location
.
pathname
===
item
.
path
const
Icon
=
item
.
icon
return
(
<
motion
.
button
key=
{
item
.
path
}
onClick=
{
()
=>
navigate
(
item
.
path
)
}
className=
{
`flex flex-col items-center gap-0.5 px-3 py-2 rounded-xl relative ${
isActive ? 'bg-gold/15' : ''
}`
}
whileTap=
{
{
scale
:
0.8
}
}
whileHover=
{
{
scale
:
1.05
}
}
>
<
motion
.
div
animate=
{
isActive
?
{
y
:
-
2
}
:
{
y
:
0
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
20
}
}
>
<
Icon
size=
{
22
}
className=
{
isActive
?
'text-gold'
:
'text-text-muted'
}
strokeWidth=
{
isActive
?
2.8
:
2
}
/>
</
motion
.
div
>
<
span
className=
{
`text-[10px] font-bold ${isActive ? 'text-gold' : 'text-text-muted'}`
}
>
{
item
.
label
}
</
span
>
{
isActive
&&
(
<
motion
.
div
className=
"absolute -bottom-0.5 w-6 h-[4px] rounded-full bg-gold"
layoutId=
"nav-indicator"
style=
{
{
boxShadow
:
'0 0 10px rgba(255, 200, 60, 0.8)'
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
30
}
}
/>
)
}
</
motion
.
button
>
)
})
}
{
/* Decorative corner accents */
}
<
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
isCenter
=
index
===
CENTER_INDEX
const
Icon
=
item
.
icon
return
(
<
motion
.
button
key=
{
item
.
path
}
onClick=
{
()
=>
navigate
(
item
.
path
)
}
className=
"relative flex flex-col items-center gap-0.5 px-2 py-1"
whileTap=
{
{
scale
:
0.8
}
}
>
{
/* 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
animate=
{
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
size=
{
isCenter
?
24
:
20
}
className=
{
isActive
?
'text-gold drop-shadow-[0_0_6px_rgba(255,200,60,0.6)]'
:
'text-text-muted'
}
strokeWidth=
{
isActive
?
2.8
:
2
}
/>
</
motion
.
div
>
</
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
}
</
motion
.
span
>
{
/* Active glow indicator */
}
{
isActive
&&
(
<
motion
.
div
className=
"absolute -bottom-1 w-5 h-[3px] rounded-full bg-gold"
layoutId=
"nav-indicator"
style=
{
{
boxShadow
:
'0 0 8px rgba(255, 200, 60, 0.7)'
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
30
}
}
/>
)
}
</
motion
.
button
>
)
})
}
</
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
>
...
...
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,55 +11,99 @@ export function Header() {
const
navigate
=
useNavigate
()
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=
"flex items-center justify-between px-4 py-2.5 bg-surface-2 border-3 border-border rounded-2xl shadow-lg shadow-black/30"
>
<
div
className=
"flex items-center gap-2"
>
<
GoldCrown
size=
{
28
}
animate=
{
false
}
/>
<
span
className=
"text-base font-black text-gold tracking-wider"
>
EL3AB
</
span
>
</
div
>
{
/* HUD Bar with diagonal bottom edge */
}
<
div
className=
"game-panel relative border-4 border-border bg-surface-2"
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
className=
"flex items-center gap-2"
>
{
/* Center: Level indicator */
}
{
profile
&&
(
<>
<
motion
.
div
className=
"flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-gold/15 border-2 border-gold/40"
whileTap=
{
{
scale
:
0.9
}
}
>
<
Coins
size=
{
14
}
className=
"text-gold"
/
>
<
span
className=
"text-xs font-black text-gold"
>
{
profile
.
coins
}
</
span
>
</
motion
.
div
>
<
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
>
)
}
{
profile
.
gems
>
0
&&
(
{
/* Resources section (left side in RTL) */
}
<
div
className=
"flex items-center gap-2"
>
{
profile
&&
(
<>
{
/* Coins capsule */
}
<
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-
gold/10 border-2 border-gold/40 shadow-[inset_0_0_8px_rgba(255,200,60,0.15)]
"
whileTap=
{
{
scale
:
0.9
}
}
>
<
Gem
size=
{
12
}
className=
"text-purple
"
/>
<
span
className=
"text-xs font-black text-
purple"
>
{
profile
.
gem
s
}
</
span
>
<
Coins
size=
{
14
}
className=
"text-gold
"
/>
<
span
className=
"text-xs font-black text-
gold"
>
{
profile
.
coin
s
}
</
span
>
</
motion
.
div
>
)
}
</>
)
}
<
motion
.
button
className=
"relative p-2.5 rounded-xl bg-surface-3 border-2 border-border"
onClick=
{
()
=>
navigate
(
'/notifications'
)
}
whileTap=
{
{
scale
:
0.85
}
}
whileHover=
{
{
scale
:
1.05
}
}
>
<
Bell
size=
{
18
}
className=
"text-text-secondary"
/>
{
unreadCount
>
0
&&
(
<
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"
initial=
{
{
scale
:
0
}
}
animate=
{
{
scale
:
1
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
15
}
}
>
<
span
className=
"text-[9px] font-black text-white"
>
{
unreadCount
>
9
?
'9+'
:
unreadCount
}
</
span
>
</
motion
.
div
>
{
/* Gems capsule */
}
{
profile
.
gems
>
0
&&
(
<
motion
.
div
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
}
}
>
<
Gem
size=
{
12
}
className=
"text-purple"
/>
<
span
className=
"text-xs font-black text-purple"
>
{
profile
.
gems
}
</
span
>
</
motion
.
div
>
)
}
</>
)
}
</
motion
.
button
>
{
/* Bell: hexagonal button */
}
<
motion
.
button
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'
)
}
whileTap=
{
{
scale
:
0.85
}
}
whileHover=
{
{
scale
:
1.05
}
}
>
<
Bell
size=
{
16
}
className=
"text-text-secondary"
/>
{
unreadCount
>
0
&&
(
<
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 animate-pulse-glow"
initial=
{
{
scale
:
0
}
}
animate=
{
{
scale
:
1
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
15
}
}
>
<
span
className=
"text-[9px] font-black text-white"
>
{
unreadCount
>
9
?
'9+'
:
unreadCount
}
</
span
>
</
motion
.
div
>
)
}
</
motion
.
button
>
</
div
>
</
div
>
</
div
>
</
div
>
...
...
src/components/layout/PageTransition.tsx
View file @
4d37e0d3
...
...
@@ -9,10 +9,10 @@ interface PageTransitionProps {
export
function
PageTransition
({
children
,
className
=
''
}:
PageTransitionProps
)
{
return
(
<
motion
.
div
className=
{
`app-container py-
6 flex flex-col gap-6
${className}`
}
initial=
{
{
opacity
:
0
,
y
:
2
0
}
}
animate=
{
{
opacity
:
1
,
y
:
0
}
}
exit=
{
{
opacity
:
0
,
y
:
-
20
}
}
className=
{
`app-container py-
8 flex flex-col gap-7
${className}`
}
initial=
{
{
opacity
:
0
,
y
:
2
4
,
scale
:
0.97
}
}
animate=
{
{
opacity
:
1
,
y
:
0
,
scale
:
1
}
}
exit=
{
{
opacity
:
0
,
y
:
-
16
,
scale
:
0.98
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
300
,
damping
:
28
}
}
>
{
children
}
...
...
src/components/layout/index.ts
View file @
4d37e0d3
export
{
AppShell
}
from
'./AppShell'
export
{
DecorativeBackground
}
from
'./DecorativeBackground'
export
{
Header
}
from
'./Header'
export
{
BottomNav
}
from
'./BottomNav'
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
{
Card
}
from
'./Card'
export
{
GamePanel
}
from
'./GamePanel'
export
{
GameProgressBar
}
from
'./GameProgressBar'
export
{
Input
}
from
'./Input'
export
{
RibbonHeader
}
from
'./RibbonHeader'
export
{
ShieldBadge
}
from
'./ShieldBadge'
export
{
ToastContainer
}
from
'./ToastContainer'
src/index.css
View file @
4d37e0d3
...
...
@@ -76,56 +76,224 @@ body {
background
:
var
(
--color-gold-muted
);
}
/* === GAME CARD THICK BORDERS === */
.game-card
{
border
:
3px
solid
var
(
--color-border
);
border-radius
:
20px
;
background
:
var
(
--color-surface-1
);
transition
:
transform
0.15s
,
box-shadow
0.15s
;
/* ============================================================
ARENA BACKGROUND SYSTEM
============================================================ */
.bg-arena
{
background
:
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
{
transform
:
translateY
(
-3px
);
box-shadow
:
0
8px
24px
rgba
(
0
,
0
,
0
,
0.5
);
.clip-nameplate
{
clip-path
:
polygon
(
5%
0%
,
100%
0%
,
95%
100%
,
0%
100%
);
}
.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
);
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
;
border-bottom
:
4px
solid
rgba
(
0
,
0
,
0
,
0.3
);
transition
:
all
0.1s
;
border-radius
:
16px
;
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
{
border-bottom-width
:
1px
;
transform
:
translateY
(
3px
);
.animate-shimmer
{
animation
:
shimmer
3s
ease-in-out
infinite
;
}
/* === GLOW ANIMATIONS === */
@keyframes
pulse-glow
{
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
);
}
.animate-badge-bounce
{
animation
:
badge-bounce
0.5s
ease-in-out
;
}
@keyframes
float
{
0
%,
100
%
{
transform
:
translateY
(
0
);
}
50
%
{
transform
:
translateY
(
-6px
);
}
.animate-rays-rotate
{
animation
:
rays-rotate
90s
linear
infinite
;
}
.animate-
pulse-glow
{
animation
:
pulse-glow
2
s
ease-in-out
infinite
;
.animate-
float-gentle
{
animation
:
float-gentle
3
s
ease-in-out
infinite
;
}
.animate-
floa
t
{
animation
:
float
3
s
ease-in-out
infinite
;
.animate-
pulse-sof
t
{
animation
:
pulse-soft
2
s
ease-in-out
infinite
;
}
/* === RESPONSIVE CONTAINER === */
/* ============================================================
RESPONSIVE CONTAINER
============================================================ */
.app-container
{
width
:
100%
;
max-width
:
480px
;
...
...
src/pages/BotSelectPage.tsx
View file @
4d37e0d3
...
...
@@ -2,10 +2,8 @@ import { motion } from 'framer-motion'
import
{
useEffect
,
useState
}
from
'react'
import
{
useNavigate
}
from
'react-router-dom'
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
{
ChevronRight
,
Swords
}
from
'lucide-react'
import
{
ChevronRight
,
Swords
,
Check
}
from
'lucide-react'
const
DIFFICULTY_COLORS
:
Record
<
string
,
string
>
=
{
beginner
:
'#00E5CC'
,
...
...
@@ -17,7 +15,7 @@ const DIFFICULTY_COLORS: Record<string, string> = {
near_perfect
:
'#FFE066'
,
}
const
TOTAL_STRENGTH_
DO
TS
=
7
const
TOTAL_STRENGTH_
SEGMEN
TS
=
7
export
function
BotSelectPage
()
{
const
navigate
=
useNavigate
()
...
...
@@ -40,7 +38,7 @@ export function BotSelectPage() {
if
(
loading
)
{
return
(
<
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
>
</
PageTransition
>
)
...
...
@@ -48,13 +46,19 @@ export function BotSelectPage() {
return
(
<
PageTransition
className=
"pb-36"
>
{
/* Header */
}
<
div
className=
"flex items-center gap-4"
>
<
motion
.
button
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
}
}
>
<
ChevronRight
size=
{
20
}
className=
"text-text-secondary"
/>
<
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"
/>
</
div
>
</
motion
.
button
>
<
div
>
<
h1
className=
"text-xl font-black"
>
العب ضد الروبوت
</
h1
>
...
...
@@ -62,27 +66,48 @@ export function BotSelectPage() {
</
div
>
</
div
>
{
/* Bot cards */
}
<
div
className=
"flex flex-col gap-3"
>
{
bots
.
map
((
bot
,
i
)
=>
{
const
isSelected
=
selectedBot
===
bot
.
id
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
(
<
motion
.
div
key=
{
bot
.
id
}
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
}
}
>
<
Card
glow=
{
isSelected
}
<
motion
.
div
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
className=
"absolute right-0 top-0 bottom-0 w-[5px] rounded-r-full"
style=
{
{
backgroundColor
:
diffColor
}
}
/>
{
/* Portrait frame */
}
<
div
className=
"relative w-14 h-14 rounded-xl overflow-hidden flex-shrink-0 border-2"
style=
{
{
backgroundColor
:
`${diffColor}15`
,
borderColor
:
`${diffColor}60`
}
}
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
src=
{
getBotPortraitUrl
(
bot
.
id
)
}
...
...
@@ -91,51 +116,71 @@ export function BotSelectPage() {
onError=
{
(
e
)
=>
{
(
e
.
target
as
HTMLImageElement
).
style
.
display
=
'none'
}
}
/>
<
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
>
{
/* Info */
}
<
div
className=
"flex-1 min-w-0 space-y-1.5"
>
<
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
className=
"px-2 py-0.5 rounded-lg text-[9px] font-black"
style=
{
{
backgroundColor
:
`${diffColor}25`
,
color
:
diffColor
}
}
className=
"px-2.5 py-0.5 text-[9px] font-black rounded-md"
style=
{
{
backgroundColor
:
`${diffColor}20`
,
color
:
diffColor
,
border
:
`2px solid ${diffColor}40`
,
}
}
>
{
bot
.
style_ar
}
</
span
>
</
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
>
<
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
key=
{
dotIndex
}
className=
"w-[6px] h-[6px] rounded-full"
style=
{
{
backgroundColor
:
dotIndex
<
strengthLevel
?
diffColor
:
`${diffColor}20`
}
}
key=
{
segIndex
}
className=
"w-[14px] h-[7px] rounded-sm"
style=
{
{
backgroundColor
:
segIndex
<
strengthLevel
?
diffColor
:
`${diffColor}15`
,
boxShadow
:
segIndex
<
strengthLevel
?
`0 0 4px ${diffColor}40`
:
'none'
,
}
}
/>
))
}
</
div
>
</
div
>
{
/* Bio */
}
<
p
className=
"text-[11px] text-text-muted truncate font-semibold"
>
{
bot
.
bio_ar
}
</
p
>
</
div
>
{
/* Selected state - checkmark in shield */
}
{
isSelected
&&
(
<
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
}
}
animate=
{
{
scale
:
1
,
rotate
:
0
}
}
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
>
)
}
</
Card
>
</
motion
.
div
>
</
motion
.
div
>
)
})
}
</
div
>
{
/* Bottom sticky button */
}
{
selectedBot
&&
(
<
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"
...
...
@@ -143,10 +188,16 @@ export function BotSelectPage() {
animate=
{
{
opacity
:
1
,
y
:
0
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
400
,
damping
:
22
}
}
>
<
div
className=
"app-container !p-0"
>
<
Button
onClick=
{
startGame
}
className=
"w-[80%] mx-auto block"
size=
"lg"
>
ابدا المباراة
</
Button
>
<
div
className=
"flex justify-center"
>
<
motion
.
button
onClick=
{
startGame
}
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
>
</
motion
.
div
>
)
}
...
...
src/pages/FriendsPage.tsx
View file @
4d37e0d3
import
{
useState
}
from
'react'
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
{
Card
}
from
'../components/ui/Card'
import
{
Button
}
from
'../components/ui/Button'
import
{
useFriends
}
from
'../hooks/useFriends'
import
{
supabase
}
from
'../lib/supabase'
import
{
useAuthStore
}
from
'../stores/authStore'
...
...
@@ -75,207 +73,283 @@ export function FriendsPage() {
return
(
<
PageTransition
>
<
div
className=
"flex flex-col gap-5 py-6"
>
<
div
className=
"flex items-center justify-between"
>
<
h1
className=
"text-xl font-black"
>
الأصدقاء
</
h1
>
<
motion
.
div
className=
"p-2.5 rounded-xl bg-[#FFC83D]/10 border-3 border-[#FFC83D]/30"
whileTap=
{
{
scale
:
0.85
}
}
>
<
UserPlus
size=
{
20
}
className=
"text-[#FFC83D]"
/>
</
motion
.
div
>
{
/* Header */
}
<
div
className=
"flex items-center justify-between"
>
<
div
className=
"flex items-center gap-3"
>
<
h1
className=
"text-2xl font-black text-text-primary"
>
الأصدقاء
</
h1
>
</
div
>
<
motion
.
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%)'
}
}
whileTap=
{
{
scale
:
0.85
}
}
>
<
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
>
</
div
>
<
div
className=
"relative"
>
<
Search
size=
{
16
}
className=
"absolute right-3 top-1/2 -translate-y-1/2 text-text-muted"
/>
<
input
type=
"text"
placeholder=
"بحث عن لاعب..."
value=
{
searchQuery
}
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"
dir=
"rtl"
/>
</
div
>
{
/* Decorative accent line */
}
<
div
className=
"h-[3px] rounded-full bg-gradient-to-l from-[#FFC83D] via-[#FFC83D]/40 to-transparent"
/>
{
searching
&&
(
<
div
className=
"flex justify-center py-4"
>
<
Loader2
size=
{
22
}
className=
"animate-spin text-[#FFC83D]"
/>
</
div
>
)
}
{
/* Search Input - recessed game style */
}
<
div
className=
"relative"
>
<
Search
size=
{
16
}
className=
"absolute right-4 top-1/2 -translate-y-1/2 text-text-muted z-10"
/>
<
input
type=
"text"
placeholder=
"بحث عن لاعب..."
value=
{
searchQuery
}
onChange=
{
(
e
)
=>
handleSearch
(
e
.
target
.
value
)
}
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"
/>
</
div
>
<
AnimatePresence
>
{
searchResults
.
length
>
0
&&
(
<
motion
.
div
className=
"flex flex-col gap-2"
variants=
{
stagger
}
initial=
"hidden"
animate=
"show"
exit=
{
{
opacity
:
0
}
}
>
{
searchResults
.
map
((
result
)
=>
{
const
alreadyFriend
=
friends
.
some
((
f
)
=>
f
.
profile
.
id
===
result
.
id
)
const
alreadySent
=
sentIds
.
has
(
result
.
id
)
return
(
<
motion
.
div
key=
{
result
.
id
}
variants=
{
item
}
>
<
Card
className=
"!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"
>
{
searching
&&
(
<
div
className=
"flex justify-center py-4"
>
<
Loader2
size=
{
22
}
className=
"animate-spin text-[#FFC83D]"
/>
</
div
>
)
}
<
AnimatePresence
>
{
searchResults
.
length
>
0
&&
(
<
motion
.
div
className=
"flex flex-col gap-2"
variants=
{
stagger
}
initial=
"hidden"
animate=
"show"
exit=
{
{
opacity
:
0
}
}
>
{
searchResults
.
map
((
result
)
=>
{
const
alreadyFriend
=
friends
.
some
((
f
)
=>
f
.
profile
.
id
===
result
.
id
)
const
alreadySent
=
sentIds
.
has
(
result
.
id
)
return
(
<
motion
.
div
key=
{
result
.
id
}
variants=
{
item
}
>
<
div
className=
"game-panel !p-3.5 flex items-center gap-3"
>
<
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]"
>
{
result
.
display_name
?.[
0
]
||
result
.
username
[
0
]
}
</
span
>
</
div
>
<
div
className=
"flex-1 min-w-0"
>
<
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
>
</
div
>
{
alreadyFriend
?
(
<
UserCheck
size=
{
18
}
className=
"text-[#00E5CC] shrink-0"
/>
)
:
alreadySent
?
(
<
span
className=
"text-xs font-bold text-text-muted"
>
تم الارسال
</
span
>
)
:
(
<
Button
size=
"sm"
variant=
"gold"
onClick=
{
()
=>
handleSendRequest
(
result
.
id
)
}
>
اضافة
</
Button
>
)
}
</
Card
>
</
motion
.
div
>
)
})
}
</
motion
.
div
>
)
}
</
AnimatePresence
>
</
div
>
<
div
className=
"flex-1 min-w-0"
>
<
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
>
</
div
>
{
alreadyFriend
?
(
<
UserCheck
size=
{
18
}
className=
"text-[#00E5CC] shrink-0"
/>
)
:
alreadySent
?
(
<
span
className=
"text-xs font-bold text-text-muted"
>
تم الارسال
</
span
>
)
:
(
<
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"
>
اضافة
</
motion
.
button
>
)
}
</
div
>
</
motion
.
div
>
)
})
}
</
motion
.
div
>
)
}
</
AnimatePresence
>
{
loading
?
(
<
div
className=
"flex-1 flex items-center justify-center py-12"
>
<
Loader2
size=
{
28
}
className=
"animate-spin text-[#FFC83D]"
/>
</
div
>
)
:
(
<
motion
.
div
className=
"flex flex-col gap-5"
variants=
{
stagger
}
initial=
"hidden"
animate=
"show"
>
{
pendingReceived
.
length
>
0
&&
(
<
motion
.
div
className=
"flex flex-col gap-2"
variants=
{
item
}
>
<
h2
className=
"text-sm font-black text-[#FFC83D]"
>
طلبات الصداقة (
{
pendingReceived
.
length
}
)
</
h2
>
<
div
className=
"flex flex-col gap-2"
>
{
pendingReceived
.
map
((
req
)
=>
(
<
Card
key=
{
req
.
id
}
className=
"!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"
>
{
loading
?
(
<
div
className=
"flex-1 flex items-center justify-center py-12"
>
<
Loader2
size=
{
28
}
className=
"animate-spin text-[#FFC83D]"
/>
</
div
>
)
:
(
<
motion
.
div
className=
"flex flex-col gap-5"
variants=
{
stagger
}
initial=
"hidden"
animate=
"show"
>
{
/* Friend Requests */
}
{
pendingReceived
.
length
>
0
&&
(
<
motion
.
div
className=
"flex flex-col gap-2.5"
variants=
{
item
}
>
<
h2
className=
"text-sm font-black text-[#FFC83D] flex items-center 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
)
=>
(
<
div
key=
{
req
.
id
}
className=
"game-panel-gold !p-3.5 flex items-center gap-3"
>
<
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]"
>
{
req
.
profile
.
display_name
?.[
0
]
||
req
.
profile
.
username
[
0
]
}
</
span
>
</
div
>
<
div
className=
"flex-1 min-w-0"
>
<
p
className=
"text-sm font-black truncate"
>
{
req
.
profile
.
display_name
||
req
.
profile
.
username
}
</
p
>
<
p
className=
"text-xs text-text-muted font-bold"
>
{
req
.
profile
.
elo_blitz
}
</
p
>
</
div
>
<
div
className=
"flex gap-2"
>
<
Button
size=
"sm"
variant=
"cyan"
onClick=
{
()
=>
acceptRequest
(
req
.
id
)
}
>
قبول
</
Button
>
<
Button
size=
"sm"
variant=
"coral"
onClick=
{
()
=>
rejectRequest
(
req
.
id
)
}
>
رفض
</
Button
>
</
div
>
</
Card
>
))
}
</
div
>
</
motion
.
div
>
)
}
</
div
>
<
div
className=
"flex-1 min-w-0"
>
<
p
className=
"text-sm font-black truncate"
>
{
req
.
profile
.
display_name
||
req
.
profile
.
username
}
</
p
>
<
p
className=
"text-xs text-text-muted font-bold"
>
{
req
.
profile
.
elo_blitz
}
</
p
>
</
div
>
<
div
className=
"flex gap-2"
>
<
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"
>
قبول
</
motion
.
button
>
<
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"
>
رفض
</
motion
.
button
>
</
div
>
</
div
>
))
}
</
div
>
</
motion
.
div
>
)
}
{
/* Online Friends */
}
{
onlineFriends
.
length
>
0
&&
(
<
motion
.
div
className=
"flex flex-col gap-2.5"
variants=
{
item
}
>
<
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
}
)
</
h2
>
<
div
className=
"flex flex-col gap-2.5"
>
{
onlineFriends
.
map
((
friend
)
=>
(
<
div
key=
{
friend
.
id
}
className=
"game-panel !p-3 flex items-center gap-3 relative overflow-hidden"
>
{
/* Green accent stripe */
}
<
div
className=
"absolute right-0 top-0 bottom-0 w-[4px] bg-[#4ADE80] rounded-r-full"
/>
{
onlineFriends
.
length
>
0
&&
(
<
motion
.
div
className=
"flex flex-col gap-2"
variants=
{
item
}
>
<
h2
className=
"text-sm font-black text-[#00E5CC]"
>
متصل (
{
onlineFriends
.
length
}
)
</
h2
>
<
div
className=
"flex flex-col gap-2"
>
{
onlineFriends
.
map
((
friend
)
=>
(
<
Card
key=
{
friend
.
id
}
className=
"!p-3.5 flex items-center gap-3"
>
<
div
className=
"relative"
>
<
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=
"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]"
>
{
friend
.
profile
.
display_name
?.[
0
]
||
friend
.
profile
.
username
[
0
]
}
</
span
>
</
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
className=
"flex-1 min-w-0"
>
<
p
className=
"text-sm font-black truncate"
>
{
friend
.
profile
.
display_name
||
friend
.
profile
.
username
}
</
p
>
<
p
className=
"text-xs text-text-muted font-bold"
>
{
friend
.
profile
.
elo_blitz
}
</
p
>
{
/* 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
>
<
motion
.
button
className=
"p-2 rounded-xl bg-[#FF5252]/10 border-2 border-[#FF5252]/20"
whileTap=
{
{
scale
:
0.8
}
}
onClick=
{
()
=>
removeFriend
(
friend
.
id
)
}
>
<
UserX
size=
{
14
}
className=
"text-[#FF5252]"
/>
</
motion
.
button
>
</
Card
>
))
}
</
div
>
</
motion
.
div
>
)
}
</
div
>
{
offlineFriends
.
length
>
0
&&
(
<
motion
.
div
className=
"flex flex-col gap-2"
variants=
{
item
}
>
<
h2
className=
"text-sm font-black text-text-muted"
>
غير متصل (
{
offlineFriends
.
length
}
)
</
h2
>
<
div
className=
"flex flex-col gap-2"
>
{
offlineFriends
.
map
((
friend
)
=>
(
<
Card
key=
{
friend
.
id
}
className=
"!p-3.5 flex items-center gap-3 opacity-70"
>
<
div
className=
"relative"
>
<
div
className=
"w-10 h-10 rounded-full bg-surface-3 border-3 border-border flex items-center justify-center"
>
<
div
className=
"flex-1 min-w-0"
>
<
p
className=
"text-sm font-black truncate"
>
{
friend
.
profile
.
display_name
||
friend
.
profile
.
username
}
</
p
>
<
p
className=
"text-xs text-text-muted font-bold"
>
{
friend
.
profile
.
elo_blitz
}
</
p
>
</
div
>
<
motion
.
button
className=
"w-9 h-9 rounded-xl bg-[#FF5252]/10 border-2 border-[#FF5252]/25 flex items-center justify-center"
whileTap=
{
{
scale
:
0.8
}
}
onClick=
{
()
=>
removeFriend
(
friend
.
id
)
}
>
<
UserX
size=
{
14
}
className=
"text-[#FF5252]"
/>
</
motion
.
button
>
</
div
>
))
}
</
div
>
</
motion
.
div
>
)
}
{
/* Offline Friends */
}
{
offlineFriends
.
length
>
0
&&
(
<
motion
.
div
className=
"flex flex-col gap-2.5"
variants=
{
item
}
>
<
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
}
)
</
h2
>
<
div
className=
"flex flex-col gap-2.5"
>
{
offlineFriends
.
map
((
friend
)
=>
(
<
div
key=
{
friend
.
id
}
className=
"game-panel !p-3 flex items-center gap-3 opacity-60 relative overflow-hidden"
>
{
/* Gray accent stripe */
}
<
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"
>
{
friend
.
profile
.
display_name
?.[
0
]
||
friend
.
profile
.
username
[
0
]
}
</
span
>
</
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
className=
"flex-1 min-w-0"
>
<
p
className=
"text-sm font-bold truncate"
>
{
friend
.
profile
.
display_name
||
friend
.
profile
.
username
}
</
p
>
<
p
className=
"text-xs text-text-muted"
>
آخر ظهور
{
relativeTime
(
friend
.
profile
.
last_seen_at
)
}
</
p
>
</
div
>
<
motion
.
button
className=
"p-2 rounded-xl bg-[#FF5252]/10 border-2 border-[#FF5252]/20"
whileTap=
{
{
scale
:
0.8
}
}
onClick=
{
()
=>
removeFriend
(
friend
.
id
)
}
>
<
UserX
size=
{
14
}
className=
"text-[#FF5252]"
/>
</
motion
.
button
>
</
Card
>
))
}
</
div
>
</
motion
.
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"
>
<
p
className=
"text-sm font-bold truncate"
>
{
friend
.
profile
.
display_name
||
friend
.
profile
.
username
}
</
p
>
<
p
className=
"text-xs text-text-muted"
>
آخر ظهور
{
relativeTime
(
friend
.
profile
.
last_seen_at
)
}
</
p
>
</
div
>
<
motion
.
button
className=
"w-9 h-9 rounded-xl bg-[#FF5252]/10 border-2 border-[#FF5252]/20 flex items-center justify-center"
whileTap=
{
{
scale
:
0.8
}
}
onClick=
{
()
=>
removeFriend
(
friend
.
id
)
}
>
<
UserX
size=
{
14
}
className=
"text-[#FF5252]"
/>
</
motion
.
button
>
</
div
>
))
}
</
div
>
</
motion
.
div
>
)
}
{
friends
.
length
===
0
&&
pendingReceived
.
length
===
0
&&
(
{
/* Empty State */
}
{
friends
.
length
===
0
&&
pendingReceived
.
length
===
0
&&
(
<
motion
.
div
className=
"flex flex-col items-center justify-center py-16 gap-5"
variants=
{
item
}
>
<
motion
.
div
className=
"flex flex-col items-center justify-center py-16 gap-4"
variants=
{
item
}
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
}
}
animate=
{
{
scale
:
1
,
rotate
:
0
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
300
,
damping
:
18
}
}
>
<
motion
.
div
className=
"w-20 h-20 rounded-full bg-surface-2 border-3 border-border flex items-center justify-center"
initial=
{
{
scale
:
0
,
rotate
:
-
15
}
}
animate=
{
{
scale
:
1
,
rotate
:
0
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
300
,
damping
:
18
}
}
<
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%)'
}
}
>
<
UserPlus
size=
{
32
}
className=
"text-text-muted"
/>
</
motion
.
div
>
<
div
className=
"text-center"
>
<
p
className=
"text-text-muted font-black text-base"
>
لا يوجد اصدقاء بعد
</
p
>
<
p
className=
"text-text-muted text-xs font-bold mt-1"
>
ابحث عن لاعبين لاضافتهم
</
p
>
<
Shield
size=
{
36
}
className=
"text-text-muted"
/>
</
div
>
</
motion
.
div
>
)
}
</
motion
.
div
>
)
}
</
div
>
<
div
className=
"text-center"
>
<
p
className=
"text-text-muted font-black text-base"
>
لا يوجد اصدقاء
</
p
>
<
p
className=
"text-text-muted text-xs font-bold mt-1.5"
>
ابحث عن لاعبين لاضافتهم
</
p
>
</
div
>
</
motion
.
div
>
)
}
</
motion
.
div
>
)
}
</
PageTransition
>
)
}
src/pages/HomePage.tsx
View file @
4d37e0d3
...
...
@@ -4,7 +4,6 @@ import { Play, TrendingUp, Swords, Flame, Bot, Users, Lightbulb, Crown } from 'l
import
{
useNavigate
}
from
'react-router-dom'
import
{
useAuthStore
}
from
'../stores/authStore'
import
{
PageTransition
}
from
'../components/layout/PageTransition'
import
{
Card
}
from
'../components/ui/Card'
import
{
DailyRewardModal
}
from
'../components/DailyRewardModal'
import
{
useDailyReward
}
from
'../hooks/useDailyReward'
import
{
playSound
}
from
'../lib/sounds'
...
...
@@ -22,13 +21,13 @@ const dailyTips = [
const
stagger
=
{
hidden
:
{},
show
:
{
transition
:
{
staggerChildren
:
0.0
6
},
transition
:
{
staggerChildren
:
0.0
8
},
},
}
const
fadeUp
=
{
hidden
:
{
opacity
:
0
,
y
:
2
0
},
show
:
{
opacity
:
1
,
y
:
0
,
transition
:
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
22
}
},
hidden
:
{
opacity
:
0
,
y
:
2
4
},
show
:
{
opacity
:
1
,
y
:
0
,
transition
:
{
type
:
'spring'
,
stiffness
:
400
,
damping
:
24
}
},
}
export
function
HomePage
()
{
...
...
@@ -60,155 +59,266 @@ export function HomePage() {
variants=
{
stagger
}
initial=
"hidden"
animate=
"show"
className=
"flex flex-col gap-
6
"
className=
"flex flex-col gap-
7
"
>
{
/* === 1. PLAYER NAMEPLATE — Angled Banner === */
}
{
profile
&&
(
<
motion
.
div
variants=
{
fadeUp
}
className=
"flex items-center gap-4"
>
<
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-2xl font-black text-gold"
>
{
profile
.
display_name
?.
charAt
(
0
)
||
'L'
}
</
span
>
</
div
>
<
div
>
<
h2
className=
"text-2xl font-black"
>
اهلا،
{
profile
.
display_name
}
</
h2
>
<
div
className=
"flex items-center gap-2 mt-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"
>
<
motion
.
div
variants=
{
fadeUp
}
className=
"relative"
>
<
div
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))'
,
}
}
>
<
span
className=
"text-xl font-black text-gold"
>
{
profile
.
display_name
?.
charAt
(
0
)
||
'L'
}
</
span
>
</
div
>
</
div
>
{
/* Name + Level */
}
<
div
className=
"flex-1"
>
<
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
}
</
span
>
</
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
>
</
motion
.
div
>
)
}
{
/* === 2. PLAY BUTTON — The Hero Crest === */
}
<
motion
.
div
variants=
{
fadeUp
}
className=
"flex justify-center"
>
<
motion
.
button
onClick=
{
()
=>
{
playSound
(
'click'
)
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"
whileTap=
{
{
scale
:
0.93
,
y
:
3
}
}
whileHover=
{
{
scale
:
1.03
}
}
className=
"relative w-[85%] cursor-pointer"
whileTap=
{
{
y
:
4
,
scale
:
0.97
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
600
,
damping
:
20
}
}
>
<
motion
.
div
className=
"absolute inset-0 rounded-[22px] border-[3px] border-gold/60 animate-pulse-glow"
{
/* Shadow layer (3D depth) */
}
<
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=
"absolute inset-0 bg-gradient-to-r from-transparent via-white/25 to-transparent"
animate=
{
{
x
:
[
'-200%'
,
'200%'
]
}
}
transition=
{
{
duration
:
2.5
,
repeat
:
Infinity
,
ease
:
'linear'
}
}
/>
<
div
className=
"relative flex flex-col items-center gap-3"
>
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
animate=
{
{
scale
:
[
1
,
1.12
,
1
]
}
}
transition=
{
{
duration
:
1.8
,
repeat
:
Infinity
,
ease
:
'easeInOut'
}
}
>
<
Play
size=
{
48
}
className=
"text-background"
fill=
"currentColor"
/>
</
motion
.
div
>
<
span
className=
"text-3xl font-black text-background uppercase tracking-wide"
>
العب الان
</
span
>
</
div
>
className=
"absolute inset-0"
style=
{
{
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'
}
}
/>
{
/* Content */
}
<
div
className=
"relative flex flex-col items-center gap-2"
>
<
motion
.
div
animate=
{
{
scale
:
[
1
,
1.1
,
1
]
}
}
transition=
{
{
duration
:
1.6
,
repeat
:
Infinity
,
ease
:
'easeInOut'
}
}
>
<
Play
size=
{
52
}
className=
"text-background"
fill=
"currentColor"
/>
</
motion
.
div
>
<
span
className=
"text-3xl font-black text-background tracking-wide"
>
العب الان
</
span
>
</
div
>
</
motion
.
div
>
</
motion
.
button
>
</
motion
.
div
>
{
/* === 3. STATS AS SHIELD BADGES === */
}
{
profile
&&
(
<
motion
.
div
variants=
{
fadeUp
}
className=
"grid grid-cols-3 gap-3"
>
<
StatCard
icon=
{
<
TrendingUp
size=
{
22
}
className=
"text-gold"
/>
}
<
motion
.
div
variants=
{
fadeUp
}
className=
"relative flex justify-center items-center gap-4 py-3"
>
{
/* Decorative connecting line */
}
<
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
}
label=
"تقييم"
accent=
"gold"
gradient=
"linear-gradient(135deg, rgba(255,200,60,0.15), rgba(255,200,60,0.05))"
borderColor=
"#FFC83D"
/>
<
Stat
Card
icon=
{
<
Swords
size=
{
22
}
className=
"text-cyan"
/>
}
<
Stat
Badge
icon=
{
<
Swords
size=
{
18
}
className=
"text-cyan"
/>
}
value=
{
profile
.
total_games_played
}
label=
"مباراة"
accent=
"cyan"
gradient=
"linear-gradient(135deg, rgba(0,229,204,0.15), rgba(0,229,204,0.05))"
borderColor=
"#00E5CC"
isCenter
/>
<
Stat
Card
icon=
{
<
Flame
size=
{
22
}
className=
"text-coral"
/>
}
<
Stat
Badge
icon=
{
<
Flame
size=
{
18
}
className=
"text-coral"
/>
}
value=
{
profile
.
win_streak
}
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
variants=
{
fadeUp
}
className=
"grid grid-cols-2 gap-3"
>
<
Card
variant=
"default"
className=
"flex flex-col items-center gap-3 !p-5"
{
/* === 4. QUICK ACTIONS — Chunky 3D Game Tiles === */
}
<
motion
.
div
variants=
{
fadeUp
}
className=
"grid grid-cols-2 gap-4 px-2"
>
<
motion
.
button
className=
"relative cursor-pointer"
style=
{
{
transform
:
'rotate(-1deg)'
}
}
whileTap=
{
{
y
:
4
,
boxShadow
:
'none'
}
}
onClick=
{
()
=>
{
playSound
(
'click'
)
navigate
(
'/bot-select'
)
}
}
>
<
div
className=
"w-14 h-14 rounded-2xl bg-purple/15 border-3 border-purple/30 flex items-center justify-center"
>
<
Bot
size=
{
28
}
className=
"text-purple"
/>
</
div
>
<
div
className=
"text-center"
>
<
p
className=
"text-sm font-black"
>
العب ضد روبوت
</
p
>
<
p
className=
"text-[10px] text-text-muted mt-1"
>
تدريب وتحسين
</
p
>
<
div
className=
"relative flex flex-col items-center gap-3 p-5 rounded-2xl bg-surface-1 border-3 border-border"
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
className=
"text-center"
>
<
p
className=
"text-sm font-black text-text-primary"
>
العب ضد روبوت
</
p
>
<
p
className=
"text-[10px] text-text-muted mt-0.5"
>
تدريب وتحسين
</
p
>
</
div
>
</
div
>
</
Card
>
</
motion
.
button
>
<
Card
variant=
"default"
className=
"flex flex-col items-center gap-3 !p-5"
<
motion
.
button
className=
"relative cursor-pointer"
style=
{
{
transform
:
'rotate(1deg)'
}
}
whileTap=
{
{
y
:
4
,
boxShadow
:
'none'
}
}
onClick=
{
()
=>
{
playSound
(
'click'
)
navigate
(
'/friends'
)
}
}
>
<
div
className=
"w-14 h-14 rounded-2xl bg-gold/15 border-3 border-gold/30 flex items-center justify-center"
>
<
Users
size=
{
28
}
className=
"text-gold"
/>
</
div
>
<
div
className=
"text-center"
>
<
p
className=
"text-sm font-black"
>
تحدى صديق
</
p
>
<
p
className=
"text-[10px] text-text-muted mt-1"
>
ارسل دعوة
</
p
>
<
div
className=
"relative flex flex-col items-center gap-3 p-5 rounded-2xl bg-surface-1 border-3 border-border"
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
className=
"text-center"
>
<
p
className=
"text-sm font-black text-text-primary"
>
تحدى صديق
</
p
>
<
p
className=
"text-[10px] text-text-muted mt-0.5"
>
ارسل دعوة
</
p
>
</
div
>
</
div
>
</
Card
>
</
motion
.
button
>
</
motion
.
div
>
<
motion
.
div
variants=
{
fadeUp
}
>
<
Card
variant=
"gold"
className=
"relative overflow-hidden"
>
<
div
className=
"absolute top-0 right-0 w-28 h-28 bg-gradient-to-bl from-gold/10 to-transparent rounded-bl-full"
/>
<
div
className=
"flex items-start gap-4"
>
<
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"
/>
</
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
>
<
p
className=
"text-[12px] text-text-secondary leading-[1.8]"
>
{
todayTip
}
</
p
>
</
div
>
{
/* === 5. DAILY TIP — Parchment/Scroll Style === */
}
<
motion
.
div
variants=
{
fadeUp
}
className=
"relative overflow-hidden rounded-2xl border-2 border-gold/20 p-5"
style=
{
{
background
:
'linear-gradient(160deg, rgba(30,35,64,1) 0%, rgba(40,35,30,0.4) 100%)'
,
}
}
>
{
/* 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
>
</
Card
>
</
div
>
{
/* 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
>
<
p
className=
"text-[12px] text-text-secondary leading-[2] pt-1.5"
>
{
todayTip
}
</
p
>
</
div
>
</
motion
.
div
>
</
motion
.
div
>
</
PageTransition
>
)
}
function
StatCard
({
icon
,
value
,
label
,
accent
}:
{
icon
:
React
.
ReactNode
;
value
:
number
;
label
:
string
;
accent
:
string
})
{
const
borderMap
:
Record
<
string
,
string
>
=
{
cyan
:
'border-t-cyan'
,
gold
:
'border-t-gold'
,
coral
:
'border-t-coral'
,
}
/* --- Hexagonal Stat Badge --- */
function
StatBadge
({
icon
,
value
,
label
,
gradient
,
borderColor
,
isCenter
=
false
,
}:
{
icon
:
React
.
ReactNode
value
:
number
label
:
string
gradient
:
string
borderColor
:
string
isCenter
?:
boolean
})
{
return
(
<
div
className=
{
`flex flex-col items-center gap-2 p-4 rounded-2xl bg-surface-1 border-3 border-border ${borderMap[accent] || ''}`
}
style=
{
{
borderTopWidth
:
'4px
'
}
}
className=
"relative flex flex-col items-center justify-center z-10"
style=
{
{
transform
:
isCenter
?
'scale(1.08)'
:
'scale(1)
'
}
}
>
{
icon
}
<
span
className=
"text-2xl font-black"
>
{
value
}
</
span
>
<
span
className=
"text-[10px] text-text-muted font-bold"
>
{
label
}
</
span
>
<
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
}
<
span
className=
"text-lg font-black text-text-primary mt-0.5"
>
{
value
}
</
span
>
</
div
>
<
span
className=
"text-[9px] text-text-muted font-bold mt-1"
>
{
label
}
</
span
>
</
div
>
)
}
src/pages/LeaderboardPage.tsx
View file @
4d37e0d3
import
{
useState
}
from
'react'
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
{
useAuthStore
}
from
'../stores/authStore'
import
{
useLeaderboard
}
from
'../hooks/useLeaderboard'
...
...
@@ -8,11 +8,11 @@ import { useLeaderboard } from '../hooks/useLeaderboard'
type
TimeControlType
=
'bullet'
|
'blitz'
|
'rapid'
|
'classical'
type
Period
=
'weekly'
|
'monthly'
|
'all_time'
const
TIME_CONTROLS
:
{
label
:
string
;
value
:
TimeControlType
}[]
=
[
{
label
:
'رصاصة'
,
value
:
'bullet'
},
{
label
:
'خاطف'
,
value
:
'blitz'
},
{
label
:
'سريع'
,
value
:
'rapid'
},
{
label
:
'كلاسيكي'
,
value
:
'classical'
},
const
TIME_CONTROLS
:
{
label
:
string
;
value
:
TimeControlType
;
icon
:
React
.
ReactNode
}[]
=
[
{
label
:
'رصاصة'
,
value
:
'bullet'
,
icon
:
<
Zap
size=
{
14
}
/>
},
{
label
:
'خاطف'
,
value
:
'blitz'
,
icon
:
<
Timer
size=
{
14
}
/>
},
{
label
:
'سريع'
,
value
:
'rapid'
,
icon
:
<
Clock
size=
{
14
}
/>
},
{
label
:
'كلاسيكي'
,
value
:
'classical'
,
icon
:
<
Hourglass
size=
{
14
}
/>
},
]
const
PERIODS
:
{
label
:
string
;
value
:
Period
}[]
=
[
...
...
@@ -21,23 +21,45 @@ const PERIODS: { label: string; value: Period }[] = [
{
label
:
'الكل'
,
value
:
'all_time'
},
]
function
Avatar
({
name
,
url
,
size
=
44
}:
{
name
:
string
;
url
?:
string
|
null
;
size
?:
number
})
{
if
(
url
)
{
return
(
<
img
src=
{
url
}
alt=
{
name
}
className=
"rounded-full object-cover border-2 border-border"
style=
{
{
width
:
size
,
height
:
size
}
}
/>
)
}
const
HEX_CLIP
=
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
function
HexAvatar
({
name
,
url
,
size
=
44
}:
{
name
:
string
;
url
?:
string
|
null
;
size
?:
number
})
{
return
(
<
div
className=
"r
ounded-full bg-gradient-to-br from-surface-3 to-surface-2 flex items-center justify-center font-black text-text-secondary border-2 border-bord
er"
style=
{
{
width
:
size
,
height
:
size
,
fontSize
:
size
*
0.38
}
}
className=
"r
elative flex items-center justify-cent
er"
style=
{
{
width
:
size
,
height
:
size
}
}
>
{
name
?.
charAt
(
0
)
||
'?'
}
{
/* Gold hex border */
}
<
div
className=
"absolute inset-0"
style=
{
{
clipPath
:
HEX_CLIP
,
background
:
'linear-gradient(135deg, #FFC83D, #B44DFF)'
,
}
}
/>
{
/* Inner content */
}
<
div
className=
"flex items-center justify-center"
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
)
||
'?'
}
</
span
>
)
}
</
div
>
</
div
>
)
}
...
...
@@ -53,47 +75,97 @@ export function LeaderboardPage() {
return
(
<
PageTransition
className=
"flex flex-col gap-5"
>
<
div
className=
"flex items-center gap-2.5"
>
<
Trophy
size=
{
24
}
className=
"text-[#FFC83D]"
/>
<
h1
className=
"text-2xl font-black"
>
لوحة المتصدرين
</
h1
>
</
div
>
{
/* === PAGE HEADER === */
}
<
motion
.
div
className=
"relative flex items-center justify-center py-3"
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"
>
{
TIME_CONTROLS
.
map
((
tc
)
=>
(
<
motion
.
button
key=
{
tc
.
value
}
whileTap=
{
{
scale
:
0.93
}
}
onClick=
{
()
=>
setTimeControl
(
tc
.
value
)
}
className=
{
`px-4 py-2 rounded-xl text-xs font-bold whitespace-nowrap border-2 transition-colors ${
timeControl === tc.value
? 'bg-[#FFC83D] border-[#FFC83D] text-background font-black'
: 'bg-surface-2 text-text-muted border-border hover:border-[#FFC83D]/40'
}`
}
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
20
}
}
>
{
tc
.
label
}
</
motion
.
button
>
))
}
</
div
>
{
/* === FILTER TABS (Trapezoidal) === */
}
<
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
key=
{
tc
.
value
}
whileTap=
{
{
scale
:
0.93
}
}
onClick=
{
()
=>
setTimeControl
(
tc
.
value
)
}
className=
"relative flex items-center gap-1.5 px-4 py-2.5 text-xs font-black whitespace-nowrap transition-all"
style=
{
{
clipPath
:
'polygon(8% 0%, 92% 0%, 100% 100%, 0% 100%)'
,
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
}
}
>
{
tc
.
icon
}
{
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
.
div
>
<
div
className=
"flex gap-2 overflow-x-auto no-scrollbar"
>
{
PERIODS
.
map
((
p
)
=>
(
<
motion
.
button
key=
{
p
.
value
}
whileTap=
{
{
scale
:
0.93
}
}
onClick=
{
()
=>
setPeriod
(
p
.
value
)
}
className=
{
`px-3.5 py-1.5 rounded-xl text-[11px] font-bold whitespace-nowrap border-2 transition-colors ${
period === p.value
? 'bg-[#B44DFF]/15 text-[#B44DFF] border-[#B44DFF]/50 font-black'
: 'bg-surface-1 text-text-muted border-border hover:border-[#B44DFF]/30'
}`
}
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
20
}
}
>
{
p
.
label
}
</
motion
.
button
>
))
}
</
div
>
{
/* === PERIOD SELECTOR === */
}
<
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
key=
{
p
.
value
}
whileTap=
{
{
scale
:
0.93
}
}
onClick=
{
()
=>
setPeriod
(
p
.
value
)
}
className=
"px-4 py-1.5 rounded-full text-[11px] font-bold whitespace-nowrap transition-all"
style=
{
{
background
:
isActive
?
'rgba(180,77,255,0.15)'
:
'var(--color-surface-1)'
,
color
:
isActive
?
'#B44DFF'
:
'var(--color-text-muted)'
,
border
:
isActive
?
'2px solid rgba(180,77,255,0.5)'
:
'2px solid var(--color-border)'
,
boxShadow
:
isActive
?
'none'
:
'inset 0 1px 3px rgba(0,0,0,0.2)'
,
}
}
>
{
p
.
label
}
</
motion
.
button
>
)
})
}
</
motion
.
div
>
{
/* === CONTENT === */
}
{
loading
?
(
<
div
className=
"flex flex-col items-center py-16"
>
<
motion
.
div
...
...
@@ -105,7 +177,12 @@ export function LeaderboardPage() {
)
:
entries
.
length
===
0
?
(
<
div
className=
"flex flex-col items-center justify-center py-16 gap-4"
>
<
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
}
}
animate=
{
{
scale
:
1
,
rotate
:
0
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
300
,
damping
:
18
}
}
...
...
@@ -117,41 +194,108 @@ export function LeaderboardPage() {
</
div
>
)
:
(
<>
{
/* === THE PODIUM === */
}
{
top3
.
length
>
0
&&
<
Podium
entries=
{
top3
}
currentUserId=
{
user
?.
id
}
/>
}
<
div
className=
"flex flex-col gap-2.5"
>
{
rest
.
map
((
entry
,
i
)
=>
(
<
motion
.
div
key=
{
entry
.
player_id
}
initial=
{
{
opacity
:
0
,
x
:
-
12
}
}
animate=
{
{
opacity
:
1
,
x
:
0
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
400
,
damping
:
24
,
delay
:
0.3
+
i
*
0.04
}
}
className=
{
`flex items-center gap-3 p-3.5 rounded-2xl bg-surface-1 border-3 ${
entry.player_id === user?.id
? 'border-[#FFC83D]/60 bg-[#FFC83D]/10'
: 'border-border'
}`
}
>
<
span
className=
"w-8 text-center text-sm font-black text-text-muted"
>
{
entry
.
rank
}
</
span
>
<
Avatar
name=
{
entry
.
display_name
}
url=
{
entry
.
avatar_url
}
size=
{
38
}
/>
<
div
className=
"flex-1 min-w-0"
>
<
p
className=
"text-sm font-black truncate"
>
{
entry
.
display_name
}
</
p
>
<
p
className=
"text-[10px] text-text-muted font-bold"
>
{
entry
.
games_played
}
مباراة
</
p
>
</
div
>
<
span
className=
"text-sm font-black text-[#FFC83D]"
>
{
entry
.
rating
}
</
span
>
</
motion
.
div
>
))
}
</
div
>
{
/* === PLAYER LIST === */
}
{
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
initial=
{
{
opacity
:
0
,
x
:
-
12
}
}
animate=
{
{
opacity
:
1
,
x
:
0
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
400
,
damping
:
24
,
delay
:
0.3
+
i
*
0.03
}
}
className=
"flex items-center gap-3 py-3 px-3"
style=
{
{
borderRadius
:
14
,
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'
,
}
}
>
{
/* Rank shield */
}
{
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"
>
<
p
className=
"text-sm font-black truncate"
>
{
entry
.
display_name
}
</
p
>
<
p
className=
"text-[10px] text-text-muted font-bold"
>
{
entry
.
games_played
}
مباراة
</
p
>
</
div
>
{
/* 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
>
</
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
>
)
}
</>
)
}
</
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
({
entries
,
currentUserId
,
...
...
@@ -159,84 +303,135 @@ function Podium({
entries
:
{
rank
:
number
;
player_id
:
string
;
rating
:
number
;
games_played
:
number
;
display_name
:
string
;
avatar_url
?:
string
|
null
}[]
currentUserId
?:
string
})
{
// Order: 2nd | 1st | 3rd (RTL: right to left visually)
const
ordered
=
[
entries
[
1
],
entries
[
0
],
entries
[
2
]].
filter
(
Boolean
)
const
podiumConfig
=
[
{
color
:
'#C0C0C0'
,
height
:
'h-24'
,
avatarSize
:
52
,
rankIcon
:
<
Medal
size=
{
16
}
className=
"text-[#C0C0C0]"
/>,
podiumHeight
:
90
,
avatarSize
:
64
,
rankIcon
:
<
Medal
size=
{
20
}
className=
"text-[#C0C0C0]"
/>,
label
:
'2'
,
},
{
color
:
'#FFC83D'
,
height
:
'h-32'
,
avatarSize
:
68
,
rankIcon
:
<
Crown
size=
{
22
}
className=
"text-[#FFC83D] fill-[#FFC83D]/20"
/>,
podiumHeight
:
120
,
avatarSize
:
80
,
rankIcon
:
<
Crown
size=
{
26
}
className=
"text-[#FFC83D] animate-float"
/>,
label
:
'1'
,
},
{
color
:
'#CD7F32'
,
height
:
'h-20'
,
avatarSize
:
48
,
rankIcon
:
<
Medal
size=
{
16
}
className=
"text-[#CD7F32]"
/>,
podiumHeight
:
70
,
avatarSize
:
64
,
rankIcon
:
<
Medal
size=
{
20
}
className=
"text-[#CD7F32]"
/>,
label
:
'3'
,
},
]
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
)
=>
{
if
(
!
entry
)
return
null
const
config
=
podiumConfig
[
i
]
const
isMe
=
entry
.
player_id
===
currentUserId
const
isFirst
=
i
===
1
return
(
<
motion
.
div
key=
{
entry
.
player_id
}
className=
"flex flex-col items-center gap-2"
initial=
{
{
scale
:
0
,
opacity
:
0
,
y
:
30
}
}
className=
"relative flex flex-col items-center"
style=
{
{
flex
:
1
,
maxWidth
:
isFirst
?
130
:
110
}
}
initial=
{
{
scale
:
0
,
opacity
:
0
,
y
:
40
}
}
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
className=
"mb-1"
initial=
{
{
scale
:
0
,
rotate
:
-
20
}
}
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
}
}
>
{
config
.
rankIcon
}
{
isFirst
?
(
<
div
className=
"relative"
>
{
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
>
{
/* 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
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=
{
{
width
:
config
.
avatarSize
,
height
:
config
.
avatarSize
,
borderColor
:
config
.
color
,
backgroundColor
:
`${config.color}15`
,
color
:
config
.
color
,
fontSize
:
config
.
avatarSize
*
0.35
,
background
:
`linear-gradient(135deg, ${config.color}25, ${config.color}10)`
,
border
:
`2px solid ${config.color}60`
,
}
}
>
{
entry
.
display_name
?.
charAt
(
0
)
||
'?'
}
</
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
>
<
span
className=
"text-sm font-black"
style=
{
{
color
:
config
.
color
}
}
>
{
entry
.
rating
}
</
span
>
</
div
>
{
/* 3D Podium step */
}
<
div
className=
{
`w-full ${config.height} rounded-t-xl border-3 border-b-0`
}
className=
"w-full mt-2 relative overflow-hidden"
style=
{
{
borderColor
:
config
.
color
,
background
:
`linear-gradient(to top, ${config.color}20, transparent)`
,
height
:
config
.
podiumHeight
,
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
>
)
})
}
</
div
>
</
motion
.
div
>
)
}
src/pages/MatchmakingPage.tsx
View file @
4d37e0d3
import
{
useEffect
}
from
'react'
import
{
motion
,
AnimatePresence
}
from
'framer-motion'
import
{
useNavigate
,
useSearchParams
}
from
'react-router-dom'
import
{
Button
}
from
'../components/ui/Button'
import
{
useMatchmaking
}
from
'../hooks/useMatchmaking'
import
{
useMatchStore
}
from
'../stores/matchStore'
import
type
{
TIME_CONTROLS
}
from
'../lib/constants'
...
...
@@ -36,7 +35,9 @@ export function MatchmakingPage() {
}
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"
>
{
matchFound
?
(
<
motion
.
div
...
...
@@ -46,15 +47,63 @@ export function MatchmakingPage() {
animate=
{
{
scale
:
1
,
opacity
:
1
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
300
,
damping
:
15
}
}
>
<
motion
.
div
className=
"w-28 h-28 rounded-full bg-gold/20 border-3 border-gold flex items-center justify-center animate-pulse-glow"
animate=
{
{
scale
:
[
1
,
1.15
,
1
]
}
}
transition=
{
{
duration
:
0.6
,
repeat
:
2
}
}
>
<
span
className=
"text-5xl font-black text-gold"
>
VS
</
span
>
</
motion
.
div
>
{
/* VS Badge - hex/shield shape with gold border */
}
<
div
className=
"relative"
>
<
motion
.
div
className=
"w-32 h-32 flex items-center justify-center"
style=
{
{
clipPath
:
'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
}
}
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-[#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
>
{
/* 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
className=
"mt-
6 text-2xl font-black text-gold
"
className=
"mt-
8 text-2xl font-black text-[#FFC83D]
"
initial=
{
{
opacity
:
0
,
y
:
10
}
}
animate=
{
{
opacity
:
1
,
y
:
0
}
}
transition=
{
{
delay
:
0.2
}
}
...
...
@@ -68,31 +117,58 @@ export function MatchmakingPage() {
className=
"flex flex-col items-center"
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
)
=>
(
<
motion
.
div
key=
{
i
}
className=
"absolute inset-0
rounded-full border-3 border-gold/30
"
initial=
{
{
scale
:
0.
5
,
opacity
:
0.8
}
}
animate=
{
{
scale
:
2.5
,
opacity
:
0
}
}
className=
"absolute inset-0
flex items-center justify-center
"
initial=
{
{
scale
:
0.
4
,
opacity
:
0.8
}
}
animate=
{
{
scale
:
[
0.5
+
i
*
0.2
,
1.4
+
i
*
0.3
],
opacity
:
[
0.6
,
0
]
}
}
transition=
{
{
duration
:
2.5
,
repeat
:
Infinity
,
delay
:
i
*
0.8
,
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
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"
animate=
{
{
scale
:
[
1
,
1.08
,
1
],
rotate
:
[
0
,
3
,
-
3
,
0
]
}
}
className=
"w-20 h-20 flex items-center justify-center z-10"
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'
}
}
>
<
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
>
</
div
>
{
/* Title */
}
<
motion
.
h2
className=
"mt-8 text-xl font-black"
initial=
{
{
opacity
:
0
}
}
...
...
@@ -102,17 +178,21 @@ export function MatchmakingPage() {
جاري البحث عن خصم
</
motion
.
h2
>
{
/* Timer in a game-panel mini-bar */
}
<
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
}
}
animate=
{
{
opacity
:
1
}
}
transition=
{
{
delay
:
0.4
}
}
>
{
formatElapsed
(
elapsed
)
}
<
span
className=
"text-2xl font-black font-mono text-[#FFC83D] tabular-nums"
>
{
formatElapsed
(
elapsed
)
}
</
span
>
</
motion
.
div
>
{
/* Bouncing dots */
}
<
motion
.
div
className=
"mt-
3 flex gap-1.5
"
className=
"mt-
4 flex gap-2
"
initial=
{
{
opacity
:
0
}
}
animate=
{
{
opacity
:
1
}
}
transition=
{
{
delay
:
0.5
}
}
...
...
@@ -120,22 +200,28 @@ export function MatchmakingPage() {
{
[
0
,
1
,
2
].
map
((
i
)
=>
(
<
motion
.
span
key=
{
i
}
className=
"w-3 h-3 rounded-full bg-
gold
"
animate=
{
{
opacity
:
[
0.2
,
1
,
0.2
],
scale
:
[
0.8
,
1.2
,
0.8
]
}
}
transition=
{
{
duration
:
1
.2
,
repeat
:
Infinity
,
delay
:
i
*
0.3
}
}
className=
"w-3 h-3 rounded-full bg-
[#FFC83D]
"
animate=
{
{
y
:
[
0
,
-
8
,
0
],
opacity
:
[
0.4
,
1
,
0.4
]
}
}
transition=
{
{
duration
:
1
,
repeat
:
Infinity
,
delay
:
i
*
0.2
}
}
/>
))
}
</
motion
.
div
>
{
/* Cancel - 3D ghost button */
}
<
motion
.
div
className=
"mt-12"
initial=
{
{
opacity
:
0
}
}
animate=
{
{
opacity
:
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
>
)
}
...
...
src/pages/PlayPage.tsx
View file @
4d37e0d3
import
{
useState
}
from
'react'
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
{
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
{
playSound
}
from
'../lib/sounds'
...
...
@@ -15,23 +13,16 @@ const CATEGORIES = [
{
key
:
'classical'
,
label
:
'كلاسيكي'
,
icon
:
Hourglass
},
]
as
const
const
GAME_ICONS
:
Record
<
string
,
string
>
=
{
backgammon
:
'⚀'
,
dominoes
:
'🂡'
,
ludo
:
'⚄'
,
trivia
:
'?'
,
}
const
stagger
=
{
hidden
:
{},
show
:
{
transition
:
{
staggerChildren
:
0.0
6
},
transition
:
{
staggerChildren
:
0.0
7
},
},
}
const
fadeUp
=
{
hidden
:
{
opacity
:
0
,
y
:
2
0
},
show
:
{
opacity
:
1
,
y
:
0
,
transition
:
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
22
}
},
hidden
:
{
opacity
:
0
,
y
:
2
4
},
show
:
{
opacity
:
1
,
y
:
0
,
transition
:
{
type
:
'spring'
,
stiffness
:
400
,
damping
:
24
}
},
}
export
function
PlayPage
()
{
...
...
@@ -46,76 +37,160 @@ export function PlayPage() {
)?.[
1
].
category
return
(
<
PageTransition
>
<
PageTransition
className=
"!py-6"
>
<
motion
.
div
variants=
{
stagger
}
initial=
"hidden"
animate=
"show"
className=
"flex flex-col gap-6"
>
<
motion
.
h1
variants=
{
fadeUp
}
className=
"text-2xl font-black"
>
اختر اللعبة
</
motion
.
h1
>
<
motion
.
div
variants=
{
fadeUp
}
>
<
Card
variant=
"gold"
className=
"relative !p-6"
>
<
div
className=
"flex items-center gap-5"
>
<
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"
>
<
Crown
size=
{
38
}
className=
"text-gold"
/>
{
/* === 1. PAGE HEADER — Ribbon Banner === */
}
<
motion
.
div
variants=
{
fadeUp
}
className=
"flex justify-center"
>
<
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
>
{
/* Content */
}
<
div
className=
"relative p-6 flex items-center gap-5"
>
{
/* Crown with radial glow */
}
<
div
className=
"relative flex-shrink-0"
>
<
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
className=
"flex-1"
>
<
span
className=
"text-xl font-black"
>
{
chessGame
.
nameAr
}
</
span
>
<
p
className=
"text-xs text-text-secondary mt-
2
leading-relaxed"
>
<
span
className=
"text-xl font-black
text-text-primary
"
>
{
chessGame
.
nameAr
}
</
span
>
<
p
className=
"text-xs text-text-secondary mt-
1.5
leading-relaxed"
>
العب شطرنج اونلاين ضد لاعبين حقيقيين
</
p
>
{
/* Online indicator */
}
<
div
className=
"flex items-center gap-2 mt-2.5"
>
<
motion
.
div
className=
"w-2.5 h-2.5 rounded-full bg-green"
animate=
{
{
opacity
:
[
1
,
0.4
,
1
],
scale
:
[
1
,
1.3
,
1
]
}
}
transition=
{
{
duration
:
1.8
,
repeat
:
Infinity
}
}
/>
<
span
className=
"text-[11px] font-bold text-green"
>
اونلاين
</
span
>
</
div
>
</
div
>
</
div
>
<
motion
.
div
className=
"absolute top-4 left-4 w-3 h-3 rounded-full bg-green-500 border-2 border-green-400/50"
animate=
{
{
opacity
:
[
1
,
0.4
,
1
],
scale
:
[
1
,
1.2
,
1
]
}
}
transition=
{
{
duration
:
2
,
repeat
:
Infinity
}
}
/>
</
Card
>
{
/* Gold bottom accent */
}
<
div
className=
"h-[3px] w-full bg-gradient-to-r from-transparent via-gold to-transparent"
/>
</
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
)
=>
(
<
div
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"
>
<
span
className=
"text-2xl font-bold text-text-muted"
>
{
GAME_ICONS
[
game
.
key
]
||
''
}
</
span
>
</
div
>
<
span
className=
"text-sm font-black text-text-secondary"
>
{
game
.
nameAr
}
</
span
>
<
div
className=
"absolute inset-0 flex items-center justify-center bg-background/60 backdrop-blur-[2px]"
>
<
div
className=
"flex items-center gap-1.5 px-3.5 py-2 rounded-full bg-surface-2/90 border-2 border-border"
>
<
Lock
size=
{
13
}
className=
"text-text-muted"
/>
<
span
className=
"text-[11px] text-text-muted font-bold"
>
قريبا
</
span
>
{
/* Frosted overlay */
}
<
div
className=
"absolute inset-0 bg-background/40 backdrop-blur-[1px]"
/>
{
/* Lock badge */
}
<
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-[11px] font-bold text-text-muted block"
>
{
game
.
nameAr
}
</
span
>
<
span
className=
"text-[9px] text-text-muted/60"
>
قريبا
</
span
>
</
div
>
</
div
>
</
div
>
))
}
</
motion
.
div
>
{
/* === 4. TIME CONTROL SECTION === */
}
<
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
)
=>
{
const
isActive
=
activeCategory
===
cat
.
key
const
Icon
=
cat
.
icon
return
(
<
motion
.
button
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 ${
isActive
? 'bg-gold/20 border-gold text-gold shadow-md shadow-gold/15'
: 'bg-surface-2 border-border text-text-muted'
}`
}
whileTap=
{
{
scale
:
0.9
}
}
className=
"relative cursor-pointer"
whileTap=
{
{
scale
:
0.92
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
20
}
}
onClick=
{
()
=>
{
playSound
(
'click'
)
...
...
@@ -123,60 +198,128 @@ export function PlayPage() {
if
(
first
)
setSelectedTC
(
first
[
0
])
}
}
>
<
Icon
size=
{
14
}
/>
{
cat
.
label
}
<
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
}
</
div
>
</
motion
.
button
>
)
})
}
</
div
>
{
/* Time Options Grid */
}
<
div
className=
"grid grid-cols-3 gap-2.5"
>
{
Object
.
entries
(
TIME_CONTROLS
)
.
filter
(([,
v
])
=>
v
.
category
===
activeCategory
)
.
map
(([
key
,
tc
])
=>
(
<
motion
.
button
key=
{
key
}
className=
{
`py-4 rounded-xl text-center font-black text-sm border-3 transition-all ${
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
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
20
}
}
onClick=
{
()
=>
{
playSound
(
'click'
)
setSelectedTC
(
key
)
}
}
>
{
tc
.
labelAr
}
</
motion
.
button
>
))
}
.
map
(([
key
,
tc
])
=>
{
const
isSelected
=
selectedTC
===
key
return
(
<
motion
.
button
key=
{
key
}
className=
"relative cursor-pointer"
whileTap=
{
{
scale
:
0.9
}
}
animate=
{
isSelected
?
{
y
:
-
2
}
:
{
y
:
0
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
20
}
}
onClick=
{
()
=>
{
playSound
(
'click'
)
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
}
</
div
>
</
motion
.
button
>
)
})
}
</
div
>
</
motion
.
div
>
<
motion
.
div
variants=
{
fadeUp
}
className=
"flex flex-col items-center gap-3 mt-2 pb-4"
>
<
Button
{
/* === 5. ACTION BUTTONS === */
}
<
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=
{
()
=>
{
playSound
(
'click'
)
navigate
(
`/matchmaking?tc=${selectedTC}&game=chess`
)
}
}
className=
"w-[80%]"
size=
"lg"
>
البحث عن خصم
</
Button
>
<
Button
<
div
className=
"relative flex items-center justify-center gap-3 py-5 rounded-2xl font-black text-lg text-background overflow-hidden"
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=
{
()
=>
{
playSound
(
'click'
)
navigate
(
'/bot-select'
)
}
}
variant=
"ghost"
className=
"w-[80%]"
size=
"md"
>
<
Cpu
size=
{
18
}
className=
"text-purple"
/>
العب ضد الروبوت
</
Button
>
<
div
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"
/>
<
span
>
العب ضد الروبوت
</
span
>
</
div
>
</
motion
.
button
>
</
motion
.
div
>
</
motion
.
div
>
</
PageTransition
>
...
...
src/pages/ProfilePage.tsx
View file @
4d37e0d3
import
{
useState
}
from
'react'
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
{
useNotificationStore
}
from
'../stores/notificationStore'
import
{
PageTransition
}
from
'../components/layout/PageTransition'
import
{
Card
}
from
'../components/ui/Card'
import
{
Button
}
from
'../components/ui/Button'
import
{
supabase
}
from
'../lib/supabase'
const
stagger
=
{
hidden
:
{
opacity
:
0
},
show
:
{
opacity
:
1
,
transition
:
{
staggerChildren
:
0.0
6
}
},
show
:
{
opacity
:
1
,
transition
:
{
staggerChildren
:
0.0
8
}
},
}
const
item
=
{
hidden
:
{
opacity
:
0
,
y
:
1
2
,
scale
:
0.95
},
show
:
{
opacity
:
1
,
y
:
0
,
scale
:
1
,
transition
:
{
type
:
'spring'
,
stiffness
:
40
0
,
damping
:
22
}
},
hidden
:
{
opacity
:
0
,
y
:
1
6
,
scale
:
0.93
},
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
()
{
const
{
user
,
profile
,
setProfile
}
=
useAuthStore
()
const
{
showToast
}
=
useNotificationStore
()
...
...
@@ -74,116 +75,272 @@ export function ProfilePage() {
const
xpForNextLevel
=
500
const
currentXpInLevel
=
profile
.
xp
%
xpForNextLevel
const
xpRemaining
=
xpForNextLevel
-
currentXpInLevel
const
xpPercent
=
Math
.
min
((
currentXpInLevel
/
xpForNextLevel
)
*
100
,
100
)
const
ratings
=
[
{
label
:
'رصاصة'
,
value
:
profile
.
elo_bullet
,
color
:
'border-t-[#FF5252]'
},
{
label
:
'خاطف'
,
value
:
profile
.
elo_blitz
,
color
:
'border-t-[#FFC83D]'
},
{
label
:
'سريع'
,
value
:
profile
.
elo_rapid
,
color
:
'border-t-[#00E5CC]'
},
{
label
:
'كلاسيكي'
,
value
:
profile
.
elo_classical
,
color
:
'border-t-[#B44DFF]'
},
{
label
:
'رصاصة'
,
value
:
profile
.
elo_bullet
,
color
:
'#FF5252'
,
borderColor
:
'border-[#FF5252]'
},
{
label
:
'خاطف'
,
value
:
profile
.
elo_blitz
,
color
:
'#FFC83D'
,
borderColor
:
'border-[#FFC83D]'
},
{
label
:
'سريع'
,
value
:
profile
.
elo_rapid
,
color
:
'#00E5CC'
,
borderColor
:
'border-[#00E5CC]'
},
{
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
(
<
PageTransition
>
<
motion
.
div
className=
"flex flex-col gap-
5 py-
6"
className=
"flex flex-col gap-6"
variants=
{
stagger
}
initial=
"hidden"
animate=
"show"
>
<
motion
.
div
className=
"flex flex-col items-center gap-3"
variants=
{
item
}
>
<
div
className=
"relative"
>
<
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"
initial=
{
{
scale
:
0
,
rotate
:
-
20
}
}
animate=
{
{
scale
:
1
,
rotate
:
0
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
300
,
damping
:
18
}
}
>
<
span
className=
"text-4xl font-black text-[#FFC83D]"
>
{
profile
.
display_name
?.
charAt
(
0
)
||
'?'
}
</
span
>
</
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"
initial=
{
{
scale
:
0
}
}
animate=
{
{
scale
:
1
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
20
,
delay
:
0.2
}
}
>
<
span
className=
"text-[10px] font-black text-background"
>
{
profile
.
level
}
</
span
>
</
motion
.
div
>
</
div
>
<
div
className=
"text-center"
>
<
h1
className=
"text-xl font-black"
>
{
profile
.
display_name
}
</
h1
>
<
p
className=
"text-sm text-text-muted font-bold"
>
@
{
profile
.
username
}
</
p
>
{
profile
.
bio
&&
(
<
p
className=
"text-xs text-text-secondary mt-1 max-w-[220px] mx-auto line-clamp-2"
>
{
profile
.
bio
}
</
p
>
)
}
</
div
>
</
motion
.
div
>
{
/* === PLAYER CARD / NAMEPLATE === */
}
<
motion
.
div
variants=
{
item
}
>
<
Card
variant=
"gold"
className=
"!p-4"
>
<
div
className=
"flex items-center justify-between mb-2"
>
<
span
className=
"text-lg font-black"
>
المستوى
{
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"
>
{
profile
.
xp
}
XP
</
span
>
</
div
>
<
div
className=
"w-full h-4 rounded-full bg-surface-3 border-2 border-border overflow-hidden"
>
<
motion
.
div
className=
"h-full rounded-full bg-gradient-to-l from-[#FFC83D] to-[#FFE082]"
initial=
{
{
width
:
0
}
}
animate=
{
{
width
:
`${xpPercent}%`
}
}
transition=
{
{
duration
:
1.2
,
ease
:
'easeOut'
,
delay
:
0.3
}
}
/>
<
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"
>
<
motion
.
div
className=
"flex items-center justify-center"
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
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
300
,
damping
:
18
}
}
>
<
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
)
||
'?'
}
</
span
>
</
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
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
}
}
animate=
{
{
scale
:
1
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
20
,
delay
:
0.2
}
}
>
<
span
className=
"text-[10px] font-black text-[#0B0E1A]"
>
{
profile
.
level
}
</
span
>
</
motion
.
div
>
</
div
>
{
/* 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
>
{
profile
.
bio
&&
(
<
p
className=
"text-xs text-text-secondary mt-1.5 max-w-[240px] mx-auto line-clamp-2 leading-relaxed"
>
{
profile
.
bio
}
</
p
>
)
}
</
div
>
{
/* XP Progress bar */
}
<
div
className=
"w-full mt-2"
>
<
div
className=
"flex items-center justify-between mb-1.5"
>
<
span
className=
"text-xs font-black text-text-secondary"
>
المستوى
{
profile
.
level
}
</
span
>
<
span
className=
"text-xs font-black text-[#FFC83D]"
>
XP
{
currentXpInLevel
}
</
span
>
</
div
>
<
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
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
}
}
animate=
{
{
width
:
`${xpPercent}%`
}
}
transition=
{
{
duration
:
1.2
,
ease
:
'easeOut'
,
delay
:
0.4
}
}
/>
</
div
>
</
div
>
</
div
>
<
p
className=
"text-xs text-text-muted font-bold mt-2"
>
{
xpRemaining
}
XP للمستوى التالي
</
p
>
</
Card
>
</
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"
>
{
ratings
.
map
((
rating
,
i
)
=>
(
<
motion
.
div
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`
}
initial=
{
{
opacity
:
0
,
y
:
14
}
}
animate=
{
{
opacity
:
1
,
y
:
0
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
400
,
damping
:
22
,
delay
:
0.15
+
i
*
0.06
}
}
className=
"relative flex flex-col items-center justify-center overflow-hidden"
style=
{
{
height
:
90
,
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
}
}
>
<
span
className=
"text-2xl font-black"
>
{
rating
.
value
}
</
span
>
<
span
className=
"text-xs font-bold text-text-muted"
>
{
rating
.
label
}
</
span
>
{
/* 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 mt-2"
>
{
rating
.
value
}
</
span
>
<
span
className=
"text-[10px] font-bold text-text-muted"
>
{
rating
.
label
}
</
span
>
</
div
>
</
motion
.
div
>
))
}
</
div
>
</
motion
.
div
>
<
motion
.
div
variants=
{
item
}
>
<
div
className=
"grid grid-cols-4 gap-2"
>
<
StatBox
icon=
{
<
TrendingUp
size=
{
18
}
className=
"text-[#B44DFF]"
/>
}
value=
{
profile
.
total_games_played
}
label=
"مباريات"
/>
<
StatBox
icon=
{
<
Trophy
size=
{
18
}
className=
"text-[#FFC83D]"
/>
}
value=
{
profile
.
total_wins
}
label=
"انتصارات"
/>
<
StatBox
icon=
{
<
Target
size=
{
18
}
className=
"text-[#00E5CC]"
/>
}
value=
{
`${winRate}%`
}
label=
"نسبة الفوز"
/>
<
StatBox
icon=
{
<
Flame
size=
{
18
}
className=
"text-[#FF5252]"
/>
}
value=
{
profile
.
best_win_streak
}
label=
"افضل سلسلة"
/>
{
/* === STATS 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(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
>
</
motion
.
div
>
<
motion
.
div
className=
"flex flex-col items-center gap-3 mt-2"
variants=
{
item
}
>
<
Button
variant=
"ghost"
onClick=
{
openEdit
}
className=
"w-[80%]"
>
<
Pencil
size=
{
16
}
/>
تعديل الملف الشخصي
</
Button
>
{
/* === ACTION BUTTONS === */
}
<
motion
.
div
className=
"flex flex-col items-center gap-3 mt-1"
variants=
{
item
}
>
{
/* Edit profile button */
}
<
motion
.
button
className=
"text-sm font-bold text-[#FF5252]/70 flex items-center gap-1.5 py-2"
whileTap=
{
{
scale
:
0.9
}
}
className=
"w-[75%] flex items-center justify-center gap-2.5 py-3.5 rounded-2xl font-black text-sm"
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
}
>
<
LogOut
size=
{
14
}
/>
تسجيل الخروج
<
span
>
تسجيل الخروج
</
span
>
</
motion
.
button
>
</
motion
.
div
>
</
motion
.
div
>
{
/* === EDIT MODAL === */
}
<
AnimatePresence
>
{
editOpen
&&
(
<
motion
.
div
...
...
@@ -200,31 +357,58 @@ export function ProfilePage() {
exit=
{
{
opacity
:
0
}
}
/>
<
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
}
}
animate=
{
{
scale
:
1
,
opacity
:
1
,
y
:
0
}
}
exit=
{
{
scale
:
0.85
,
opacity
:
0
,
y
:
30
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
400
,
damping
:
22
}
}
>
<
div
className=
"flex items-center justify-between"
>
<
h2
className=
"text-lg font-black"
>
تعديل الملف الشخصي
</
h2
>
<
motion
.
button
whileTap=
{
{
scale
:
0.85
}
}
onClick=
{
()
=>
setEditOpen
(
false
)
}
className=
"p-2 rounded-xl bg-surface-3 border-2 border-border"
>
<
X
size=
{
16
}
className=
"text-text-muted"
/>
</
motion
.
button
>
{
/* Ribbon banner header */
}
<
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
>
<
div
className=
"flex flex-col gap-4"
>
{
/* Close button */
}
<
motion
.
button
whileTap=
{
{
scale
:
0.85
}
}
onClick=
{
()
=>
setEditOpen
(
false
)
}
className=
"absolute top-3 left-3 p-2 rounded-xl"
style=
{
{
background
:
'var(--color-surface-3)'
,
border
:
'2px solid var(--color-border)'
,
}
}
>
<
X
size=
{
14
}
className=
"text-text-muted"
/>
</
motion
.
button
>
{
/* Form */
}
<
div
className=
"flex flex-col gap-4 px-5 pb-5"
>
<
div
className=
"flex flex-col gap-1.5"
>
<
label
className=
"text-xs font-black text-text-secondary"
>
الاسم
</
label
>
<
input
type=
"text"
value=
{
editForm
.
display_name
}
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
}
dir=
"rtl"
/>
...
...
@@ -234,21 +418,26 @@ export function ProfilePage() {
<
textarea
value=
{
editForm
.
bio
}
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
}
dir=
"rtl"
/>
</
div
>
</
div
>
<
Button
onClick=
{
handleSave
}
loading=
{
saving
}
disabled=
{
!
editForm
.
display_name
.
trim
()
}
className=
"w-[80%] mx-auto"
>
حفظ التعديلات
</
Button
>
<
Button
onClick=
{
handleSave
}
loading=
{
saving
}
disabled=
{
!
editForm
.
display_name
.
trim
()
}
className=
"w-[80%] mx-auto mt-1"
>
حفظ التعديلات
</
Button
>
</
div
>
</
motion
.
div
>
</
motion
.
div
>
)
}
...
...
@@ -256,13 +445,3 @@ export function ProfilePage() {
</
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
{
useNavigate
}
from
'react-router-dom'
import
{
PageTransition
}
from
'../components/layout/PageTransition'
import
{
Card
}
from
'../components/ui/Card'
import
{
useUIStore
}
from
'../stores/uiStore'
import
{
supabase
}
from
'../lib/supabase'
...
...
@@ -17,49 +16,83 @@ export function SettingsPage() {
return
(
<
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"
>
<
Card
className=
"flex items-center justify-between"
>
<
div
className=
"flex flex-col gap-3"
>
{
/* Sound toggle */
}
<
div
className=
"game-panel !p-4 flex items-center justify-between"
>
<
div
className=
"flex items-center gap-3"
>
{
soundEnabled
?
<
Volume2
size=
{
20
}
className=
"text-cyan"
/>
:
<
VolumeX
size=
{
20
}
className=
"text-text-muted"
/>
}
<
span
className=
"text-sm font-bold"
>
الاصوات
</
span
>
{
soundEnabled
?
(
<
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
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
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
)
}
whileTap=
{
{
scale
:
0.9
}
}
>
<
motion
.
div
className=
{
`w-5 h-5 rounded-full ${soundEnabled ? 'bg-cyan' : 'bg-text-muted'}`
}
animate=
{
{
x
:
soundEnabled
?
0
:
28
}
}
className=
{
`w-[18px] h-[18px] rounded-full shadow-md ${
soundEnabled ? 'bg-[#00E5CC]' : 'bg-text-muted'
}`
}
animate=
{
{
x
:
soundEnabled
?
0
:
20
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
30
}
}
/>
</
motion
.
button
>
</
Card
>
</
div
>
<
Card
className=
"flex items-center gap-3"
>
<
Info
size=
{
20
}
className=
"text-gold"
/>
{
/* Report bug */
}
<
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
>
<
p
className=
"text-sm font-b
old"
>
EL3AB Player
</
p
>
<
p
className=
"text-
xs text-text-muted font-semibold"
>
الاصدار 1.0.0
</
p
>
<
p
className=
"text-sm font-b
lack"
>
الابلاغ عن مشكلة
</
p
>
<
p
className=
"text-
[11px] text-text-muted font-bold"
>
ساعدنا في تحسين التطبيق
</
p
>
</
div
>
</
Card
>
</
div
>
<
Card
className=
"flex items-center gap-3"
>
<
Bug
size=
{
20
}
className=
"text-purple"
/>
{
/* Version info - recessed panel */
}
<
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
>
<
p
className=
"text-sm font-bold"
>
الابلاغ عن مشكلة
</
p
>
<
p
className=
"text-
xs text-text-muted font-semibold"
>
ساعدنا في تحسين التطبيق
</
p
>
<
p
className=
"text-sm font-bold"
>
EL3AB Player
</
p
>
<
p
className=
"text-
[11px] text-text-muted font-semibold"
>
الاصدار 1.0.0
</
p
>
</
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
whileTap=
{
{
scale
:
0.9
5
}
}
whileTap=
{
{
scale
:
0.9
3
}
}
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
}
/>
<
span
>
تسجيل الخروج
</
span
>
...
...
@@ -67,11 +100,11 @@ export function SettingsPage() {
<
motion
.
button
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
}
/>
<
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
>
</
div
>
</
PageTransition
>
...
...
src/pages/ShopPage.tsx
View file @
4d37e0d3
import
{
useState
}
from
'react'
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
{
Button
}
from
'../components/ui/Button'
import
{
useAuthStore
}
from
'../stores/authStore'
import
{
useShop
,
type
Cosmetic
}
from
'../hooks/useShop'
import
{
RARITY_COLORS
}
from
'../lib/constants'
...
...
@@ -26,6 +25,14 @@ const RARITY_LABELS: Record<string, string> = {
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
()
{
const
{
profile
}
=
useAuthStore
()
const
[
filter
,
setFilter
]
=
useState
<
FilterType
>
(
'all'
)
...
...
@@ -58,33 +65,41 @@ export function ShopPage() {
return
(
<
PageTransition
className=
"flex flex-col gap-5"
>
{
/* Header */
}
<
div
className=
"flex items-center justify-between"
>
<
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
>
</
div
>
{
/* Currency capsules */
}
<
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]"
/>
<
span
className=
"text-sm font-black text-[#FFC83D]"
>
{
profile
?.
coins
||
0
}
</
span
>
</
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]"
/>
<
span
className=
"text-sm font-black text-[#B44DFF]"
>
{
profile
?.
gems
||
0
}
</
span
>
</
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
)
=>
(
<
motion
.
button
key=
{
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
? 'bg-
[#FF8C42] border-[#FF8C42] text-white
'
: 'bg-surface-2 border-border text-text-muted
hover:border-[#FF8C42]/40
'
? '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'
}`
}
style=
{
filter
!==
opt
.
key
?
{
boxShadow
:
'inset 0 2px 4px rgba(0,0,0,0.3)'
}
:
{}
}
whileTap=
{
{
scale
:
0.93
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
20
}
}
>
...
...
@@ -102,16 +117,21 @@ export function ShopPage() {
/>
</
div
>
)
:
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
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
}
}
animate=
{
{
scale
:
1
,
rotate
:
0
}
}
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
>
<
p
className=
"text-text-muted text-sm font-b
old"
>
لا توجد عناصر متاحة
</
p
>
<
p
className=
"text-text-muted text-sm font-b
lack"
>
لا توجد عناصر
</
p
>
</
div
>
)
:
(
<
div
className=
"grid grid-cols-2 gap-4"
>
...
...
@@ -120,37 +140,48 @@ export function ShopPage() {
const
isEquipped
=
equippedIds
.
includes
(
item
.
id
)
const
rarityColor
=
RARITY_COLORS
[
item
.
rarity
]
const
isLegendary
=
item
.
rarity
===
'legendary'
const
rotation
=
index
%
2
===
0
?
'-1deg'
:
'1deg'
return
(
<
motion
.
button
key=
{
item
.
id
}
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' : ''}`
}
style=
{
{
borderColor
:
rarityColor
}
}
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
,
transform
:
`rotate(${rotation})`
,
boxShadow
:
RARITY_GLOW
[
item
.
rarity
],
}
}
initial=
{
{
opacity
:
0
,
y
:
16
,
scale
:
0.9
}
}
animate=
{
{
opacity
:
1
,
y
:
0
,
scale
:
1
}
}
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
}
}
>
{
/* Legendary shimmer overlay */
}
{
isLegendary
&&
(
<
motion
.
div
className=
"absolute inset-0 rounded-2xl pointer-events-none"
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
}
}
<
div
className=
"absolute inset-0 rounded-2xl pointer-events-none animate-shimmer"
style=
{
{
background
:
'linear-gradient(110deg, transparent 25%, rgba(255,200,61,0.1) 50%, transparent 75%)'
,
backgroundSize
:
'200% 100%'
}
}
/>
)
}
{
/* Owned stamp - shield overlay */
}
{
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"
>
<
Check
size=
{
10
}
className=
"text-green-400"
/>
<
span
className=
"text-[9px] font-black text-green-400"
>
{
isEquipped
?
'مفعّل'
:
'مملوك'
}
</
span
>
<
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"
/>
<
span
className=
"text-[7px] font-black text-green-400 leading-none mt-0.5"
>
{
isEquipped
?
'مفعّل'
:
'مملوك'
}
</
span
>
</
div
>
</
div
>
)
}
{
/* Preview */
}
<
div
className=
"w-full aspect-square rounded-xl bg-surface-2 border-2 flex items-center justify-center"
style=
{
{
borderColor
:
`${rarityColor}40`
}
}
...
...
@@ -169,16 +200,16 @@ export function ShopPage() {
)
}
</
div
>
<
div
className=
"flex items-center gap-1.5"
>
<
div
className=
"w-2.5 h-2.5 rounded-full border-2"
style=
{
{
backgroundColor
:
rarityColor
,
borderColor
:
`${rarityColor}80`
}
}
/>
<
span
className=
"text-xs font-black text-text-primary truncate max-w-[90px]"
>
{
/* Name - ribbon style */
}
<
div
className=
"w-[110%] -mx-[5%] py-1.5 bg-surface-2 border-t-2 border-b-2"
style=
{
{
borderColor
:
`${rarityColor}30`
}
}
>
<
span
className=
"text-xs font-black text-text-primary truncate block px-2"
>
{
item
.
name_ar
}
</
span
>
</
div
>
{
/* Price ribbon */
}
{
!
isOwned
&&
(
<
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
?
(
...
...
@@ -200,6 +231,7 @@ export function ShopPage() {
</
div
>
)
}
{
/* Purchase Modal */
}
<
AnimatePresence
>
{
selectedItem
&&
(
<
motion
.
div
...
...
@@ -210,138 +242,143 @@ export function ShopPage() {
onClick=
{
()
=>
!
purchasing
&&
setSelectedItem
(
null
)
}
>
<
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
}
}
animate=
{
{
scale
:
1
,
opacity
:
1
,
rotate
:
0
}
}
exit=
{
{
scale
:
0.7
,
opacity
:
0
,
rotate
:
3
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
350
,
damping
:
22
}
}
onClick=
{
(
e
)
=>
e
.
stopPropagation
()
}
>
<
button
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"
>
<
X
size=
{
16
}
className=
"text-text-muted"
/>
</
button
>
{
/* Rarity-colored header */
}
<
div
className=
"w-full h-2 rounded-t-2xl"
style=
{
{
backgroundColor
:
RARITY_COLORS
[
selectedItem
.
rarity
]
}
}
/>
{
purchaseSuccess
?
(
<
motion
.
div
className=
"flex flex-col items-center gap-4 py-8"
initial=
{
{
scale
:
0
}
}
animate=
{
{
scale
:
1
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
300
,
damping
:
18
}
}
<
div
className=
"p-6 flex flex-col items-center gap-4 w-full"
>
<
button
onClick=
{
()
=>
!
purchasing
&&
setSelectedItem
(
null
)
}
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"
/>
</
button
>
{
purchaseSuccess
?
(
<
motion
.
div
className=
"w-20 h-20 rounded-full bg-green-500/10 border-3 border-green-500/50 flex items-center justify-center"
animate=
{
{
scale
:
[
1
,
1.15
,
1
]
}
}
transition=
{
{
duration
:
0.5
}
}
className=
"flex flex-col items-center gap-4 py-8"
initial=
{
{
scale
:
0
}
}
animate=
{
{
scale
:
1
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
300
,
damping
:
18
}
}
>
<
Check
size=
{
36
}
className=
"text-green-400"
/>
<
motion
.
div
className=
"w-20 h-20 rounded-full bg-green-500/10 border-3 border-green-500/50 flex items-center justify-center"
animate=
{
{
scale
:
[
1
,
1.15
,
1
]
}
}
transition=
{
{
duration
:
0.5
}
}
>
<
Check
size=
{
36
}
className=
"text-green-400"
/>
</
motion
.
div
>
<
p
className=
"text-xl font-black text-green-400"
>
تم الشراء بنجاح
</
p
>
</
motion
.
div
>
<
p
className=
"text-xl font-black text-green-400"
>
تم الشراء بنجاح
</
p
>
</
motion
.
div
>
)
:
(
<>
<
div
className=
"w-full aspect-[4/3] rounded-2xl bg-surface-2 border-2 flex items-center justify-center mt-6"
style=
{
{
borderColor
:
`${RARITY_COLORS[selectedItem.rarity]}40`
}
}
>
{
selectedItem
.
preview_url
?
(
<
img
src=
{
selectedItem
.
preview_url
}
alt=
{
selectedItem
.
name_ar
}
className=
"w-full h-full object-cover rounded-2xl"
/>
)
:
(
<
div
className=
"w-24 h-24 rounded-full"
style=
{
{
background
:
`radial-gradient(circle, ${RARITY_COLORS[selectedItem.rarity]}60, ${RARITY_COLORS[selectedItem.rarity]}15)`
,
}
}
/>
)
}
</
div
>
<
div
className=
"text-center"
>
<
h3
className=
"text-xl font-black text-text-primary"
>
{
selectedItem
.
name_ar
}
</
h3
>
{
selectedItem
.
description
&&
(
<
p
className=
"text-xs text-text-muted mt-1"
>
{
selectedItem
.
description
}
</
p
>
)
}
</
div
>
<
div
className=
"flex items-center gap-2"
>
)
:
(
<>
<
div
className=
"w-3 h-3 rounded-full border-2"
style=
{
{
backgroundColor
:
RARITY_COLORS
[
selectedItem
.
rarity
],
borderColor
:
`${RARITY_COLORS[selectedItem.rarity]}80`
}
}
/>
<
span
className=
"text-sm font-black"
style=
{
{
color
:
RARITY_COLORS
[
selectedItem
.
rarity
]
}
}
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`
}
}
>
{
RARITY_LABELS
[
selectedItem
.
rarity
]
}
</
span
>
{
selectedItem
.
rarity
===
'legendary'
&&
(
<
Sparkles
size=
{
14
}
style=
{
{
color
:
RARITY_COLORS
.
legendary
}
}
/>
)
}
</
div
>
{
ownedIds
.
includes
(
selectedItem
.
id
)
?
(
<
div
className=
"flex flex-col gap-3 w-[70%] mx-auto"
>
{
equippedIds
.
includes
(
selectedItem
.
id
)
?
(
<
div
className=
"flex items-center justify-center gap-2 py-3 rounded-xl bg-green-500/10 border-3 border-green-500/40"
>
<
Check
size=
{
16
}
className=
"text-green-400"
/>
<
span
className=
"text-sm font-black text-green-400"
>
مفعّل حاليا
</
span
>
</
div
>
{
selectedItem
.
preview_url
?
(
<
img
src=
{
selectedItem
.
preview_url
}
alt=
{
selectedItem
.
name_ar
}
className=
"w-full h-full object-cover rounded-2xl"
/>
)
:
(
<
Button
variant=
"cyan"
size=
"md"
onClick=
{
handleEquip
}
loading=
{
purchasing
}
className=
"w-full"
>
تفعيل
</
Button
>
<
div
className=
"w-24 h-24 rounded-full"
style=
{
{
background
:
`radial-gradient(circle, ${RARITY_COLORS[selectedItem.rarity]}60, ${RARITY_COLORS[selectedItem.rarity]}15)`
,
}
}
/>
)
}
</
div
>
)
:
(
<
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"
>
{
selectedItem
.
price_gems
?
(
<>
<
Gem
size=
{
18
}
className=
"text-[#B44DFF]"
/>
<
span
className=
"text-lg font-black text-[#B44DFF]"
>
{
selectedItem
.
price_gems
}
</
span
>
</>
)
:
(
<>
<
Coins
size=
{
18
}
className=
"text-[#FFC83D]"
/>
<
span
className=
"text-lg font-black text-[#FFC83D]"
>
{
selectedItem
.
price_coins
}
</
span
>
</>
)
}
</
div
>
<
Button
variant=
"gold"
size=
"md"
onClick=
{
handlePurchase
}
loading=
{
purchasing
}
disabled=
{
selectedItem
.
price_gems
?
(
profile
?.
gems
||
0
)
<
(
selectedItem
.
price_gems
||
0
)
:
(
profile
?.
coins
||
0
)
<
(
selectedItem
.
price_coins
||
0
)
}
className=
"w-full"
<
div
className=
"text-center"
>
<
h3
className=
"text-xl font-black text-text-primary"
>
{
selectedItem
.
name_ar
}
</
h3
>
{
selectedItem
.
description
&&
(
<
p
className=
"text-xs text-text-muted mt-1"
>
{
selectedItem
.
description
}
</
p
>
)
}
</
div
>
<
div
className=
"flex items-center gap-2"
>
<
div
className=
"w-3 h-3 rounded-full border-2"
style=
{
{
backgroundColor
:
RARITY_COLORS
[
selectedItem
.
rarity
],
borderColor
:
`${RARITY_COLORS[selectedItem.rarity]}80`
}
}
/>
<
span
className=
"text-sm font-black"
style=
{
{
color
:
RARITY_COLORS
[
selectedItem
.
rarity
]
}
}
>
شراء
</
Button
>
{
(
selectedItem
.
price_gems
?
(
profile
?.
gems
||
0
)
<
(
selectedItem
.
price_gems
||
0
)
:
(
profile
?.
coins
||
0
)
<
(
selectedItem
.
price_coins
||
0
))
&&
(
<
p
className=
"text-[11px] text-[#FF5252] text-center font-bold"
>
رصيد غير كافي
</
p
>
{
RARITY_LABELS
[
selectedItem
.
rarity
]
}
</
span
>
{
selectedItem
.
rarity
===
'legendary'
&&
(
<
Sparkles
size=
{
14
}
style=
{
{
color
:
RARITY_COLORS
.
legendary
}
}
/>
)
}
</
div
>
)
}
</>
)
}
{
ownedIds
.
includes
(
selectedItem
.
id
)
?
(
<
div
className=
"flex flex-col gap-3 w-[70%] mx-auto"
>
{
equippedIds
.
includes
(
selectedItem
.
id
)
?
(
<
div
className=
"flex items-center justify-center gap-2 py-3 rounded-xl bg-green-500/10 border-3 border-green-500/40"
>
<
Check
size=
{
16
}
className=
"text-green-400"
/>
<
span
className=
"text-sm font-black text-green-400"
>
مفعّل حاليا
</
span
>
</
div
>
)
:
(
<
motion
.
button
whileTap=
{
{
scale
:
0.93
}
}
onClick=
{
handleEquip
}
disabled=
{
purchasing
}
className=
"btn-3d w-full py-3 rounded-xl bg-[#00E5CC] text-background text-sm font-black disabled:opacity-50"
>
{
purchasing
?
'جاري...'
:
'تفعيل'
}
</
motion
.
button
>
)
}
</
div
>
)
:
(
<
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"
style=
{
{
boxShadow
:
'inset 0 2px 4px rgba(0,0,0,0.2)'
}
}
>
{
selectedItem
.
price_gems
?
(
<>
<
Gem
size=
{
18
}
className=
"text-[#B44DFF]"
/>
<
span
className=
"text-lg font-black text-[#B44DFF]"
>
{
selectedItem
.
price_gems
}
</
span
>
</>
)
:
(
<>
<
Coins
size=
{
18
}
className=
"text-[#FFC83D]"
/>
<
span
className=
"text-lg font-black text-[#FFC83D]"
>
{
selectedItem
.
price_coins
}
</
span
>
</>
)
}
</
div
>
<
motion
.
button
whileTap=
{
{
scale
:
0.93
}
}
onClick=
{
handlePurchase
}
disabled=
{
purchasing
||
(
selectedItem
.
price_gems
?
(
profile
?.
gems
||
0
)
<
(
selectedItem
.
price_gems
||
0
)
:
(
profile
?.
coins
||
0
)
<
(
selectedItem
.
price_coins
||
0
))
}
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
?
'جاري...'
:
'شراء'
}
</
motion
.
button
>
{
(
selectedItem
.
price_gems
?
(
profile
?.
gems
||
0
)
<
(
selectedItem
.
price_gems
||
0
)
:
(
profile
?.
coins
||
0
)
<
(
selectedItem
.
price_coins
||
0
))
&&
(
<
p
className=
"text-[11px] text-[#FF5252] text-center font-bold"
>
رصيد غير كافي
</
p
>
)
}
</
div
>
)
}
</>
)
}
</
div
>
</
motion
.
div
>
</
motion
.
div
>
)
}
...
...
src/pages/TournamentsPage.tsx
View file @
4d37e0d3
import
{
useState
}
from
'react'
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
{
Card
}
from
'../components/ui/Card'
import
{
Button
}
from
'../components/ui/Button'
import
{
useAuthStore
}
from
'../stores/authStore'
import
{
useTournaments
}
from
'../hooks/useTournaments'
...
...
@@ -33,21 +31,11 @@ const TIME_CONTROL_LABELS: Record<string, string> = {
classical
:
'كلاسيكي'
,
}
function
StatusBadge
({
status
}:
{
status
:
string
})
{
const
config
:
Record
<
string
,
{
label
:
string
;
classes
:
string
}
>
=
{
registration
:
{
label
:
'مفتوحة'
,
classes
:
'bg-[#00E5CC]/15 text-[#00E5CC] border-[#00E5CC]/50'
},
in_progress
:
{
label
:
'جارية'
,
classes
:
'bg-[#FFC83D]/15 text-[#FFC83D] border-[#FFC83D]/50 animate-pulse'
},
completed
:
{
label
:
'منتهية'
,
classes
:
'bg-surface-3 text-text-muted border-border'
},
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
>
)
const
STATUS_CONFIG
:
Record
<
string
,
{
label
:
string
;
color
:
string
;
bg
:
string
}
>
=
{
registration
:
{
label
:
'مفتوحة'
,
color
:
'#00E5CC'
,
bg
:
'rgba(0,229,204,0.15)'
},
in_progress
:
{
label
:
'جارية'
,
color
:
'#FFC83D'
,
bg
:
'rgba(255,200,61,0.15)'
},
completed
:
{
label
:
'منتهية'
,
color
:
'#6E748C'
,
bg
:
'rgba(110,116,140,0.15)'
},
cancelled
:
{
label
:
'ملغاة'
,
color
:
'#FF5252'
,
bg
:
'rgba(255,82,82,0.15)'
},
}
export
function
TournamentsPage
()
{
...
...
@@ -60,22 +48,32 @@ export function TournamentsPage() {
return
(
<
PageTransition
className=
"flex flex-col gap-5"
>
<
div
className=
"flex items-center gap-2.5"
>
<
Trophy
size=
{
24
}
className=
"text-[#FFC83D]"
/>
<
h1
className=
"text-2xl font-black"
>
البطولات
</
h1
>
{
/* Header - Banner shape */
}
<
div
className=
"flex items-center gap-3"
>
<
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
>
{
/* Filter tabs - thick bordered buttons */
}
<
div
className=
"flex gap-2 overflow-x-auto no-scrollbar pb-1"
>
{
FILTERS
.
map
((
f
)
=>
(
<
motion
.
button
key=
{
f
.
label
}
whileTap=
{
{
scale
:
0.93
}
}
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
? 'bg-
[#00E5CC] border-[#00E5CC] text-background font-black
'
: 'bg-surface-2 text-text-muted border-border
hover:border-[#00E5CC]/40
'
? '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'
}`
}
style=
{
activeFilter
!==
f
.
value
?
{
boxShadow
:
'inset 0 2px 4px rgba(0,0,0,0.3)'
}
:
{}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
500
,
damping
:
20
}
}
>
{
f
.
label
}
...
...
@@ -86,105 +84,149 @@ export function TournamentsPage() {
{
loading
?
(
<
div
className=
"flex flex-col gap-4"
>
{
[
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-24 h-3 rounded bg-surface-3"
/>
</
div
>
))
}
</
div
>
)
:
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
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
}
}
animate=
{
{
scale
:
1
,
rotate
:
0
}
}
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
>
<
p
className=
"text-text-muted text-sm font-b
old
"
>
لا توجد بطولات
</
p
>
<
p
className=
"text-text-muted text-sm font-b
lack
"
>
لا توجد بطولات
</
p
>
</
div
>
)
:
(
<
div
className=
"flex flex-col gap-4"
>
<
AnimatePresence
>
{
tournaments
.
map
((
t
,
i
)
=>
(
<
motion
.
div
key=
{
t
.
id
}
initial=
{
{
opacity
:
0
,
y
:
20
,
scale
:
0.95
}
}
animate=
{
{
opacity
:
1
,
y
:
0
,
scale
:
1
}
}
exit=
{
{
opacity
:
0
,
y
:
-
16
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
400
,
damping
:
24
,
delay
:
i
*
0.06
}
}
>
<
Card
className=
"flex flex-col gap-3 relative"
>
<
div
className=
"absolute top-4 left-4"
>
<
StatusBadge
status=
{
t
.
status
}
/>
</
div
>
<
h3
className=
"text-base font-black leading-tight pr-0 pl-20"
>
{
t
.
name_ar
||
t
.
name
}
</
h3
>
<
div
className=
"flex items-center gap-4 text-xs text-text-muted font-bold"
>
<
span
className=
"flex items-center gap-1.5 bg-surface-2 border-2 border-border rounded-lg px-2 py-1"
>
{
FORMAT_LABELS
[
t
.
format
]
??
t
.
format
}
</
span
>
<
span
className=
"flex items-center gap-1"
>
<
Clock
size=
{
12
}
/>
{
TIME_CONTROL_LABELS
[
t
.
time_control
]
??
t
.
time_control
}
</
span
>
<
span
className=
"flex items-center gap-1"
>
<
Users
size=
{
12
}
/>
{
t
.
registrations_count
}
/
{
t
.
max_players
}
</
span
>
</
div
>
{
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
key=
{
t
.
id
}
initial=
{
{
opacity
:
0
,
y
:
20
,
scale
:
0.95
}
}
animate=
{
{
opacity
:
1
,
y
:
0
,
scale
:
1
}
}
exit=
{
{
opacity
:
0
,
y
:
-
16
}
}
transition=
{
{
type
:
'spring'
,
stiffness
:
400
,
damping
:
24
,
delay
:
i
*
0.06
}
}
>
<
div
className=
"game-panel relative overflow-hidden !p-5"
>
{
/* Status ribbon - diagonal corner badge */
}
<
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
>
{
(
t
.
prize_pool_coins
>
0
||
t
.
prize_pool_gems
>
0
)
&&
(
<
div
className=
"flex items-center gap
-3"
>
{
t
.
prize_pool_coins
>
0
&&
(
<
span
className=
"flex items-center gap-1.5 text-[#FFC83D] font-black text-sm"
>
<
Coins
size=
{
14
}
className=
"text-[#FFC83D]"
/>
{
t
.
prize_pool_coins
.
toLocaleString
(
'ar-EG'
)
}
</
span
>
)
}
{
t
.
prize_pool_gems
>
0
&&
(
<
span
className=
"flex items-center gap-1.5 text-[#B44DFF] font-black text-sm"
>
<
Gem
size=
{
14
}
className=
"text-[#B44DFF]"
/
>
{
t
.
prize_pool_gems
.
toLocaleString
(
'ar-EG'
)
}
</
span
>
)
}
{
/* Tournament name */
}
<
h3
className=
"text-lg font-black leading-tight mt-5 mb
-3"
>
{
t
.
name_ar
||
t
.
name
}
</
h3
>
{
/* Info row */
}
<
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
}
</
span
>
<
span
className=
"flex items-center gap-1"
>
<
Clock
size=
{
12
}
/>
{
TIME_CONTROL_LABELS
[
t
.
time_control
]
??
t
.
time_control
}
</
span
>
</
div
>
)
}
{
t
.
status
===
'registration'
&&
user
&&
(
<
div
className=
"flex justify-center mt-1"
>
{
isRegistered
(
t
.
id
)
?
(
<
div
className=
"flex items-center gap-2"
>
<
span
className=
"px-4 py-2 rounded-xl bg-[#FFC83D]/15 border-2 border-[#FFC83D]/50 text-[#FFC83D] text-xs font-black"
>
مسجل
{
/* Prize display - treasure row */
}
{
(
t
.
prize_pool_coins
>
0
||
t
.
prize_pool_gems
>
0
)
&&
(
<
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
&&
(
<
span
className=
"flex items-center gap-1.5 text-[#FFC83D] font-black text-sm"
>
<
Coins
size=
{
16
}
className=
"text-[#FFC83D]"
/>
{
t
.
prize_pool_coins
.
toLocaleString
(
'ar-EG'
)
}
</
span
>
)
}
{
t
.
prize_pool_gems
>
0
&&
(
<
span
className=
"flex items-center gap-1.5 text-[#B44DFF] font-black text-sm"
>
<
Gem
size=
{
16
}
className=
"text-[#B44DFF]"
/>
{
t
.
prize_pool_gems
.
toLocaleString
(
'ar-EG'
)
}
</
span
>
)
}
</
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
&&
(
<
div
className=
"flex justify-center mt-2"
>
{
isRegistered
(
t
.
id
)
?
(
<
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
>
<
motion
.
button
whileTap=
{
{
scale
:
0.93
}
}
onClick=
{
()
=>
unregister
(
t
.
id
)
}
className=
"btn-3d px-3.5 py-2 rounded-xl bg-[#FF5252] text-white text-xs font-black"
>
إلغاء
</
motion
.
button
>
</
div
>
)
:
(
<
motion
.
button
whileTap=
{
{
scale
:
0.93
}
}
onClick=
{
()
=>
un
register
(
t
.
id
)
}
className=
"
px-3 py-2 rounded-xl bg-[#FF5252]/10 border-2 border-[#FF5252]/30 text-[#FF5252] text-xs font-bold
"
onClick=
{
()
=>
register
(
t
.
id
)
}
className=
"
btn-3d w-[75%] py-3 rounded-xl bg-gradient-to-b from-[#FFC83D] to-[#E5A800] text-background text-sm font-black
"
>
إلغاء
سجل الان
</
motion
.
button
>
</
div
>
)
:
(
<
Button
variant=
"cyan"
size=
"sm"
onClick=
{
()
=>
register
(
t
.
id
)
}
className=
"w-[80%] mx-auto"
>
سجل
</
Button
>
)
}
</
div
>
)
}
</
Card
>
</
motion
.
div
>
))
}
)
}
</
div
>
)
}
</
div
>
</
motion
.
div
>
)
})
}
</
AnimatePresence
>
</
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