Commit 271b0c17 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat: apply 30 UI/UX improvements across all pages

Game: coordinate labels, player panels, captured pieces, move list,
material advantage, glowing turn indicator, tighter layout.
Home: quick actions, daily tips, better empty state, stat card borders.
Play: chess hero card, larger pills, removed debug button.
BotSelect: back nav, difficulty dots, colored avatar fallback.
Profile: XP fix, section dividers, achievements placeholder, better logout.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent c8dabe57
......@@ -5,7 +5,7 @@ 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 { Cpu, Swords } from 'lucide-react'
import { ChevronRight, Swords } from 'lucide-react'
const DIFFICULTY_COLORS: Record<string, string> = {
beginner: '#4ECDC4',
......@@ -17,6 +17,8 @@ const DIFFICULTY_COLORS: Record<string, string> = {
near_perfect: '#FFD700',
}
const TOTAL_STRENGTH_DOTS = 7
export function BotSelectPage() {
const navigate = useNavigate()
const [bots, setBots] = useState<Bot[]>([])
......@@ -45,9 +47,16 @@ export function BotSelectPage() {
}
return (
<PageTransition className="px-4 py-6 flex flex-col gap-5">
<div className="flex items-center gap-2">
<Cpu size={20} className="text-gold" />
<PageTransition className="px-4 py-6 flex flex-col gap-5 pb-32">
{/* Header with back button */}
<div className="flex items-center gap-3">
<motion.button
onClick={() => navigate(-1)}
className="w-9 h-9 rounded-full bg-surface-2 border border-border flex items-center justify-center"
whileTap={{ scale: 0.9 }}
>
<ChevronRight size={18} className="text-text-secondary" />
</motion.button>
<h1 className="text-xl font-bold">العب ضد الروبوت</h1>
</div>
......@@ -57,6 +66,7 @@ export function BotSelectPage() {
{bots.map((bot, i) => {
const isSelected = selectedBot === bot.id
const diffColor = DIFFICULTY_COLORS[bot.style] || '#6B6B80'
const strengthLevel = Math.min(i + 1, TOTAL_STRENGTH_DOTS)
return (
<motion.div
......@@ -68,22 +78,34 @@ export function BotSelectPage() {
<Card
glow={isSelected}
onClick={() => setSelectedBot(bot.id)}
className={`flex items-center gap-3 transition-all ${isSelected ? 'border-gold/60' : ''}`}
className={`flex items-center gap-3 transition-all ${
isSelected
? 'border-gold/60 scale-[1.02] border-r-4'
: ''
}`}
>
<div className="relative w-12 h-12 rounded-full overflow-hidden bg-surface-3 flex-shrink-0">
{/* Bot Portrait with colored fallback */}
<div
className="relative w-12 h-12 rounded-full overflow-hidden flex-shrink-0"
style={{ backgroundColor: `${diffColor}20` }}
>
<img
src={getBotPortraitUrl(bot.id)}
alt={bot.name}
className="w-full h-full object-cover"
className="w-full h-full object-cover relative z-10"
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none'
}}
/>
<div className="absolute inset-0 flex items-center justify-center">
<Cpu size={18} className="text-text-muted" />
<div
className="absolute inset-0 flex items-center justify-center"
style={{ color: diffColor }}
>
<span className="text-lg font-bold">{bot.name_ar?.charAt(0) || bot.name.charAt(0)}</span>
</div>
</div>
{/* Bot Info */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<h3 className="text-sm font-bold truncate">{bot.name_ar}</h3>
......@@ -95,13 +117,26 @@ export function BotSelectPage() {
</span>
</div>
<p className="text-[11px] text-text-muted truncate mt-0.5">{bot.bio_ar}</p>
<div className="flex items-center gap-2 mt-1">
<div className="flex items-center gap-3 mt-1.5">
<span className="text-[10px] text-text-muted">
{bot.elo_min}-{bot.elo_max} تقييم
</span>
{/* Strength dots */}
<div className="flex items-center gap-0.5">
{Array.from({ length: TOTAL_STRENGTH_DOTS }).map((_, dotIndex) => (
<div
key={dotIndex}
className="w-1.5 h-1.5 rounded-full transition-colors"
style={{
backgroundColor: dotIndex < strengthLevel ? diffColor : `${diffColor}30`,
}}
/>
))}
</div>
</div>
</div>
{/* Selection indicator */}
{isSelected && (
<motion.div
className="w-5 h-5 rounded-full bg-gold flex items-center justify-center flex-shrink-0"
......@@ -118,9 +153,10 @@ export function BotSelectPage() {
})}
</div>
{/* Bottom CTA with safe area */}
{selectedBot && (
<motion.div
className="sticky bottom-20 pt-3"
className="fixed bottom-0 left-0 right-0 p-4 pb-8 bg-gradient-to-t from-background via-background to-transparent"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ type: 'spring', stiffness: 400, damping: 25 }}
......
import { useState, useCallback, useEffect, useRef } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { Chess } from 'chess.js'
import { Flag, RotateCcw, Cpu } from 'lucide-react'
import { Flag, RotateCcw, Cpu, ChevronRight, Crown } from 'lucide-react'
import { useNavigate, useParams, useLocation } from 'react-router-dom'
import { Button } from '../components/ui/Button'
import { getBotMove, uciToMove, getBotPortraitUrl } from '../lib/stockfish'
import { getBotMove, uciToMove, getBotPortraitUrl, fetchBots } from '../lib/stockfish'
import { playSound } from '../lib/sounds'
const PIECE_UNICODE: Record<string, string> = {
......@@ -17,6 +17,11 @@ const DARK_SQUARE = '#4A3728'
const HIGHLIGHT_COLOR = 'rgba(212, 168, 67, 0.4)'
const LEGAL_DOT_COLOR = 'rgba(212, 168, 67, 0.5)'
const PIECE_VALUES: Record<string, number> = { p: 1, n: 3, b: 3, r: 5, q: 9 }
const FILES = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
const RANKS = ['8', '7', '6', '5', '4', '3', '2', '1']
export function GamePage() {
const navigate = useNavigate()
const { id } = useParams<{ id: string }>()
......@@ -33,7 +38,23 @@ export function GamePage() {
const [result, setResult] = useState<string | null>(null)
const [botThinking, setBotThinking] = useState(false)
const [playerColor] = useState<'w' | 'b'>('w')
const [capturedByWhite, setCapturedByWhite] = useState<string[]>([])
const [capturedByBlack, setCapturedByBlack] = useState<string[]>([])
const [botNameAr, setBotNameAr] = useState<string>('')
const botMoveInProgress = useRef(false)
const moveListRef = useRef<HTMLDivElement>(null)
// Fetch bot Arabic name
useEffect(() => {
if (!botId) return
fetchBots()
.then((bots) => {
const bot = bots.find((b) => b.id === botId)
if (bot) setBotNameAr(bot.name_ar)
else setBotNameAr(botId)
})
.catch(() => setBotNameAr(botId))
}, [botId])
const getSquareName = useCallback((row: number, col: number): string => {
const file = String.fromCharCode(97 + col)
......@@ -67,6 +88,17 @@ export function GamePage() {
return false
}, [isBot, playerColor])
const addCapturedPiece = useCallback((move: { captured?: string; color: string }) => {
if (move.captured) {
// The color in the move is who made the capture
if (move.color === 'w') {
setCapturedByWhite((prev) => [...prev, move.captured!])
} else {
setCapturedByBlack((prev) => [...prev, move.captured!])
}
}
}, [])
const makeBotMove = useCallback(async (currentGame: Chess) => {
if (botMoveInProgress.current) return
if (!botId || currentGame.isGameOver()) return
......@@ -85,6 +117,7 @@ export function GamePage() {
if (move) {
setGame(newGame)
setLastMove({ from, to })
addCapturedPiece(move)
playSound(move.captured ? 'capture' : 'move')
if (newGame.inCheck()) playSound('check')
checkGameEnd(newGame)
......@@ -95,7 +128,7 @@ export function GamePage() {
setBotThinking(false)
botMoveInProgress.current = false
}
}, [botId, playerColor, checkGameEnd])
}, [botId, playerColor, checkGameEnd, addCapturedPiece])
useEffect(() => {
if (isBot && game.turn() !== playerColor && !gameOver && !botMoveInProgress.current) {
......@@ -123,6 +156,7 @@ export function GamePage() {
setLastMove({ from: selectedSquare, to: square })
setSelectedSquare(null)
setLegalMoves([])
addCapturedPiece(move)
playSound(move.captured ? 'capture' : 'move')
if (newGame.inCheck()) playSound('check')
checkGameEnd(newGame)
......@@ -142,7 +176,7 @@ export function GamePage() {
setLegalMoves(moves.map((m) => m.to))
}
}
}, [game, selectedSquare, legalMoves, gameOver, isBot, playerColor, botThinking, getSquareName, getDisplayCoords, checkGameEnd])
}, [game, selectedSquare, legalMoves, gameOver, isBot, playerColor, botThinking, getSquareName, getDisplayCoords, checkGameEnd, addCapturedPiece])
const resetGame = () => {
setGame(new Chess())
......@@ -152,9 +186,69 @@ export function GamePage() {
setGameOver(false)
setResult(null)
setBotThinking(false)
setCapturedByWhite([])
setCapturedByBlack([])
botMoveInProgress.current = false
}
// Scroll move list to end when moves change
useEffect(() => {
if (moveListRef.current) {
moveListRef.current.scrollLeft = moveListRef.current.scrollWidth
}
}, [game])
const getMaterialAdvantage = (pieces: string[]): number => {
return pieces.reduce((sum, p) => sum + (PIECE_VALUES[p] || 0), 0)
}
const renderCapturedPieces = (pieces: string[], color: 'w' | 'b') => {
const sorted = [...pieces].sort((a, b) => (PIECE_VALUES[b] || 0) - (PIECE_VALUES[a] || 0))
const materialValue = getMaterialAdvantage(pieces)
const opponentPieces = color === 'w' ? capturedByBlack : capturedByWhite
const opponentValue = getMaterialAdvantage(opponentPieces)
const advantage = materialValue - opponentValue
return (
<div className="flex items-center gap-0.5 min-h-[20px]">
{sorted.map((piece, i) => (
<span key={i} className="text-sm leading-none opacity-80">
{PIECE_UNICODE[`${color === 'w' ? 'b' : 'w'}${piece}`]}
</span>
))}
{advantage > 0 && (
<span className="text-[10px] text-gold font-bold mr-1">+{advantage}</span>
)}
</div>
)
}
const renderFileLabels = () => {
const files = boardFlipped ? [...FILES].reverse() : FILES
return (
<div className="grid grid-cols-8 w-full" style={{ paddingRight: '16px' }}>
{files.map((f) => (
<div key={f} className="flex items-center justify-center">
<span className="text-[10px] text-text-muted/60 font-mono">{f}</span>
</div>
))}
</div>
)
}
const renderRankLabels = () => {
const ranks = boardFlipped ? [...RANKS].reverse() : RANKS
return (
<div className="grid grid-rows-8 h-full w-4 shrink-0">
{ranks.map((r) => (
<div key={r} className="flex items-center justify-center">
<span className="text-[10px] text-text-muted/60 font-mono">{r}</span>
</div>
))}
</div>
)
}
const renderBoard = () => {
const board = game.board()
const squares = []
......@@ -230,81 +324,178 @@ export function GamePage() {
return squares
}
const renderMoveList = () => {
const history = game.history()
if (history.length === 0) return null
const pairs: { num: number; white: string; black?: string }[] = []
for (let i = 0; i < history.length; i += 2) {
pairs.push({
num: Math.floor(i / 2) + 1,
white: history[i],
black: history[i + 1],
})
}
return (
<div
ref={moveListRef}
className="flex items-center gap-1 overflow-x-auto scrollbar-hide py-2 px-1"
style={{ direction: 'ltr' }}
>
{pairs.map((pair) => (
<div
key={pair.num}
className="flex items-center gap-0.5 shrink-0 px-1.5 py-0.5 rounded bg-surface-2/60 text-[11px]"
>
<span className="text-text-muted/50 font-mono">{pair.num}.</span>
<span className="text-text-primary font-medium">{pair.white}</span>
{pair.black && (
<span className="text-text-secondary">{pair.black}</span>
)}
</div>
))}
</div>
)
}
const opponentName = isBot ? (botNameAr || botId || '') : 'الخصم'
const isPlayerTurn = game.turn() === playerColor
return (
<div className="flex flex-col min-h-dvh bg-background">
<div className="flex items-center justify-between px-4 py-3 bg-surface-1 border-b border-border">
{/* Header */}
<div className="flex items-center justify-between px-4 py-2 bg-surface-1 border-b border-border">
<motion.button
onClick={() => navigate(-1)}
className="text-text-muted text-sm"
className="flex items-center gap-0.5 text-text-muted p-2 -m-2"
whileTap={{ scale: 0.9 }}
>
رجوع
<ChevronRight size={20} className="text-text-muted" />
</motion.button>
<div className="flex items-center gap-2">
{botThinking && (
<motion.div
className="flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-surface-2"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
>
<Cpu size={12} className="text-gold animate-pulse" />
<span className="text-[10px] text-gold">يفكر...</span>
</motion.div>
)}
<span className="text-sm font-semibold text-text-secondary">
{game.turn() === 'w' ? 'دور الابيض' : 'دور الاسود'}
</span>
</div>
{/* Turn indicator - glowing pill */}
<motion.div
className={`flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-bold ${
isPlayerTurn
? 'bg-gold/20 text-gold border border-gold/40'
: 'bg-surface-2 text-text-secondary border border-border'
}`}
animate={isPlayerTurn ? {
boxShadow: ['0 0 4px rgba(212,168,67,0.3)', '0 0 12px rgba(212,168,67,0.5)', '0 0 4px rgba(212,168,67,0.3)'],
} : {
boxShadow: '0 0 0px transparent',
}}
transition={{ duration: 1.5, repeat: Infinity }}
>
{botThinking && <Cpu size={12} className="animate-pulse" />}
{isPlayerTurn ? 'دورك' : 'دور الخصم'}
</motion.div>
<motion.button
onClick={() => setBoardFlipped(!boardFlipped)}
whileTap={{ scale: 0.9 }}
className="p-1.5 rounded-lg bg-surface-2"
className="p-2 -m-2 rounded-lg"
>
<RotateCcw size={16} className="text-text-muted" />
<RotateCcw size={18} className="text-text-muted" />
</motion.button>
</div>
{isBot && (
<div className="flex items-center justify-center gap-2 px-4 py-2 bg-surface-2/50 border-b border-border">
<div className="w-6 h-6 rounded-full overflow-hidden bg-surface-3">
<img
src={getBotPortraitUrl(botId!)}
alt=""
className="w-full h-full object-cover"
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
/>
{/* Main content */}
<div className="flex-1 flex flex-col items-center px-2 pt-2 pb-4">
{/* Opponent panel (top) */}
<div className="w-full max-w-[min(100vw-16px,400px)] flex items-center gap-2 px-1 py-1.5">
<div className="w-8 h-8 rounded-full overflow-hidden bg-surface-3 border border-border flex items-center justify-center shrink-0">
{isBot && botId ? (
<img
src={getBotPortraitUrl(botId)}
alt=""
className="w-full h-full object-cover"
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
/>
) : (
<Cpu size={14} className="text-text-muted" />
)}
</div>
<div className="flex flex-col flex-1 min-w-0">
<div className="flex items-center gap-1.5">
<span className="text-xs font-bold text-text-primary truncate">
{isBot && <span className="text-text-muted font-normal">ضد </span>}
{opponentName}
</span>
{isBot && <Cpu size={10} className="text-text-muted shrink-0" />}
</div>
{renderCapturedPieces(capturedByBlack, 'b')}
</div>
<span className="text-xs text-text-secondary font-semibold">
ضد {botId}
</span>
</div>
)}
<div className="flex-1 flex flex-col items-center justify-center px-2 py-4">
<div className="w-full max-w-[min(100vw-16px,400px)] aspect-square">
<div className="grid grid-cols-8 grid-rows-8 w-full h-full rounded-lg overflow-hidden shadow-xl border border-border">
{renderBoard()}
{/* Board with coordinates */}
<div className="w-full max-w-[min(100vw-16px,400px)]">
<div className="flex">
{/* Rank labels (left side) */}
{renderRankLabels()}
{/* Board */}
<div className="flex-1 aspect-square">
<div className="grid grid-cols-8 grid-rows-8 w-full h-full rounded-lg overflow-hidden shadow-xl border border-border">
{renderBoard()}
</div>
</div>
</div>
{/* File labels (bottom) */}
<div className="mr-4">
{renderFileLabels()}
</div>
</div>
<div className="flex items-center gap-3 mt-4">
<Button variant="coral" size="sm" onClick={() => { setGameOver(true); setResult('استسلام'); playSound('lose') }}>
<Flag size={14} />
{/* Player panel (bottom) */}
<div className="w-full max-w-[min(100vw-16px,400px)] flex items-center gap-2 px-1 py-1.5">
<div className="w-8 h-8 rounded-full bg-surface-3 border border-border flex items-center justify-center shrink-0 relative">
<Crown size={14} className="text-gold" />
<div className="absolute -bottom-0.5 -left-0.5 w-2.5 h-2.5 rounded-full bg-green-500 border-2 border-background" />
</div>
<div className="flex flex-col flex-1 min-w-0">
<span className="text-xs font-bold text-text-primary">أنت</span>
{renderCapturedPieces(capturedByWhite, 'w')}
</div>
</div>
{/* Action buttons */}
<div className="flex items-center gap-3 mt-3">
<Button
variant="coral"
size="md"
onClick={() => { setGameOver(true); setResult('استسلام'); playSound('lose') }}
>
<Flag size={18} />
استسلام
</Button>
<Button variant="ghost" size="sm" onClick={resetGame}>
<RotateCcw size={14} />
<Button variant="ghost" size="md" onClick={resetGame}>
<RotateCcw size={18} />
جديدة
</Button>
</div>
<div className="mt-3 text-xs text-text-muted text-center">
{/* Move list */}
<div className="w-full max-w-[min(100vw-16px,400px)] mt-3">
<div className="text-[10px] text-text-muted/50 px-1 mb-0.5">النقلات</div>
<div className="bg-surface-1 rounded-lg border border-border min-h-[36px] max-h-[56px] overflow-hidden">
{renderMoveList() || (
<div className="flex items-center justify-center h-[36px] text-[11px] text-text-muted/40">
لم تبدأ اللعبة بعد
</div>
)}
</div>
</div>
{/* Move counter */}
<div className="mt-2 text-[11px] text-text-muted text-center">
النقلة {game.moveNumber()} {game.inCheck() ? '- كش!' : ''}
</div>
</div>
{/* Game over modal */}
<AnimatePresence>
{gameOver && result && (
<motion.div
......
import { motion } from 'framer-motion'
import { Play, TrendingUp, Swords, Flame } from 'lucide-react'
import { Play, TrendingUp, Swords, Flame, Grid3X3, Bot, Users, Lightbulb, Crown } from 'lucide-react'
import { useNavigate } from 'react-router-dom'
import { useAuthStore } from '../stores/authStore'
import { PageTransition } from '../components/layout/PageTransition'
import { Card } from '../components/ui/Card'
const dailyTips = [
'تحكم بالمركز في بداية اللعبة - الاحصنة والفيلة تكون اقوى من المركز',
'لا تحرك نفس القطعة مرتين في الافتتاح - طور قطعك بسرعة',
'قم بالتبييت مبكرا لحماية ملكك وتفعيل القلعة',
'الاحصنة قبل الفيلة - طور الاحصنة اولا لانها تحتاج وقت اكثر',
'لا تخرج الملكة مبكرا - قد تتعرض للهجوم وتضيع وقتك',
'ادرس نهايات اللعب - الكثير من المباريات تحسم في النهاية',
'فكر في خطة خصمك قبل ان تلعب - لا تركز فقط على هجومك',
]
export function HomePage() {
const navigate = useNavigate()
const { profile } = useAuthStore()
const todayTip = dailyTips[new Date().getDay()]
return (
<PageTransition className="px-4 py-6 flex flex-col gap-6">
......@@ -35,6 +46,13 @@ export function HomePage() {
className="relative w-full py-8 rounded-3xl bg-gradient-to-bl from-gold via-gold-light to-gold overflow-hidden"
whileTap={{ scale: 0.97 }}
transition={{ type: 'spring', stiffness: 400, damping: 20 }}
animate={{
boxShadow: [
'0 0 20px rgba(212, 175, 55, 0.3)',
'0 0 40px rgba(212, 175, 55, 0.6)',
'0 0 20px rgba(212, 175, 55, 0.3)',
],
}}
>
<motion.div
className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent"
......@@ -55,42 +73,114 @@ export function HomePage() {
{profile && (
<div className="grid grid-cols-3 gap-3">
<StatCard
icon={<Swords size={18} className="text-cyan" />}
icon={<Swords size={20} className="text-cyan" />}
value={profile.total_games_played}
label="مباراة"
delay={0.2}
accentColor="from-cyan/60 to-cyan/20"
/>
<StatCard
icon={<TrendingUp size={18} className="text-gold" />}
icon={<TrendingUp size={20} className="text-gold" />}
value={profile.elo_blitz}
label="تقييم"
delay={0.3}
accentColor="from-gold/60 to-gold/20"
/>
<StatCard
icon={<Flame size={18} className="text-coral" />}
icon={<Flame size={20} className="text-coral" />}
value={profile.win_streak}
label="سلسلة فوز"
delay={0.4}
accentColor="from-coral/60 to-coral/20"
/>
</div>
)}
{/* Quick Actions */}
<div className="grid grid-cols-2 gap-3">
<motion.button
onClick={() => navigate('/bot-select')}
className="flex items-center gap-3 p-4 rounded-2xl bg-surface-1 border border-border text-start"
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.3, type: 'spring', stiffness: 400, damping: 25 }}
whileTap={{ scale: 0.96 }}
>
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-purple/30 to-cyan/20 flex items-center justify-center">
<Bot size={20} className="text-purple" />
</div>
<div>
<p className="text-sm font-bold">العب ضد روبوت</p>
<p className="text-[10px] text-text-muted">تدريب وتحسين</p>
</div>
</motion.button>
<motion.button
onClick={() => navigate('/friends')}
className="flex items-center gap-3 p-4 rounded-2xl bg-surface-1 border border-border text-start"
initial={{ opacity: 0, x: 10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.4, type: 'spring', stiffness: 400, damping: 25 }}
whileTap={{ scale: 0.96 }}
>
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-gold/30 to-coral/20 flex items-center justify-center">
<Users size={20} className="text-gold" />
</div>
<div>
<p className="text-sm font-bold">تحدى صديق</p>
<p className="text-[10px] text-text-muted">ارسل دعوة</p>
</div>
</motion.button>
</div>
{/* Recent Matches */}
<div>
<h3 className="text-base font-bold mb-3 text-text-secondary">اخر المباريات</h3>
<div className="flex flex-col gap-2">
<Card className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-surface-3 flex items-center justify-center">
<span className="text-xs font-bold text-text-muted">?</span>
</div>
<div>
<p className="text-sm font-semibold">لا توجد مباريات بعد</p>
<p className="text-xs text-text-muted">ابدا اللعب الان</p>
</div>
<Card className="flex flex-col items-center justify-center py-8 gap-3">
<motion.div
className="w-14 h-14 rounded-2xl bg-surface-3 flex items-center justify-center"
animate={{ rotate: [0, 5, -5, 0] }}
transition={{ duration: 4, repeat: Infinity, ease: 'easeInOut' }}
>
<Grid3X3 size={28} className="text-text-muted" />
</motion.div>
<div className="text-center">
<p className="text-sm font-semibold">لا توجد مباريات بعد</p>
<p className="text-xs text-text-muted mt-1">العب اول مباراة وابدا رحلتك</p>
</div>
<motion.button
onClick={() => navigate('/play')}
className="mt-2 px-5 py-2 rounded-xl bg-gold/10 border border-gold/30 text-gold text-sm font-bold"
whileTap={{ scale: 0.95 }}
>
ابدا الان
</motion.button>
</Card>
</div>
</div>
{/* Daily Tip */}
<motion.div
className="p-4 rounded-2xl bg-surface-1 border border-border relative overflow-hidden"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5, type: 'spring', stiffness: 400, damping: 25 }}
>
<div className="absolute top-0 right-0 w-20 h-20 bg-gradient-to-bl from-gold/5 to-transparent rounded-bl-full" />
<div className="flex items-start gap-3">
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-gold/20 to-gold/5 flex items-center justify-center flex-shrink-0 mt-0.5">
<Lightbulb size={18} className="text-gold" />
</div>
<div>
<div className="flex items-center gap-2 mb-1">
<h4 className="text-sm font-bold text-gold">نصيحة اليوم</h4>
<Crown size={12} className="text-gold/50" />
</div>
<p className="text-xs text-text-secondary leading-relaxed">{todayTip}</p>
</div>
</div>
</motion.div>
</PageTransition>
)
}
......@@ -100,22 +190,25 @@ function StatCard({
value,
label,
delay,
accentColor,
}: {
icon: React.ReactNode
value: number
label: string
delay: number
accentColor: string
}) {
return (
<motion.div
className="flex flex-col items-center gap-1 p-3 rounded-xl bg-surface-1 border border-border"
className="relative flex flex-col items-center gap-1.5 p-3 rounded-xl bg-surface-1 border border-border overflow-hidden"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay, type: 'spring', stiffness: 400, damping: 25 }}
>
<div className={`absolute top-0 right-0 bottom-0 w-1 bg-gradient-to-b ${accentColor} rounded-l-full`} />
{icon}
<span className="text-lg font-bold">{value}</span>
<span className="text-[10px] text-text-muted">{label}</span>
<span className="text-xl font-bold">{value}</span>
<span className="text-[11px] text-text-muted">{label}</span>
</motion.div>
)
}
import { motion } from 'framer-motion'
import { Lock, Zap, Timer, Clock, Hourglass, Cpu } from 'lucide-react'
import { Lock, Zap, Timer, Clock, Hourglass, Cpu, Crown } from 'lucide-react'
import { useNavigate } from 'react-router-dom'
import { PageTransition } from '../components/layout/PageTransition'
import { GAMES, TIME_CONTROLS } from '../lib/constants'
......@@ -13,16 +13,44 @@ const CATEGORIES = [
{ key: 'classical', label: 'كلاسيكي', icon: Hourglass },
] as const
const GAME_ICONS: Record<string, React.ReactNode> = {
backgammon: <span className="text-2xl font-bold text-gold">&#x2680;</span>,
dominoes: <span className="text-2xl font-bold text-gold">&#x1F0A1;</span>,
ludo: <span className="text-2xl font-bold text-gold">&#x2684;</span>,
trivia: <span className="text-2xl font-bold text-gold">?</span>,
}
export function PlayPage() {
const navigate = useNavigate()
const [selectedTC, setSelectedTC] = useState<string>('blitz_5_0')
const chessGame = GAMES.find((g) => g.key === 'chess')!
const otherGames = GAMES.filter((g) => g.key !== 'chess')
return (
<PageTransition className="px-4 py-6 flex flex-col gap-6">
<h1 className="text-xl font-bold">اختر اللعبة</h1>
{/* Chess Hero Card - full width */}
<motion.div
className="relative rounded-2xl overflow-hidden border-2 border-gold/40 bg-gradient-to-br from-surface-2 via-surface-1 to-gold/5 p-5 flex items-center gap-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ type: 'spring', stiffness: 400, damping: 25 }}
whileTap={{ scale: 0.97 }}
>
<div className="w-14 h-14 rounded-xl bg-gold/10 border border-gold/30 flex items-center justify-center">
<Crown size={28} className="text-gold" />
</div>
<div className="flex-1">
<span className="text-base font-bold">{chessGame.nameAr}</span>
<p className="text-xs text-text-secondary mt-1">العب شطرنج اونلاين ضد لاعبين حقيقيين</p>
</div>
</motion.div>
{/* Other Games 2x2 Grid */}
<div className="grid grid-cols-2 gap-3">
{GAMES.map((game, i) => (
{otherGames.map((game, i) => (
<motion.div
key={game.key}
className={`relative rounded-2xl overflow-hidden border-2 ${
......@@ -32,17 +60,11 @@ export function PlayPage() {
} p-4 flex flex-col items-center gap-2`}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.08, type: 'spring', stiffness: 400, damping: 25 }}
transition={{ delay: 0.08 + i * 0.08, type: 'spring', stiffness: 400, damping: 25 }}
whileTap={game.available ? { scale: 0.95 } : undefined}
>
<div className="w-12 h-12 rounded-xl bg-surface-3 flex items-center justify-center">
<span className="text-2xl font-bold text-gold">
{game.key === 'chess' && '♚'}
{game.key === 'backgammon' && '⚀'}
{game.key === 'dominoes' && '’'}
{game.key === 'ludo' && '⚄'}
{game.key === 'trivia' && '?'}
</span>
{GAME_ICONS[game.key] || null}
</div>
<span className="text-sm font-bold">{game.nameAr}</span>
{!game.available && (
......@@ -55,9 +77,10 @@ export function PlayPage() {
))}
</div>
{/* Time Control Section */}
<div>
<h2 className="text-base font-bold mb-3">نظام الوقت</h2>
<div className="flex gap-2 mb-3">
<div className="flex gap-3 mb-3">
{CATEGORIES.map((cat) => {
const isActive = Object.entries(TIME_CONTROLS).find(
([k]) => k === selectedTC
......@@ -66,7 +89,7 @@ export function PlayPage() {
return (
<motion.button
key={cat.key}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-semibold border ${
className={`flex items-center gap-1.5 px-4 py-2 rounded-full text-xs font-semibold border ${
isActive
? 'bg-gold/10 border-gold/40 text-gold'
: 'bg-surface-2 border-border text-text-muted'
......@@ -109,18 +132,17 @@ export function PlayPage() {
</div>
</div>
<Button onClick={() => navigate('/matchmaking')} className="w-full" size="lg">
البحث عن خصم
</Button>
{/* Action Buttons */}
<div className="flex flex-col gap-3 mt-2">
<Button onClick={() => navigate('/matchmaking')} className="w-full" size="lg">
البحث عن خصم
</Button>
<Button onClick={() => navigate('/bot-select')} variant="ghost" className="w-full" size="md">
<Cpu size={16} />
العب ضد الروبوت
</Button>
<Button onClick={() => navigate('/game')} variant="ghost" className="w-full" size="md">
لعب محلي (تجربة)
</Button>
<Button onClick={() => navigate('/bot-select')} variant="ghost" className="w-full" size="md">
<Cpu size={16} className="text-gold" />
العب ضد الروبوت
</Button>
</div>
</PageTransition>
)
}
import { motion } from 'framer-motion'
import { Settings, TrendingUp, Target, Flame, Trophy } from 'lucide-react'
import { Settings, TrendingUp, Target, Flame, Trophy, LogOut, Lock } from 'lucide-react'
import { useNavigate } from 'react-router-dom'
import { useAuthStore } from '../stores/authStore'
import { PageTransition } from '../components/layout/PageTransition'
import { Card } from '../components/ui/Card'
import { Button } from '../components/ui/Button'
import { supabase } from '../lib/supabase'
function SectionDivider() {
return (
<div className="w-full h-px bg-gradient-to-l from-transparent via-gold/20 to-transparent" />
)
}
export function ProfilePage() {
const { profile } = useAuthStore()
const navigate = useNavigate()
......@@ -44,29 +49,34 @@ export function ProfilePage() {
<motion.button
whileTap={{ scale: 0.9 }}
onClick={() => navigate('/settings')}
className="p-2 rounded-lg bg-surface-2"
className="p-2 rounded-lg bg-surface-2 mr-auto ml-0"
>
<Settings size={18} className="text-text-muted" />
</motion.button>
</div>
<Card className="flex items-center gap-3">
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-1.5">
<span className="text-xs text-text-muted">المستوى {profile.level}</span>
<span className="text-xs text-gold font-semibold">{profile.xp} XP</span>
</div>
<div className="w-full h-2 rounded-full bg-surface-3 overflow-hidden">
<motion.div
className="h-full rounded-full bg-gradient-to-l from-gold to-gold-light"
initial={{ width: 0 }}
animate={{ width: `${Math.min((profile.xp % 500) / 5, 100)}%` }}
transition={{ duration: 1, ease: 'easeOut' }}
/>
</div>
<Card className="p-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-base font-bold text-text-primary">
المستوى {profile.level}
</h2>
<span className="text-sm text-gold font-semibold">{profile.xp} XP</span>
</div>
<div className="w-full h-2.5 rounded-full bg-surface-3 overflow-hidden">
<motion.div
className="h-full rounded-full bg-gradient-to-l from-gold to-gold-light"
initial={{ width: 0 }}
animate={{ width: `${Math.min((profile.xp % 500) / 5, 100)}%` }}
transition={{ duration: 1, ease: 'easeOut' }}
/>
</div>
<p className="text-[11px] text-text-muted mt-2">
{500 - (profile.xp % 500)} XP للمستوى التالي
</p>
</Card>
<SectionDivider />
<div>
<h2 className="text-sm font-bold text-text-secondary mb-2">التقييمات</h2>
<div className="grid grid-cols-2 gap-2">
......@@ -95,6 +105,8 @@ export function ProfilePage() {
</div>
</div>
<SectionDivider />
<div>
<h2 className="text-sm font-bold text-text-secondary mb-2">الاحصائيات</h2>
<div className="grid grid-cols-4 gap-2">
......@@ -105,9 +117,43 @@ export function ProfilePage() {
</div>
</div>
<Button variant="ghost" onClick={handleLogout} className="mt-4">
تسجيل الخروج
</Button>
<SectionDivider />
<div>
<h2 className="text-sm font-bold text-text-secondary mb-2">الانجازات</h2>
<div className="grid grid-cols-3 gap-2">
{[
{ label: 'المحارب' },
{ label: 'البطل' },
{ label: 'الاسطورة' },
].map((badge, i) => (
<motion.div
key={badge.label}
className="flex flex-col items-center gap-2 p-3 rounded-xl bg-surface-1 border border-border/50 opacity-60"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 0.6, scale: 1 }}
transition={{ delay: 0.2 + i * 0.08 }}
>
<div className="w-10 h-10 rounded-full bg-surface-3 border border-border flex items-center justify-center">
<Lock size={14} className="text-text-muted" />
</div>
<span className="text-[10px] font-semibold text-text-muted">{badge.label}</span>
<span className="text-[9px] text-gold/60">قريبا</span>
</motion.div>
))}
</div>
</div>
<div className="mt-auto pt-6 flex justify-center">
<motion.button
whileTap={{ scale: 0.95 }}
onClick={handleLogout}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs text-coral/80 hover:text-coral transition-colors"
>
<LogOut size={13} />
<span>تسجيل الخروج</span>
</motion.button>
</div>
</PageTransition>
)
}
......
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