Commit 4d37e0d3 authored by Mahmoud Aglan's avatar Mahmoud Aglan

go

parent 6a8bf93c
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()
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>
)
......
......@@ -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>
......
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>
)
}
......@@ -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.gems}</span>
<Coins size={14} className="text-gold" />
<span className="text-xs font-black text-gold">{profile.coins}</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>
......
......@@ -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: 20 }}
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: 24, 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}
......
export { AppShell } from './AppShell'
export { DecorativeBackground } from './DecorativeBackground'
export { Header } from './Header'
export { BottomNav } from './BottomNav'
export { PageTransition } from './PageTransition'
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>
)
}
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>
)
}
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>
)
}
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>
)
}
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'
......@@ -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 2s ease-in-out infinite;
.animate-float-gentle {
animation: float-gentle 3s ease-in-out infinite;
}
.animate-float {
animation: float 3s ease-in-out infinite;
.animate-pulse-soft {
animation: pulse-soft 2s ease-in-out infinite;
}
/* === RESPONSIVE CONTAINER === */
/* ============================================================
RESPONSIVE CONTAINER
============================================================ */
.app-container {
width: 100%;
max-width: 480px;
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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)' }}
>
الغاء
</Button>
</motion.button>
</motion.div>
</motion.div>
)}
......
This diff is collapsed.
This diff is collapsed.
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-bold">EL3AB Player</p>
<p className="text-xs text-text-muted font-semibold">الاصدار 1.0.0</p>
<p className="text-sm font-black">الابلاغ عن مشكلة</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.95 }}
whileTap={{ scale: 0.93 }}
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-50 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-40 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>
......
This diff is collapsed.
This diff is collapsed.
{
"status": "failed",
"failedTests": []
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment