Commit dcec48a9 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat: Phase 1 UI/UX overhaul — Games Hub + visual polish

- Games Hub (/games) with game cards grid, replaces separate nav entries
- Navigation: single "العاب" entry in both desktop sidebar and mobile bottom nav
- Glassmorphism: frosted glass header, desktop nav, and mobile bottom nav
- Button ripple effect on all .btn clicks
- Card hover/tap lift animation
- Gradient accent borders on active game cards
- Page enter animation (fade + slide)
- Skeleton loading state CSS utilities
- Streak calendar strip on home page (7-day visual)
- Ludo piece bounce on landing animation
- Chess legal move pulsing indicator
- Tablet breakpoint (768-1023px)
- Breadcrumb back-links on game lobbies
- Optimistic UI helper (App.optimistic)
- Number animation helper (App.animateNumber)
- Lazy image placeholder CSS
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent c447dcd8
...@@ -9,6 +9,8 @@ if ($route === '' || $route === 'home') { ...@@ -9,6 +9,8 @@ if ($route === '' || $route === 'home') {
require 'pages/login.php'; require 'pages/login.php';
} elseif ($route === 'register') { } elseif ($route === 'register') {
require 'pages/register.php'; require 'pages/register.php';
} elseif ($route === 'games') {
require 'pages/games.php';
} elseif ($route === 'play') { } elseif ($route === 'play') {
require 'pages/play.php'; require 'pages/play.php';
} elseif ($route === 'game') { } elseif ($route === 'game') {
......
<?php $pageTitle = 'EL3AB - العاب'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<div class="space-y-6" id="games-content">
<h2 class="page-title">العاب</h2>
<div class="games-grid">
<!-- Chess -->
<div class="game-card card-accent" data-game="chess">
<div class="game-card-cover game-card-cover--chess">
<svg class="game-card-icon"><use href="/public/icons/sprite.svg#icon-play"></use></svg>
</div>
<div class="game-card-info">
<div class="game-card-meta">
<span class="game-card-name">شطرنج</span>
<span class="online-badge" id="online-chess"><span class="online-dot"></span> --</span>
</div>
<a href="/play" class="btn btn-cyan btn-sm game-card-play">العب</a>
</div>
</div>
<!-- Ludo -->
<div class="game-card card-accent" data-game="ludo">
<div class="game-card-cover game-card-cover--ludo">
<svg class="game-card-icon"><use href="/public/icons/sprite.svg#icon-ludo"></use></svg>
</div>
<div class="game-card-info">
<div class="game-card-meta">
<span class="game-card-name">لودو</span>
<span class="online-badge" id="online-ludo"><span class="online-dot"></span> --</span>
</div>
<a href="/ludo" class="btn btn-cyan btn-sm game-card-play">العب</a>
</div>
</div>
<!-- Coming Soon: Dominoes -->
<div class="game-card game-card--soon">
<div class="game-card-cover game-card-cover--domino">
<svg class="game-card-icon"><use href="/public/icons/sprite.svg#icon-puzzle"></use></svg>
<span class="game-card-badge">قريبا</span>
</div>
<div class="game-card-info">
<div class="game-card-meta">
<span class="game-card-name">دومينو</span>
</div>
</div>
</div>
<!-- Coming Soon: Backgammon -->
<div class="game-card game-card--soon">
<div class="game-card-cover game-card-cover--backgammon">
<svg class="game-card-icon"><use href="/public/icons/sprite.svg#icon-puzzle"></use></svg>
<span class="game-card-badge">قريبا</span>
</div>
<div class="game-card-info">
<div class="game-card-meta">
<span class="game-card-name">طاولة</span>
</div>
</div>
</div>
<!-- Coming Soon: Trix -->
<div class="game-card game-card--soon">
<div class="game-card-cover game-card-cover--trix">
<svg class="game-card-icon"><use href="/public/icons/sprite.svg#icon-puzzle"></use></svg>
<span class="game-card-badge">قريبا</span>
</div>
<div class="game-card-info">
<div class="game-card-meta">
<span class="game-card-name">تركس</span>
</div>
</div>
</div>
<!-- Coming Soon: Baloot -->
<div class="game-card game-card--soon">
<div class="game-card-cover game-card-cover--baloot">
<svg class="game-card-icon"><use href="/public/icons/sprite.svg#icon-puzzle"></use></svg>
<span class="game-card-badge">قريبا</span>
</div>
<div class="game-card-info">
<div class="game-card-meta">
<span class="game-card-name">بلوت</span>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
document.querySelectorAll('.game-card[data-game]').forEach(card => {
card.addEventListener('click', (e) => {
if (e.target.closest('.game-card-play')) return;
const game = card.dataset.game;
if (game === 'chess') window.location.href = '/play';
else if (game === 'ludo') window.location.href = '/ludo';
});
});
});
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
...@@ -10,12 +10,12 @@ ...@@ -10,12 +10,12 @@
</div> </div>
<!-- Play Button --> <!-- Play Button -->
<a href="/play" class="btn btn-gold btn-block btn-lg"> <a href="/games" class="btn btn-gold btn-block btn-lg">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-play"></use></svg> <svg class="icon"><use href="/public/icons/sprite.svg#icon-games"></use></svg>
العب الان العب الان
</a> </a>
<!-- Daily Reward --> <!-- Daily Reward + Streak Calendar -->
<div class="card"> <div class="card">
<div class="card-body" style="display:flex;align-items:center;justify-content:space-between;"> <div class="card-body" style="display:flex;align-items:center;justify-content:space-between;">
<div style="display:flex;align-items:center;gap:12px;"> <div style="display:flex;align-items:center;gap:12px;">
...@@ -27,6 +27,15 @@ ...@@ -27,6 +27,15 @@
</div> </div>
<button class="btn btn-cyan btn-sm" id="home-claim-btn">اجمع</button> <button class="btn btn-cyan btn-sm" id="home-claim-btn">اجمع</button>
</div> </div>
<div class="streak-strip" id="streak-strip">
<div class="streak-day" data-day="1"><span class="streak-day-num">1</span></div>
<div class="streak-day" data-day="2"><span class="streak-day-num">2</span></div>
<div class="streak-day" data-day="3"><span class="streak-day-num">3</span></div>
<div class="streak-day" data-day="4"><span class="streak-day-num">4</span></div>
<div class="streak-day" data-day="5"><span class="streak-day-num">5</span></div>
<div class="streak-day" data-day="6"><span class="streak-day-num">6</span></div>
<div class="streak-day" data-day="7"><span class="streak-day-num">7</span></div>
</div>
</div> </div>
<!-- Recent Games --> <!-- Recent Games -->
...@@ -51,10 +60,24 @@ document.addEventListener('DOMContentLoaded', async () => { ...@@ -51,10 +60,24 @@ document.addEventListener('DOMContentLoaded', async () => {
const p = data.profile; const p = data.profile;
document.getElementById('home-welcome').textContent = 'اهلا يا ' + (p.display_name || p.username); document.getElementById('home-welcome').textContent = 'اهلا يا ' + (p.display_name || p.username);
document.getElementById('home-subtitle').textContent = 'المستوى ' + (p.level || 1) + ' • ' + (p.elo_blitz || 1200) + ' بليتز'; document.getElementById('home-subtitle').textContent = 'المستوى ' + (p.level || 1) + ' • ' + (p.elo_blitz || 1200) + ' بليتز';
document.getElementById('home-streak').textContent = 'اليوم ' + (p.daily_streak || 0); const streak = p.daily_streak || 0;
document.getElementById('home-streak-reward').textContent = '+' + (50 + (p.daily_streak || 0) * 10) + ' عملة'; document.getElementById('home-streak').textContent = 'اليوم ' + streak;
document.getElementById('home-streak-reward').textContent = '+' + (50 + streak * 10) + ' عملة';
// Update streak calendar strip
const claimedToday = p.last_daily_claim === new Date().toISOString().slice(0, 10);
const days = document.querySelectorAll('.streak-day');
days.forEach((day, i) => {
const dayNum = i + 1;
const streakDay = ((streak - 1) % 7) + 1;
if (dayNum < streakDay || (dayNum === streakDay && claimedToday)) {
day.classList.add('streak-day--claimed');
} else if (dayNum === streakDay + (claimedToday ? 0 : 1) || (streak === 0 && dayNum === 1)) {
day.classList.add('streak-day--current');
}
});
if (p.last_daily_claim === new Date().toISOString().slice(0, 10)) { if (claimedToday) {
document.getElementById('home-claim-btn').textContent = 'تم'; document.getElementById('home-claim-btn').textContent = 'تم';
document.getElementById('home-claim-btn').disabled = true; document.getElementById('home-claim-btn').disabled = true;
document.getElementById('home-claim-btn').classList.add('btn-ghost'); document.getElementById('home-claim-btn').classList.add('btn-ghost');
......
...@@ -3,6 +3,11 @@ ...@@ -3,6 +3,11 @@
<div class="space-y-6"> <div class="space-y-6">
<a href="/games" class="breadcrumb">
<svg class="icon" style="width:14px;height:14px;"><use href="/public/icons/sprite.svg#icon-arrow-right"></use></svg>
العاب
</a>
<div class="text-center"> <div class="text-center">
<h2 style="font-size:22px;font-weight:700;">لودو</h2> <h2 style="font-size:22px;font-weight:700;">لودو</h2>
<p class="text-muted text-sm">اختر نوع اللعب</p> <p class="text-muted text-sm">اختر نوع اللعب</p>
......
...@@ -3,6 +3,11 @@ ...@@ -3,6 +3,11 @@
<div class="space-y-6"> <div class="space-y-6">
<a href="/games" class="breadcrumb">
<svg class="icon" style="width:14px;height:14px;"><use href="/public/icons/sprite.svg#icon-arrow-right"></use></svg>
العاب
</a>
<div class="text-center"> <div class="text-center">
<h2 style="font-size:22px;font-weight:700;">العب شطرنج</h2> <h2 style="font-size:22px;font-weight:700;">العب شطرنج</h2>
<p class="text-muted text-sm">اختر نوع المباراة</p> <p class="text-muted text-sm">اختر نوع المباراة</p>
......
...@@ -225,9 +225,10 @@ img { ...@@ -225,9 +225,10 @@ img {
top: 0; top: 0;
z-index: 40; z-index: 40;
height: var(--header-h); height: var(--header-h);
background: var(--bg-1); background: rgba(10, 21, 37, 0.75);
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
backdrop-filter: blur(8px); backdrop-filter: blur(12px) saturate(1.2);
-webkit-backdrop-filter: blur(12px) saturate(1.2);
padding-top: env(safe-area-inset-top); padding-top: env(safe-area-inset-top);
} }
...@@ -312,8 +313,10 @@ img { ...@@ -312,8 +313,10 @@ img {
align-items: center; align-items: center;
padding: 16px 0; padding: 16px 0;
gap: 4px; gap: 4px;
background: var(--bg-1); background: rgba(10, 21, 37, 0.8);
border-right: 1px solid var(--border); border-right: 1px solid var(--border);
backdrop-filter: blur(12px) saturate(1.2);
-webkit-backdrop-filter: blur(12px) saturate(1.2);
z-index: 50; z-index: 50;
overflow-y: auto; overflow-y: auto;
} }
...@@ -355,8 +358,10 @@ img { ...@@ -355,8 +358,10 @@ img {
right: 0; right: 0;
z-index: 40; z-index: 40;
height: var(--nav-bottom-h); height: var(--nav-bottom-h);
background: var(--bg-1); background: rgba(10, 21, 37, 0.8);
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
backdrop-filter: blur(12px) saturate(1.2);
-webkit-backdrop-filter: blur(12px) saturate(1.2);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-around; justify-content: space-around;
...@@ -383,6 +388,20 @@ img { ...@@ -383,6 +388,20 @@ img {
font-weight: 500; font-weight: 500;
} }
/* Tablet responsive */
@media (min-width: 768px) and (max-width: 1023px) {
.main-inner {
max-width: 720px;
padding: 32px 24px;
}
.games-grid {
grid-template-columns: repeat(3, 1fr);
}
.nav-bottom-label {
font-size: 11px;
}
}
/* Desktop responsive */ /* Desktop responsive */
@media (min-width: 1024px) { @media (min-width: 1024px) {
.nav-desktop { display: flex; } .nav-desktop { display: flex; }
...@@ -433,6 +452,39 @@ img { ...@@ -433,6 +452,39 @@ img {
.btn-lg { min-height: 56px; font-size: 16px; font-weight: 700; border-radius: var(--radius-lg); } .btn-lg { min-height: 56px; font-size: 16px; font-weight: 700; border-radius: var(--radius-lg); }
.btn-sm { min-height: 36px; padding: 8px 16px; font-size: 12px; } .btn-sm { min-height: 36px; padding: 8px 16px; font-size: 12px; }
/* Button ripple */
.btn {
position: relative;
overflow: hidden;
}
.btn .ripple {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.25);
transform: scale(0);
animation: ripple-expand 0.5s ease-out forwards;
pointer-events: none;
}
@keyframes ripple-expand {
to { transform: scale(4); opacity: 0; }
}
/* Card hover/tap lift */
.card {
transition: transform 0.2s var(--ease), box-shadow 0.2s var(--ease);
}
.card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.card:active {
transform: translateY(-1px);
}
/* Inputs */ /* Inputs */
.input-group { margin-bottom: 16px; } .input-group { margin-bottom: 16px; }
...@@ -667,6 +719,331 @@ img { ...@@ -667,6 +719,331 @@ img {
.text-xs { font-size: 11px; } .text-xs { font-size: 11px; }
.fw-bold { font-weight: 700; } .fw-bold { font-weight: 700; }
/* ===== Lazy Image Placeholder ===== */
.img-lazy {
position: relative;
background: var(--bg-3);
overflow: hidden;
}
.img-lazy::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.04), transparent);
animation: skeleton-shimmer 1.5s infinite;
}
.img-lazy img {
opacity: 0;
transition: opacity 0.3s;
}
.img-lazy--loaded::before {
display: none;
}
.img-lazy--loaded img {
opacity: 1;
}
/* ===== Streak Calendar Strip ===== */
.streak-strip {
display: flex;
align-items: center;
justify-content: space-around;
padding: 12px 16px;
border-top: 1px solid var(--border);
}
.streak-day {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid var(--border-strong);
transition: all 0.3s var(--ease);
}
.streak-day--claimed {
background: var(--gold);
border-color: var(--gold);
color: var(--text-inverse);
}
.streak-day--claimed .streak-day-num {
display: none;
}
.streak-day--claimed::after {
content: '✓';
font-size: 14px;
font-weight: 700;
}
.streak-day--current {
border-color: var(--cyan);
animation: streak-pulse 1.5s ease-in-out infinite;
}
@keyframes streak-pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(21, 215, 255, 0.3); }
50% { box-shadow: 0 0 0 4px rgba(21, 215, 255, 0.1); }
}
.streak-day-num {
font-size: 11px;
font-weight: 600;
color: var(--text-3);
}
.streak-day--claimed .streak-day-num,
.streak-day--current .streak-day-num {
color: var(--text-1);
}
/* ===== Skeleton Loading ===== */
.skeleton {
background: var(--bg-3);
border-radius: var(--radius-sm);
position: relative;
overflow: hidden;
}
.skeleton::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.04), transparent);
animation: skeleton-shimmer 1.5s infinite;
}
@keyframes skeleton-shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
.skeleton-text {
height: 14px;
width: 60%;
margin-bottom: 8px;
}
.skeleton-text--short { width: 40%; }
.skeleton-text--full { width: 100%; }
.skeleton-avatar {
width: 40px;
height: 40px;
border-radius: var(--radius-full);
}
.skeleton-card {
height: 80px;
border-radius: var(--radius-lg);
}
/* ===== Page Transitions ===== */
.main-inner {
animation: page-enter 0.3s var(--ease) both;
}
@keyframes page-enter {
from {
opacity: 0;
transform: translateX(12px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.page-exit {
animation: page-exit 0.2s var(--ease) both;
}
@keyframes page-exit {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(-12px);
}
}
/* ===== Gradient Accent Border ===== */
.card-accent {
position: relative;
border: none;
}
.card-accent::before {
content: '';
position: absolute;
inset: 0;
border-radius: var(--radius-lg);
padding: 2px;
background: linear-gradient(135deg, var(--gold), var(--cyan));
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
pointer-events: none;
}
/* ===== Breadcrumb ===== */
.breadcrumb {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--text-3);
text-decoration: none;
transition: color 0.2s;
}
.breadcrumb:hover {
color: var(--cyan);
}
/* ===== Games Hub ===== */
.games-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.game-card {
border-radius: var(--radius-lg);
overflow: hidden;
background: var(--bg-2);
border: 1px solid var(--border);
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.game-card:hover,
.game-card:active {
transform: translateY(-3px);
box-shadow: var(--shadow-lg);
}
.game-card--soon {
opacity: 0.6;
pointer-events: none;
}
.game-card-cover {
position: relative;
height: 100px;
display: flex;
align-items: center;
justify-content: center;
}
.game-card-cover--chess {
background: linear-gradient(135deg, #1a2a4a, #2a4a6a);
}
.game-card-cover--ludo {
background: linear-gradient(135deg, #2a1a3a, #4a2a5a);
}
.game-card-cover--domino {
background: linear-gradient(135deg, #1a3a2a, #2a5a4a);
}
.game-card-cover--backgammon {
background: linear-gradient(135deg, #3a2a1a, #5a4a2a);
}
.game-card-cover--trix {
background: linear-gradient(135deg, #1a1a3a, #3a2a5a);
}
.game-card-cover--baloot {
background: linear-gradient(135deg, #3a1a1a, #5a2a2a);
}
.game-card-icon {
width: 40px;
height: 40px;
color: rgba(255, 255, 255, 0.9);
}
.game-card-badge {
position: absolute;
top: 8px;
left: 8px;
background: var(--gold);
color: var(--text-inverse);
font-size: 11px;
font-weight: 700;
padding: 2px 8px;
border-radius: var(--radius-full);
}
.game-card-info {
padding: 12px;
display: flex;
align-items: center;
justify-content: space-between;
}
.game-card-meta {
display: flex;
flex-direction: column;
gap: 4px;
}
.game-card-name {
font-size: 14px;
font-weight: 700;
color: var(--text-1);
}
.online-badge {
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
color: var(--text-3);
}
.online-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--online);
}
.game-card-play {
white-space: nowrap;
}
@media (max-width: 767px) {
.games-grid {
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.game-card-cover {
height: 80px;
}
}
@media (min-width: 1024px) {
.games-grid {
grid-template-columns: repeat(3, 1fr);
}
.game-card-cover {
height: 120px;
}
}
/* Reduced motion */ /* Reduced motion */
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
*, *::before, *::after { *, *::before, *::after {
......
...@@ -148,6 +148,7 @@ ...@@ -148,6 +148,7 @@
height: 30%; height: 30%;
border-radius: 50%; border-radius: 50%;
background: var(--board-legal); background: var(--board-legal);
animation: legal-pulse 1.2s ease-in-out infinite;
} }
.square.legal-capture::after { .square.legal-capture::after {
content: ''; content: '';
...@@ -157,6 +158,12 @@ ...@@ -157,6 +158,12 @@
border-radius: 50%; border-radius: 50%;
border: 3.5px solid var(--board-legal); border: 3.5px solid var(--board-legal);
background: transparent; background: transparent;
animation: legal-pulse 1.2s ease-in-out infinite;
}
@keyframes legal-pulse {
0%, 100% { opacity: 0.7; }
50% { opacity: 1; }
} }
.square.last-move { background: var(--board-last-move) !important; } .square.last-move { background: var(--board-last-move) !important; }
.square.in-check { background: var(--board-check) !important; box-shadow: inset 0 0 8px var(--board-check); } .square.in-check { background: var(--board-check) !important; box-shadow: inset 0 0 8px var(--board-check); }
......
...@@ -602,3 +602,15 @@ ...@@ -602,3 +602,15 @@
.ludo-players-row { max-width: 100%; } .ludo-players-row { max-width: 100%; }
} }
/* Piece land bounce */
.ludo-piece--landed {
animation: piece-land 0.4s var(--ease, cubic-bezier(0.4, 0, 0.2, 1)) both;
}
@keyframes piece-land {
0% { transform: translate(-50%, -50%) scale(1, 1); }
30% { transform: translate(-50%, -50%) scale(1.2, 0.8); }
60% { transform: translate(-50%, -50%) scale(0.9, 1.1); }
100% { transform: translate(-50%, -50%) scale(1, 1); }
}
...@@ -188,4 +188,11 @@ ...@@ -188,4 +188,11 @@
<path d="M7 11V7a5 5 0 0110 0v4"/> <path d="M7 11V7a5 5 0 0110 0v4"/>
</symbol> </symbol>
<symbol id="icon-games" viewBox="0 0 24 24">
<path d="M6 12h4M8 10v4"/>
<circle cx="15" cy="11" r="1" fill="currentColor" stroke="none"/>
<circle cx="17" cy="13" r="1" fill="currentColor" stroke="none"/>
<path d="M2 10a2 2 0 012-2h3.5a2 2 0 011.4.6L10 9.6a2 2 0 001.4.6h1.2a2 2 0 001.4-.6l1.1-1a2 2 0 011.4-.6H20a2 2 0 012 2v4a4 4 0 01-4 4H6a4 4 0 01-4-4v-4z"/>
</symbol>
</svg> </svg>
...@@ -68,6 +68,36 @@ const App = { ...@@ -68,6 +68,36 @@ const App = {
if (coins) coins.textContent = (p.coins || 0).toLocaleString(); if (coins) coins.textContent = (p.coins || 0).toLocaleString();
if (gems) gems.textContent = (p.gems || 0).toLocaleString(); if (gems) gems.textContent = (p.gems || 0).toLocaleString();
} }
},
async optimistic(updateFn, revertFn, promise) {
updateFn();
try {
const res = await promise;
if (!res || res.error) {
revertFn();
if (res && res.error) this.toast(res.error, 'error');
}
return res;
} catch (e) {
revertFn();
return null;
}
},
animateNumber(el, target, duration) {
duration = duration || 600;
const start = parseInt(el.textContent.replace(/[^\d]/g, '')) || 0;
const diff = target - start;
if (diff === 0) return;
const startTime = performance.now();
const step = (now) => {
const t = Math.min((now - startTime) / duration, 1);
const ease = 1 - Math.pow(1 - t, 3);
el.textContent = Math.round(start + diff * ease).toLocaleString();
if (t < 1) requestAnimationFrame(step);
};
requestAnimationFrame(step);
} }
}; };
...@@ -75,4 +105,19 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -75,4 +105,19 @@ document.addEventListener('DOMContentLoaded', () => {
if (App.isLoggedIn()) { if (App.isLoggedIn()) {
App.loadProfile(); App.loadProfile();
} }
// Button ripple effect
document.addEventListener('click', (e) => {
const btn = e.target.closest('.btn');
if (!btn) return;
const rect = btn.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const ripple = document.createElement('span');
ripple.className = 'ripple';
ripple.style.width = ripple.style.height = size + 'px';
ripple.style.left = (e.clientX - rect.left - size / 2) + 'px';
ripple.style.top = (e.clientY - rect.top - size / 2) + 'px';
btn.appendChild(ripple);
setTimeout(() => ripple.remove(), 500);
});
}); });
...@@ -140,6 +140,12 @@ var LudoUI = (function() { ...@@ -140,6 +140,12 @@ var LudoUI = (function() {
el.style.top = (coords[1] * C.STEP_LENGTH + halfStep) + '%'; el.style.top = (coords[1] * C.STEP_LENGTH + halfStep) + '%';
el.style.left = (coords[0] * C.STEP_LENGTH + halfStep) + '%'; el.style.left = (coords[0] * C.STEP_LENGTH + halfStep) + '%';
// Bounce on landing
el.classList.remove('ludo-piece--landed');
void el.offsetWidth;
el.classList.add('ludo-piece--landed');
setTimeout(function() { el.classList.remove('ludo-piece--landed'); }, 400);
// Mark as home if reached final // Mark as home if reached final
if (position === C.HOME_POSITIONS[player]) { if (position === C.HOME_POSITIONS[player]) {
el.classList.add('ludo-piece--home'); el.classList.add('ludo-piece--home');
......
...@@ -3,9 +3,9 @@ ...@@ -3,9 +3,9 @@
$currentRoute = $_GET['route'] ?? ''; $currentRoute = $_GET['route'] ?? '';
$bottomItems = [ $bottomItems = [
['/', 'icon-home', 'الرئيسية'], ['/', 'icon-home', 'الرئيسية'],
['/play', 'icon-play', 'شطرنج'], ['/games', 'icon-games', 'العاب'],
['/ludo', 'icon-ludo', 'لودو'],
['/tournaments', 'icon-trophy', 'بطولات'], ['/tournaments', 'icon-trophy', 'بطولات'],
['/friends', 'icon-friends', 'اجتماعي'],
['/profile', 'icon-profile', 'حسابي'], ['/profile', 'icon-profile', 'حسابي'],
]; ];
foreach ($bottomItems as $item): foreach ($bottomItems as $item):
......
...@@ -4,8 +4,7 @@ ...@@ -4,8 +4,7 @@
$currentRoute = $_GET['route'] ?? ''; $currentRoute = $_GET['route'] ?? '';
$navItems = [ $navItems = [
['/', 'icon-home', 'الرئيسية'], ['/', 'icon-home', 'الرئيسية'],
['/play', 'icon-play', 'شطرنج'], ['/games', 'icon-games', 'العاب'],
['/ludo', 'icon-ludo', 'لودو'],
['/puzzles', 'icon-puzzle', 'تمارين'], ['/puzzles', 'icon-puzzle', 'تمارين'],
['/tournaments', 'icon-trophy', 'بطولات'], ['/tournaments', 'icon-trophy', 'بطولات'],
['/leaderboard', 'icon-leaderboard', 'متصدرون'], ['/leaderboard', 'icon-leaderboard', 'متصدرون'],
......
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