Commit 431518eb authored by Mahmoud Aglan's avatar Mahmoud Aglan

kokowawas

parent 6d88756f
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
require_once __DIR__ . '/../includes/supabase.php';
require_once __DIR__ . '/../includes/auth.php';
$token = requireAuth();
$userId = getUserId($token);
$method = $_SERVER['REQUEST_METHOD'];
$sdb = supabaseService();
if ($method === 'GET') {
$action = $_GET['action'] ?? 'history';
if ($action === 'history') {
$friendId = $_GET['friend_id'] ?? '';
if (!$friendId) jsonError('friend_id required');
// Find the friendship between these two users
$friendship = findFriendship($sdb, $userId, $friendId);
if (!$friendship) jsonError('Not friends');
$before = $_GET['before'] ?? null;
$limit = min(intval($_GET['limit'] ?? 50), 100);
$params = [
'friendship_id' => 'eq.' . $friendship['id'],
'select' => 'id,sender_id,content,message_type,metadata,created_at,read_at',
'order' => 'created_at.desc',
'limit' => $limit
];
if ($before) {
$params['created_at'] = 'lt.' . $before;
}
$messages = $sdb->get('friend_messages', $params);
if (!is_array($messages) || isset($messages['error'])) {
jsonResponse(['messages' => []]);
}
// Mark unread messages from friend as read
$sdb->update('friend_messages', ['read_at' => gmdate('c')], [
'friendship_id' => 'eq.' . $friendship['id'],
'sender_id' => 'eq.' . $friendId,
'read_at' => 'is.null'
]);
jsonResponse(['messages' => array_reverse($messages), 'friendship_id' => $friendship['id']]);
}
if ($action === 'unread') {
// Get unread counts per friendship
$friendships = $sdb->get('friendships', [
'or' => "(requester_id.eq.{$userId},addressee_id.eq.{$userId})",
'status' => 'eq.accepted',
'select' => 'id,requester_id,addressee_id'
]);
if (!is_array($friendships) || isset($friendships['error']) || empty($friendships)) {
jsonResponse(['unread' => []]);
}
$unreadCounts = [];
foreach ($friendships as $f) {
$friendId = $f['requester_id'] === $userId ? $f['addressee_id'] : $f['requester_id'];
$unread = $sdb->get('friend_messages', [
'friendship_id' => 'eq.' . $f['id'],
'sender_id' => 'eq.' . $friendId,
'read_at' => 'is.null',
'select' => 'id'
]);
$count = (is_array($unread) && !isset($unread['error'])) ? count($unread) : 0;
if ($count > 0) {
$unreadCounts[$friendId] = $count;
}
}
jsonResponse(['unread' => $unreadCounts]);
}
if ($action === 'recent') {
// Get recent conversations (last message per friend)
$friendships = $sdb->get('friendships', [
'or' => "(requester_id.eq.{$userId},addressee_id.eq.{$userId})",
'status' => 'eq.accepted',
'select' => 'id,requester_id,addressee_id'
]);
if (!is_array($friendships) || isset($friendships['error']) || empty($friendships)) {
jsonResponse(['conversations' => []]);
}
$conversations = [];
foreach ($friendships as $f) {
$friendId = $f['requester_id'] === $userId ? $f['addressee_id'] : $f['requester_id'];
$lastMsg = $sdb->get('friend_messages', [
'friendship_id' => 'eq.' . $f['id'],
'select' => 'id,sender_id,content,message_type,created_at,read_at',
'order' => 'created_at.desc',
'limit' => 1
]);
if (is_array($lastMsg) && !isset($lastMsg['error']) && !empty($lastMsg)) {
$conversations[] = [
'friend_id' => $friendId,
'friendship_id' => $f['id'],
'last_message' => $lastMsg[0]
];
}
}
// Sort by most recent message
usort($conversations, fn($a, $b) => strcmp($b['last_message']['created_at'], $a['last_message']['created_at']));
jsonResponse(['conversations' => $conversations]);
}
jsonError('Invalid action');
}
if ($method === 'POST') {
$input = getInput();
$action = $input['action'] ?? 'send';
if ($action === 'send') {
$friendId = $input['friend_id'] ?? '';
$content = trim($input['content'] ?? '');
$messageType = $input['message_type'] ?? 'text';
$metadata = $input['metadata'] ?? [];
if (!$friendId) jsonError('friend_id required');
if (!$content) jsonError('content required');
if (mb_strlen($content) > 500) jsonError('Message too long (max 500 chars)');
$friendship = findFriendship($sdb, $userId, $friendId);
if (!$friendship) jsonError('Not friends');
$msg = $sdb->insert('friend_messages', [
'friendship_id' => $friendship['id'],
'sender_id' => $userId,
'content' => $content,
'message_type' => $messageType,
'metadata' => !empty($metadata) ? $metadata : new \stdClass()
]);
if (isset($msg['error'])) jsonError($msg['error']);
jsonResponse(['message' => $msg[0] ?? $msg, 'success' => true]);
}
if ($action === 'mark-read') {
$friendId = $input['friend_id'] ?? '';
if (!$friendId) jsonError('friend_id required');
$friendship = findFriendship($sdb, $userId, $friendId);
if (!$friendship) jsonError('Not friends');
$sdb->update('friend_messages', ['read_at' => gmdate('c')], [
'friendship_id' => 'eq.' . $friendship['id'],
'sender_id' => 'eq.' . $friendId,
'read_at' => 'is.null'
]);
jsonResponse(['success' => true]);
}
jsonError('Invalid action');
}
jsonError('Method not allowed', 405);
function findFriendship($sdb, string $userId, string $friendId): ?array {
$rows = $sdb->get('friendships', [
'or' => "(and(requester_id.eq.{$userId},addressee_id.eq.{$friendId}),and(requester_id.eq.{$friendId},addressee_id.eq.{$userId}))",
'status' => 'eq.accepted',
'select' => 'id,requester_id,addressee_id',
'limit' => 1
]);
if (is_array($rows) && !isset($rows['error']) && !empty($rows)) {
return $rows[0];
}
return null;
}
......@@ -2,7 +2,7 @@
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
......
# سكريبت — العب | 3 دقائق
---
## COLD OPEN (0:00 – 0:08)
**[VISUAL]:** أسود. صوت زهر بيتقلب على خشب. بعدها صوت قطعة شطرنج بتتحط. بعدها صوت domino tile بيخبط.
**VO:**
*(صمت. الأصوات بتتكلم لوحدها.)*
---
## HOOK (0:08 – 0:30)
**[VISUAL]:** كلوز أب على إيدين — طفل بيحرك حصان شطرنج على تابلت. قطع بتزحلق smooth. ساعة بتعد. الطفل بيبتسم ابتسامة واحد عرف إنه كسب.
**VO:**
> في ناس شايفة إن الألعاب الذهنية حاجة قديمة.
> طب ما هي القديمة دي — هي اللي صنعت عقول العالم من 5000 سنة.
> الجديد بقى... إنك تحطها في إيد كل طفل في مصر.
> على الموبايل. بشكل يحببه فيها. ويخليه يرجعلها كل يوم.
---
## المنصة — أول مرة تشوفها (0:30 – 1:00)
**[VISUAL]:** screen recording حقيقي من المنصة — الـcarousel بيتقلب بين الألعاب — الطفل بيدوس "العب" — matchmaking بيشتغل — ماتش بيبدأ في 3 ثواني. بعدها split screen: شطرنج / دومينو / لودو — كلهم شغالين في نفس الوقت.
**VO:**
> ده العب.
> ثلاث ألعاب شغالين دلوقتي — شطرنج، دومينو، لودو — وفيه كمان في الطريق.
> من أول ما تفتح لحد ما تبدأ تلعب — ثواني. مفيش تسجيل معقد. مفيش إعلان بيقطعك.
> اللعب هنا شكله حلو — 60 فريم، سلس، يحسسك إنك بتلعب لعبة console مش website.
> وكل ماتش — سواء ضد صاحبك أو ضد AI بمستويات — بيتسجل. بيأثر في تصنيفك. بيحركك لقدام.
---
## مش لعبة وخلاص (1:00 – 1:30)
**[VISUAL]:** شاشة puzzles — طفل بيحل 3 ألغاز ورا بعض — rating بيطلع. بعدها: شاشة "خطط الشطرنج" (تعليم) — animation بتوضح fork. بعدها: XP bar بيتملي — level up — coins بتنزل — الطفل بيفتح achievement.
**VO:**
> جوا العب فيه نظام تعليم شطرنج مدمج.
> ألغاز يومية. خطط. تكتيكات. بتبدأ من الصفر ومفيش سقف.
> الطفل بيلعب وهو بيتعلم — ومش حاسس إنه في حصة.
> وبنجهز نفس الكلام لبقية الألعاب.
> فيه تصنيف. فيه مستويات. فيه achievements.
> يعني الطفل عنده سبب يرجع كل يوم — وكل يوم بيبقى أحسن من امبارح.
---
## البطولات — وده الموضوع الكبير (1:30 – 2:10)
**[VISUAL]:** bracket بطولة بيتملي live — شعار مدرسة على header البطولة — بعدها شعار جامعة — بعدها شعار شركة. أطفال في "نهائي" — split screen بيبيّن اللاعبين. كأس بيظهر. خريطة مصر بتنور نقطة نقطة.]
**VO:**
> العب فيها نظام بطولات كامل — مبني ومتجرب.
> يعني إيه؟ يعني أي مدرسة، أي جامعة، أي نادي، أي شركة، أي مركز شباب، أي حي سكني —
> يقدر يعمل بطولة رسمية على المنصة.
> القرعة أوتوماتيك. الجدول أوتوماتيك. النتايج أوتوماتيك.
> Swiss، knockout، arena — كل الأنظمة موجودة.
> ده مش plan. ده شغال. البنية التحتية جاهزة.
> ومصر — فيها آلاف المدارس والجامعات والأندية اللي محتاجة بالظبط حاجة زي كده.
> ومفيش حد تاني بيعملها.
---
## الأمان — سطرين وخلاص (2:10 – 2:25)
**[VISUAL]:** أيقونة درع — شاشة emotes (عبارات جاهزة مش chat مفتوح) — أيقونة report.
**VO:**
> آه وبالمناسبة — مفيش chat مفتوح مع غرباء.
> التواصل بعبارات جاهزة. فيه نظام إبلاغ. فيه مراقبة.
> بيئة نضيفة. الأهل يطمنوا.
---
## الصورة الكبيرة (2:25 – 2:50)
**[VISUAL]:** الأرقام بتظهر واحدة واحدة بأنيميشن — "4 ألعاب" — "38 module إدارة" — "نظام بطولات كامل" — "تطوير مصري 100%". بعدها: خريطة الوطن العربي بتنور. Text: "400 مليون. مفيش منافس محلي."]
**VO:**
> العب تطوير مصري بالكامل. مفيش سطر كود من بره.
> فيها نظام إدارة بـ38 module — بطولات، اقتصاد، محتوى، moderation — كل حاجة.
> دي مش MVP. دي منصة ناضجة.
> والمنطقة العربية — 400 مليون إنسان — مفيهاش منتج واحد محلي بيعمل اللي إحنا بنعمله.
> السوق مفتوح. والمنتج جاهز.
---
## CLOSER (2:50 – 3:00)
**[VISUAL]:** الشاشة بتغمق تدريجي. لوجو EL3AB بيفضل. تحته: "منصة الألعاب الذهنية المصرية." Particles ذهبية. ثم black.
**VO:**
> العب.
> الأطفال بتوعنا يستاهلوا أحسن من كده. وده — أحسن من كده.
**[SFX]:** قطعة شطرنج. خلاص.
---
## ملاحظات
| | |
|---|---|
| **Tone** | واحد بيعرض مشروعه — واثق، مش بيبيع. بيوريك الحاجة وخلاص. |
| **VO** | صوت راجل مصري 30s-40s. مش announcer. مش YouTuber. واحد عادي بيتكلم بثقة. |
| **Music** | Lo-fi ambient في الأول → builds subtle percussion في البطولات → drops quiet في الختام |
| **Pacing** | يسيب فراغات. مش كل ثانية عليها كلام. الصورة بتشتغل لوحدها أوقات. |
| **مفيش** | كلمة "استثمار"، "عائد"، "فرصة"، "نمو" — الأرقام بتتكلم لوحدها |
| **الرسالة اللي واصلة** | منتج كامل + سوق فاضي + scale جاهز + فريق بيعرف يعمل إيه = واحد هيكلمكم بعد الفيديو |
# سكريبت فيديو إعلاني — EL3AB (4 دقائق)
---
## SCENE 1 — المشكلة (0:00 – 0:25)
**[VISUAL]:** طفل قاعد على كنبة، عينيه على الموبايل، وشه ملوش تعبير. الأم بتبصله بقلق. مشهد تاني: مجموعة أطفال في مدرسة كل واحد في موبايله. مشهد تالت: أب بيحاول يكلم ابنه والابن مش سامعه.]
**VO (صوت راجل، مصري، هادي في الأول):**
> ولادنا بيقضوا ساعات كل يوم على الشاشة.
> ألعاب عنيفة. محتوى ملوش معنى. وقت بيضيع.
> وإحنا كأهالي... قلقانين. بس مش عارفين البديل إيه.
> لأن الحقيقة — مش هنقدر ناخد الموبايل من إيدهم.
> بس نقدر نغيّر اللي جواه.
---
## SCENE 2 — الحل (0:25 – 0:55)
**[VISUAL]:** الشاشة بتتحول من غامقة لذهبي. لوجو EL3AB بيظهر بأنيميشن — particles ذهبية. بعدها الكاميرا بتدخل جوا المنصة: رقعة شطرنج بتتكوّن قطعة قطعة — دومينو بيصطف — زهر لودو بيتقلب في الهوا.]
**VO (النبرة بتتحوّل — فيها حماس):**
> العب.
> أول منصة مصرية 100% للألعاب الذهنية.
> شطرنج. دومينو. لودو. وألعاب تانية في الطريق.
> منصة اتبنت من الصفر — بإيدين مصرية — عشان تدّي ولادنا مكان يفكّروا فيه.
> مش لعبة واحدة... ده عالم كامل.
---
## SCENE 3 — تجربة اللعب (0:55 – 1:25)
**[VISUAL]:** شاشة المنصة الحقيقية: carousel الألعاب — طفل بيختار شطرنج — بيدوس Play — الماتشميكنج بيلاقيله خصم — اللعبة بتبدأ. القطع بتتحرك بسلاسة 60fps. ساعة بتعد. الطفل بيفكر وبياخد قرار.]
**VO:**
> التجربة جوا العب مختلفة عن أي حاجة تانية.
> اللعبة بتبدأ في ثواني — مفيش تعقيد. مفيش إعلانات مزعجة.
> كل ماتش هو تحدّي حقيقي — ضد لاعب حقيقي أو ضد ذكاء اصطناعي بمستويات.
> الطفل بيتعلّم يركّز. يخطط. يستنى. ياخد قرار تحت ضغط.
> مهارات الحياة... من خلال لعبة.
---
## SCENE 4 — التعليم والتطوير (1:25 – 1:55)
**[VISUAL]:** شاشة puzzle — الطفل بيحل لغز شطرنج — أنيميشن "Correct!" مع نجوم. بعدها شاشة rating بيطلع. بعدها شاشة تعليمية فيها شرح خطة (fork مثلاً). XP bar بيتملي. Level up بأنيميشن كبيرة.]
**VO:**
> جوا العب، الطفل مش بس بيلعب... بيتعلّم ويتطوّر.
> نظام ألغاز شطرنج — كل يوم تحدّي جديد بمستواه.
> منهج تعليم مدمج — من أول القواعد الأساسية لحد الخطط المتقدمة.
> كل خطوة بتتقاس. كل تقدّم بيتسجّل.
> والطفل بيشوف نفسه بيتحسّن — بالأرقام.
> وبنجهّز محتوى تعليمي أكتر لكل الألعاب — مش بس شطرنج.
---
## SCENE 5 — البيئة الآمنة (1:55 – 2:20)
**[VISUAL]:** أيقونة درع حماية — شاشة emotes (بس عبارات مهذبة: "حلو!" "لعب جميل" "GG") — مفيش chat مفتوح — أب بيراجع profile ابنه — شاشة report — إعدادات parental.]
**VO:**
> والأهم من كل ده — العب بيئة آمنة.
> مفيش شات مفتوح مع غرباء. مفيش محتوى غير مناسب.
> التواصل بين اللاعبين بيكون بعبارات محددة ومهذبة.
> نظام إبلاغ فوري. مراقبة دايمة.
> الأهل يقدروا يطمنوا — ولادهم بيلعبوا في مكان نضيف.
> لأن إحنا بنبني ده لولادنا إحنا كمان.
---
## SCENE 6 — البطولات (2:20 – 3:00)
**[VISUAL]:** brackets بطولة بتتملي — شعار مدرسة (تخيلي) على بطولة — شعار جامعة على بطولة تانية — شعار شركة — كأس بيلمع — خريطة مصر عليها نقط مضيئة في محافظات مختلفة — أطفال في ماتش نهائي والجمهور بيشجع (split screen)]
**VO:**
> بس العب مش بس لعبة فردية.
> العب مجهّزة بنظام بطولات كامل — جاهز يستضيف أي بطولة رسمية.
> مدارس عايزة تعمل بطولة شطرنج بين طلابها؟ جاهز.
> جامعة عايزة كأس ألعاب ذهنية بين كلياتها؟ جاهز.
> شركة عايزة team building بطريقة مختلفة؟ جاهز.
> أندية. مراكز شباب. أحياء سكنية.
> القرعة. الجدول. الـbrackets. النتايج. كل حاجة أوتوماتيك.
> منصة واحدة لكل بطولات الألعاب التقليدية في مصر.
---
## SCENE 7 — الحجم والرؤية (3:00 – 3:30)
**[VISUAL]:** أرقام بتظهر بأنيميشن (عداد): "106 table في الداتابيز" — "4 ألعاب" — "38 module إدارة" — "كل المحافظات". بعدها timeline مستقبلي: ألعاب جديدة بتتضاف — أيقونة trivia — أيقونة backgammon. خريطة المنطقة العربية بتنور.]
**VO:**
> العب مش فكرة على ورق.
> ده نظام متكامل — شغّال دلوقتي.
> نظام إدارة كامل. بنية تحتية جاهزة للتوسع.
> النهاردة — مصر. بكرة — كل طفل في الوطن العربي يلاقي مكان يفكّر فيه.
> الألعاب الذهنية سوق بيكبر كل يوم.
> والمنطقة العربية — 400 مليون إنسان — مفيش حد بيخدمهم بمنتج من عندهم.
> لحد دلوقتي.
---
## SCENE 8 — الأثر (3:30 – 3:50)
**[VISUAL]:** مونتاج عاطفي: طفل كسب أول بطولة مدرسة — أب فخور بابنه — مدرّس بيشرح شطرنج على العب — أطفال من محافظات مختلفة بيلعبوا ضد بعض — ضحك — تركيز — لحظة فوز.]
**VO:**
> إحنا مش بنبني app.
> إحنا بنبني جيل بيفكّر.
> جيل اتعلّم إن الذكاء ممتع. إن التحدي حلو. إن المنافسة الشريفة بتبني الشخصية.
> تطوير مصري. لأولاد مصر. والمنطقة كلها.
---
## SCENE 9 — الختام (3:50 – 4:00)
**[VISUAL]:** لوجو EL3AB كبير في النص — تحته "منصة الألعاب الذهنية المصرية" — particles ذهبية بتتحرك — الشاشة حية. ثم fade to black مع صوت قطعة شطرنج بتتحط.]
**VO:**
> **العب.**
> عقول بتتبني. أبطال بتتصنع. ومستقبل بيتكتب.
**[SUPER على الشاشة]:** el3ab.com | العب — نمّي ذكاءك
**[SFX]:** صوت قطعة شطرنج بتتحط. Beat. صمت.]
---
## ملاحظات إنتاج
| عنصر | توصية |
|------|--------|
| **المدة** | 4:00 بالظبط |
| **الـ VO** | صوت راجل مصري — هادي في الأول، واثق وحماسي في النص، عاطفي في الآخر |
| **الموسيقى** | تبدأ piano خفيف (المشكلة) → orchestral بتتصاعد (الحل/البطولات) → emotional في الختام |
| **الإيقاع** | Scene 1 بطيء (مشكلة) — Scene 3-6 سريع (إثارة) — Scene 8-9 بطيء (عاطفة) |
| **اللون** | يبدأ رمادي/بارد (المشكلة) → يتحول لـ dark + gold (المنصة) |
| **مفيش** | كلمة "استثمار" أو "عائد" أو "revenue" أو أي لغة بيزنس مباشرة |
| **الرسالة الضمنية** | سوق ضخم + منتج شغال + بنية جاهزة + مفيش منافس عربي = فرصة واضحة |
---
## تقسيم الوقت
| الجزء | المدة | الهدف |
|-------|-------|-------|
| المشكلة | 25 ثانية | يحس المشاهد بالألم |
| الحل (العب) | 30 ثانية | الأمل — فيه بديل |
| تجربة اللعب | 30 ثانية | يشوف المنتج شغال |
| التعليم | 30 ثانية | القيمة التربوية |
| البيئة الآمنة | 25 ثانية | طمأنة الأهل |
| البطولات | 40 ثانية | الـscale والمؤسسات |
| الحجم والرؤية | 30 ثانية | الفرصة (ضمنياً) |
| الأثر + ختام | 30 ثانية | العاطفة والـcall to action |
# سكريبت فيديو إعلاني — EL3AB (دقيقتين)
---
## SCENE 1 — الافتتاحية (0:00 – 0:15)
**[VISUAL]:** شاشة سودا. صوت نوتيفيكيشن موبايل. أيد طفل بتمسك تابلت. الشاشة بتنور بلون دهبي. لوجو EL3AB بيظهر بأنيميشن.]
**VO (صوت راجل، مصري، حماسي بس مش مبالغ):**
> كل يوم، ملايين الأطفال والشباب بيقضوا ساعات على الموبايل...
> بس لو الساعات دي بتشغّل دماغهم؟ بتنمّي ذكاهم؟ بتعلّمهم يفكّروا؟
---
## SCENE 2 — المنصة (0:15 – 0:40)
**[VISUAL]:** مونتاج سريع: رقعة شطرنج بتتحرك عليها قطع بسلاسة — دومينو بيتحط على الطاولة — زهر لودو بيتقلب — أطفال مبتسمين بيلعبوا على تابلت. كل ده بأنيميشن الـ60fps بتاعت المنصة.]
**VO:**
> العب... أول منصة مصرية 100% للألعاب الذهنية.
> شطرنج. دومينو. لودو. وألعاب جاية كمان.
> مش مجرد لعبة — ده عالم كامل. تصنيف. بطولات. جوايز. تطوّر.
> كل ده في بيئة آمنة تماماً للأطفال والعيلة.
---
## SCENE 3 — التعليم (0:40 – 1:00)
**[VISUAL]:** شاشة puzzle شطرنج — طفل بيحل لغز — أنيميشن level up وXP bar بيتملي — شاشة تعليم خطط شطرنج.]
**VO:**
> جوا العب، الطفل مش بس بيلعب... بيتعلّم.
> نظام تعليم شطرنج مدمج — من أول القواعد لحد الخطط المتقدمة.
> كل خطوة محسوبة إنها تخلّي الطفل يحب يفكّر أكتر.
> وبنجهّز محتوى تعليمي أكبر لكل الألعاب.
---
## SCENE 4 — البطولات (1:00 – 1:25)
**[VISUAL]:** brackets بطولات بتتملي بأسماء — شعارات مدارس وجامعات (تخيلية) — كأس دهبي بيلمع — خريطة مصر عليها نقط مضيئة في كل محافظة.]
**VO:**
> العب مجهّزة إنها تستضيف بطولات رسمية.
> مدارس. جامعات. شركات. أندية. مراكز شباب. أحياء.
> أي جهة عايزة تعمل بطولة ألعاب ذهنية — العب بتوفّرلها كل حاجة:
> التنظيم. القرعة. الجدول. النتايج. البث المباشر.
> منصة واحدة لكل بطولات الألعاب التقليدية في مصر.
---
## SCENE 5 — الرؤية (1:25 – 1:50)
**[VISUAL]:** مونتاج: طفل بيكسب ماتش وبيحتفل — ranking بيطلع — أب وابنه بيلعبوا مع بعض — شاشة leaderboard فيها أسماء عربي — الأنيميشن الدهبية بتاعت rank-up.]
**VO:**
> إحنا بنبني حاجة أكبر من لعبة.
> بنبني المكان اللي كل طفل مصري يقدر فيه يكتشف ذكاءه.
> يتحدّى نفسه. يتحدّى صحابه. يتحدّى أبطال من كل المحافظات.
> تطوير مصري. فكر مصري. لأولادنا.
---
## SCENE 6 — الختام (1:50 – 2:00)
**[VISUAL]:** لوجو EL3AB كبير في النص — تحته "منصة الألعاب الذهنية المصرية" — الخلفية فيها particles دهبية بتتحرك — الشاشة حية مش ساكنة أبداً.]
**VO:**
> **العب.**
> عقول بتتبني. أبطال بتتصنع.
**[SUPER على الشاشة]:** el3ab.com | العب — نمّي ذكاءك
**[SFX]:** صوت قطعة شطرنج بتتحط. ثم صمت.]
---
## ملاحظات إنتاج
| عنصر | توصية |
|------|--------|
| **المدة** | 2:00 بالظبط |
| **الـ VO** | صوت راجل مصري — واثق، دافي، مش loud |
| **الموسيقى** | Epic orchestral خفيفة، بتتصاعد في scene 4 و5 |
| **الإيقاع** | قطعات سريعة في المونتاج، بطيئة في الختام |
| **اللون** | Dark theme مع gold accents (زي الـ design tokens بتوع المنصة) |
| **مفيش** | كلمة "استثمار" أو "عائد" أو أي لغة بيزنس مباشرة |
# سكريبت فيديو — العب | EL3AB (3 دقائق)
# للإنتاج بالـ AI — كل مشهد فيه وصف بصري كامل
---
## معلومات الإنتاج
| عنصر | القيمة |
|------|--------|
| **المدة** | 3:00 دقائق |
| **اللغة** | عربي مصري — صوت دافي، فخور، كأنه صاحبك بيحكيلك على حاجة حلوة |
| **النبرة** | إيجابية ١٠٠٪ — فخر، حماس، دفء، فرحة |
| **الموسيقى** | Upbeat cinematic — feel-good من أول ثانية، بتتصاعد مع كل scene |
| **الألوان** | Dark navy + Gold + Cyan — premium بس دافي |
| **التأثير المطلوب** | المشاهد يبتسم، يحس بالفخر، ويقول "عايز أجرّب ده" |
---
## SCENE 1 — افتتاحية: ده العب (0:00 – 0:25)
**[VISUAL]:**
- فتح على لوجو "العب | EL3AB" — particles ذهبية بتتجمع وتكوّنه — أنيميشن سينمائي
- الكاميرا بتعدّي من خلال اللوجو — بتدخل عالم ملون: رقعة شطرنج بتتكوّن بسحر — قطع domino بتترص بإيقاع — زهر لودو بيدور في الهوا
- ألوان دافية: gold glow على كل حاجة — شطرنج (أزرق وذهبي) — دومينو (أخضر وcyan) — لودو (بنفسجي وروز)
- أطفال مصريين (٨-١٤ سنة) بيلعبوا — وشوشهم فيها تركيز وفرحة — واحد بيحرك بيدق شطرنج — واحدة بتحط domino — واحد بيرمي زهر
**VO (صوت راجل مصري، دافي وفخور — كأنه بيحكيلك حكاية حلوة):**
> تخيّل معايا مكان...
> أطفالنا يدخلوه — يلاقوا شطرنج، دومينو، لودو، طاولة...
> يلعبوا مع صحابهم، يتحدّوا ناس من كل مصر، يكسبوا بطولات.
> ويطلعوا منه — أذكى.
> ده العب. ده بيتنا.
---
## SCENE 2 — اللعب: تجربة حقيقية (0:25 – 0:55)
**[VISUAL]:**
- شاشة المنصة الحقيقية — carousel ألعاب بيتحرك — ألوان مبهجة
- طفل بيدوس "العب" — عداد matchmaking بيدور ٣ ثواني — "تم!" — اللعبة بتبدأ
- شاشة شطرنج: قطع بتتحرك بسلاسة — الطفل بيسحب الملك بإصبعه — حركة ناعمة زي الحرير
- split screen: طفل في إسكندرية (ملابس صيفي) ضد طفلة في أسيوط — نفس الماتش — وشوشهم فيها حماس
- خط ضوئي (cyan) بيربط بين الاتنين — بيمثل الاتصال الحي
- بعد الماتش: نجوم بتنزل — rating بيطلع — "+25" بتلمع — الطفل مبتسم
- مشهد تاني: ٤ أطفال بيلعبوا domino — كل واحد في بيته — بيضحكوا ويبعتوا emotes "حلو!" "يا سلام!"
**VO:**
> اللعبة بتبدأ في ثواني. حرفياً.
> تدوس play — يلاقيلك حد بمستواك — وتبدأ.
> والإحساس جواها؟ زي ما تكون قاعد على ترابيزة حقيقية.
> القطع بتتحرك تحت إيدك. الحماس حقيقي. المنافسة حقيقية.
> تلاعب صاحبك. تلاعب حد من محافظة تانية. أو تتحدى الذكاء الاصطناعي.
> وكل ماتش — بيقيس مستواك وبيطوّرك.
---
## SCENE 3 — البطولات: كل مصر بتلعب (0:55 – 1:35)
**[VISUAL]:**
- brackets بطولة بتتكوّن من فوق لتحت — أسماء عربية بتظهر في كل خانة — أنيميشن سلس
- لوجو مدرسة فوق البطولة — "بطولة مدرسة النيل — شطرنج"
- Transition ذهبي → لوجو جامعة — "كأس الجامعات — ألعاب ذهنية"
- Transition → لوجو نادي — "بطولة نادي الزمالك — دومينو"
- Transition → لوجو مركز شباب — "بطولة محافظة الجيزة"
- خريطة مصر من فوق — نقط ذهبية بتنور في محافظات — واحدة ورا واحدة — لحد ما مصر كلها منورة
- أطفال في "نهائي" — تركيز — لحظة فوز — إيديه في الهوا — كأس ذهبي بيتقدّمله
- text بيظهر: "Swiss • Round-Robin • Elimination • Arena • Hybrid"
**VO (حماسي — كأنه بيوصف حدث رياضي كبير):**
> والأحلى؟ البطولات.
> العب مش لعبة وخلاص — ده ملعب كامل.
> مدرسة عايزة تعمل بطولة شطرنج؟ بتتجهّز في دقايق.
> جامعة عايزة كأس ألعاب ذهنية بين كلياتها؟ اتفضل.
> نادي. مركز شباب. حي. محافظة. البلد كلها.
> كل أنظمة البطولات موجودة — القرعة أوتوماتيك. الجدول أوتوماتيك. النتايج لحظية.
> منصة واحدة جاهزة تستضيف كل بطولات الألعاب التقليدية في مصر.
> تخيّل — بطولة الجمهورية في الشطرنج... على العب.
---
## SCENE 4 — الأمان: بيتنا نضيف (1:35 – 2:00)
**[VISUAL]:**
- درع ذهبي بيظهر — حواليه glow دافي (مش خوف — أمان)
- جوا الدرع أيقونات بتطلع بابتسامة:
- Emotes لطيفة: "👍 لعب جميل!" — "🤝 ماتش حلو!" — "⭐ يا بطل!"
- أيقونة عيلة مبتسمة
- أيقونة checkmark خضرا
- أم مبتسمة بتبص على موبايل ابنها — الشاشة فيها ماتش شطرنج — هي مرتاحة ومطمنة
- أب بيلعب مع ابنه على المنصة — bonding moment
- ختام: الدرع بيلمع — text: "بيئة آمنة ومحمية"
**VO (دافي ومطمئن — كأنه بيكلم أب أو أم):**
> وأهم حاجة — المكان ده نضيف.
> التواصل بين اللاعبين بعبارات حلوة ومهذبة بس.
> نظام حماية شغال ٢٤ ساعة.
> يعني ابنك أو بنتك يلعبوا — وإنت مطمّن.
> ده بيتنا — وبيتنا لازم يكون آمن.
---
## SCENE 5 — مصري ١٠٠٪ (2:00 – 2:30)
**[VISUAL]:**
- علم مصر بيتحوّل لـ code lines (بس بشكل فني وجميل — مش geeky)
- Transition: إيد بتكتب على keyboard — بتتحول لقطعة شطرنج ذهبية
- خريطة مصر — "صُنع في مصر 🇪🇬" بتظهر بخط كبير
- أرقام بتظهر بأنيميشن عداد (بسيطة — مش overwhelming):
- "٤ ألعاب" — "وجاي أكتر"
- "كل المحافظات" — "متصلين"
- "٤٠٠ مليون عربي" — "مستنيين"
- خريطة المنطقة العربية — مصر مضيئة بالذهب — باقي الدول بتنور واحدة ورا واحدة
- أطفال من أشكال مختلفة (مصريين — من محافظات مختلفة) بيلعبوا ومبسوطين
**VO (فخور — صوت فيه اعتزاز حقيقي):**
> وأحلى حاجة في الموضوع كله؟
> ده شغل مصري. ١٠٠٪. من أول فكرة لآخر تفصيلة.
> مش ترجمة من برّه. مش نسخة. ده حاجة اتولدت هنا — لينا إحنا.
> النهاردة — مصر.
> بكرة — كل طفل عربي هيلاقي مكانه هنا.
> ٤٠٠ مليون إنسان في المنطقة — ومحدش بيقدّملهم ده.
> لحد دلوقتي.
---
## SCENE 6 — الختام: عقول بتتبني (2:30 – 3:00)
**[VISUAL]:**
- مونتاج سريع (١ ثانية لكل لقطة) — كله إيجابي:
- طفل كسب أول ماتش — ابتسامة كبيرة
- بنت فازت ببطولة مدرسة — صحابها بيصفقولها
- Rating بيطلع — "1250!" — celebrate animation
- ٤ أطفال بيلعبوا لودو — ضحك
- أب وابنه بيخمسوا بعض بعد ماتش
- خريطة مصر كلها منورة
- Fade لخلفية سوداء
- لوجو "العب | EL3AB" كبير في النص — particles ذهبية بتتحرك حواليه ببطء
- Tagline: "عقول بتتبني. أبطال بتتصنع."
- تحتها: el3ab.com
- Sound: نغمة موسيقية حلوة — مش dramatic — feel-good ending
**VO (قوي بس دافي — زي ختام فيلم حلو):**
> العب مش app.
> العب حلم — بيتحقق.
> كل طفل يدخل هنا — بيطلع أحسن.
> بيفكّر أحسن. بينافس أحسن. بيحب التحدي.
> صناعة مصرية — للعقول المصرية — وللمنطقة كلها.
> **العب.**
> عقول بتتبني. أبطال بتتصنع. ومستقبل بيتكتب — من هنا.
**[FINAL FRAME — 5 seconds]:**
```
العب | EL3AB
منصة الألعاب الذهنية المصرية
el3ab.com
```
---
## نسخة الـ VO كاملة متصلة (جاهزة للتسجيل أو AI voice)
```
تخيّل معايا مكان...
أطفالنا يدخلوه — يلاقوا شطرنج، دومينو، لودو، طاولة...
يلعبوا مع صحابهم، يتحدّوا ناس من كل مصر، يكسبوا بطولات.
ويطلعوا منه — أذكى.
ده العب. ده بيتنا.
اللعبة بتبدأ في ثواني. حرفياً.
تدوس play — يلاقيلك حد بمستواك — وتبدأ.
والإحساس جواها؟ زي ما تكون قاعد على ترابيزة حقيقية.
القطع بتتحرك تحت إيدك. الحماس حقيقي. المنافسة حقيقية.
تلاعب صاحبك. تلاعب حد من محافظة تانية. أو تتحدى الذكاء الاصطناعي.
وكل ماتش — بيقيس مستواك وبيطوّرك.
والأحلى؟ البطولات.
العب مش لعبة وخلاص — ده ملعب كامل.
مدرسة عايزة تعمل بطولة شطرنج؟ بتتجهّز في دقايق.
جامعة عايزة كأس ألعاب ذهنية بين كلياتها؟ اتفضل.
نادي. مركز شباب. حي. محافظة. البلد كلها.
كل أنظمة البطولات موجودة — القرعة أوتوماتيك. الجدول أوتوماتيك. النتايج لحظية.
منصة واحدة جاهزة تستضيف كل بطولات الألعاب التقليدية في مصر.
تخيّل — بطولة الجمهورية في الشطرنج... على العب.
وأهم حاجة — المكان ده نضيف.
التواصل بين اللاعبين بعبارات حلوة ومهذبة بس.
نظام حماية شغال ٢٤ ساعة.
يعني ابنك أو بنتك يلعبوا — وإنت مطمّن.
ده بيتنا — وبيتنا لازم يكون آمن.
وأحلى حاجة في الموضوع كله؟
ده شغل مصري. ١٠٠٪. من أول فكرة لآخر تفصيلة.
مش ترجمة من برّه. مش نسخة. ده حاجة اتولدت هنا — لينا إحنا.
النهاردة — مصر.
بكرة — كل طفل عربي هيلاقي مكانه هنا.
٤٠٠ مليون إنسان في المنطقة — ومحدش بيقدّملهم ده.
لحد دلوقتي.
العب مش app.
العب حلم — بيتحقق.
كل طفل يدخل هنا — بيطلع أحسن.
بيفكّر أحسن. بينافس أحسن. بيحب التحدي.
صناعة مصرية — للعقول المصرية — وللمنطقة كلها.
العب.
عقول بتتبني. أبطال بتتصنع. ومستقبل بيتكتب — من هنا.
```
---
## ملاحظات للـ AI Video Generation
### Style
- **نبرة بصرية:** دافية، مبهجة، premium بس إنسانية
- **ألوان:** Navy + Gold + Cyan — كل حاجة فيها glow دافي
- **أطفال:** مصريين، أعمار ٨-١٤، ملابس يومية، ضحك وتركيز وفرحة
- **مفيش خالص:** لقطات حزينة، ألوان باردة، نبرة خوف، مشاكل
- **حركة الكاميرا:** Smooth — dolly، slow zoom — مفيش shake
- **الانتقالات:** Gold wipes — light particles — soft
- **RTL:** كل النصوص عربي من اليمين للشمال
### Pacing
| الثانية | الإيقاع | الشعور |
|---------|---------|--------|
| 0:00–0:25 | هادي ودافي | "تعال أوريك" |
| 0:25–0:55 | متوسط ومحمّس | "شوف ده!" |
| 0:55–1:35 | سريع وحماسي | "تخيّل!" |
| 1:35–2:00 | هادي ومطمئن | "ومتقلقش" |
| 2:00–2:30 | فخور ومتصاعد | "ده بتاعنا" |
| 2:30–3:00 | قوي وختامي | "ده المستقبل" |
logof.png

85.4 KB

......@@ -99,40 +99,36 @@ export function mountGame(el, params) {
}
el.innerHTML = `
<div class="chess-layout" style="display:flex;flex-direction:column;height:100%;background:#1a1a2e;">
<!-- Ad Banner — takes all top void space, hidden if no ad -->
<div id="ad-banner-top" style="flex:1;display:none;align-items:center;justify-content:center;padding:8px 12px;background:#0a0a1a;cursor:pointer;" onclick="window.__adClick && window.__adClick()">
<div id="ad-content" style="width:100%;height:100%;max-height:180px;border-radius:10px;overflow:hidden;display:flex;align-items:center;justify-content:center;"></div>
</div>
<!-- Opponent Bar (compact — keep minimal at top) -->
<div class="chess-bar" style="display:flex;align-items:center;justify-content:space-between;padding:4px 12px;background:#0f0f1e;">
<div style="display:flex;align-items:center;gap:8px;">
<div id="opponent-avatar" style="width:30px;height:30px;border-radius:50%;background:#2a2a4a;display:flex;align-items:center;justify-content:center;overflow:hidden;border:2px solid ${mode === 'bot' ? '#64748b' : '#3B82F6'};">
${mode === 'bot' ? `<img src="https://stockfishapi.caprover.al-arcade.com/portraits/${botId || 'amina'}.png" style="width:100%;height:100%;object-fit:cover;" onerror="this.style.display='none';this.parentNode.textContent='🤖'">` : '<span style="font-size:14px;">👤</span>'}
<div class="chess-layout" style="display:flex;flex-direction:column;height:100%;background:#0f0f1e;justify-content:center;">
<!-- Opponent Bar -->
<div class="chess-bar" style="display:flex;align-items:center;justify-content:space-between;padding:8px 14px;background:#0f0f1e;">
<div style="display:flex;align-items:center;gap:10px;">
<div id="opponent-avatar" style="width:36px;height:36px;border-radius:50%;background:#2a2a4a;display:flex;align-items:center;justify-content:center;overflow:hidden;border:2px solid ${mode === 'bot' ? '#64748b' : '#3B82F6'};">
${mode === 'bot' ? `<img src="https://stockfishapi.caprover.al-arcade.com/portraits/${botId || 'amina'}.png" style="width:100%;height:100%;object-fit:cover;" onerror="this.style.display='none';this.parentNode.textContent='🤖'">` : '<span style="font-size:16px;">👤</span>'}
</div>
<div>
<div style="font-size:12px;font-weight:600;color:#f8fafc;" id="opponent-name">${mode === 'bot' ? (botId || 'Bot') : 'جاري التحميل...'}</div>
<div style="font-size:13px;font-weight:600;color:#f8fafc;" id="opponent-name">${mode === 'bot' ? (botId || 'Bot') : 'جاري التحميل...'}</div>
<div style="display:flex;gap:6px;align-items:center;">
<div id="opponent-level" style="font-size:10px;color:#64748b;">${mode === 'bot' ? 'بوت' : ''}</div>
<div id="opponent-captured" style="font-size:11px;color:#94a3b8;letter-spacing:1px;"></div>
</div>
</div>
</div>
<div id="clock-opponent" class="chess-clock" style="font-size:16px;font-weight:700;font-family:Inter,monospace;background:#1e1e3a;padding:3px 10px;border-radius:6px;color:#f8fafc;min-width:56px;text-align:center;">${clock.format(tc.time)}</div>
<div id="clock-opponent" class="chess-clock">${clock.format(tc.time)}</div>
</div>
<!-- Board -->
<div id="board-container" style="flex:0 0 auto;display:flex;align-items:center;justify-content:center;padding:2px 4px;position:relative;">
<div id="bot-thinking" style="display:none;position:absolute;top:8px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.8);color:#E4AC38;padding:4px 12px;border-radius:12px;font-size:12px;font-weight:600;z-index:10;">
<div id="board-container" style="flex:0 0 auto;display:flex;align-items:center;justify-content:center;padding:4px 6px;position:relative;">
<div id="bot-thinking" style="display:none;position:absolute;top:8px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.85);color:#E4AC38;padding:4px 12px;border-radius:12px;font-size:12px;font-weight:600;z-index:10;">
${t('game.thinking')} <span class="pulse">${emoji('thinking_dots', '●●●', 12)}</span>
</div>
<div id="promo-dialog" style="display:none;position:absolute;z-index:20;background:#1e1e3a;border-radius:8px;padding:8px;box-shadow:0 8px 32px rgba(0,0,0,0.8);border:1px solid rgba(255,255,255,0.1);"></div>
</div>
<!-- Player Bar + Emote Toggle -->
<div class="chess-bar" style="display:flex;align-items:center;justify-content:space-between;padding:6px 12px;background:#0f0f1e;">
<div style="display:flex;align-items:center;gap:8px;">
<div style="width:34px;height:34px;border-radius:50%;background:#2a2a4a;display:flex;align-items:center;justify-content:center;overflow:hidden;border:2px solid var(--gold);">
<!-- Player Bar -->
<div class="chess-bar" style="display:flex;align-items:center;justify-content:space-between;padding:8px 14px;background:#0f0f1e;">
<div style="display:flex;align-items:center;gap:10px;">
<div style="width:36px;height:36px;border-radius:50%;background:#2a2a4a;display:flex;align-items:center;justify-content:center;overflow:hidden;border:2px solid var(--gold, #E4AC38);">
${store.get('player.avatar_url') ? `<img src="${store.get('player.avatar_url')}" style="width:100%;height:100%;object-fit:cover;">` : `<span style="font-size:16px;">👤</span>`}
</div>
<div>
......@@ -142,32 +138,35 @@ export function mountGame(el, params) {
<div id="player-captured" style="font-size:11px;color:#94a3b8;letter-spacing:1px;"></div>
</div>
</div>
<button id="emote-inline-toggle" style="width:32px;height:32px;border-radius:50%;background:#1e1e3a;border:1px solid rgba(255,255,255,0.1);color:#f8fafc;font-size:14px;cursor:pointer;display:flex;align-items:center;justify-content:center;margin-inline-start:4px;">💬</button>
<button id="emote-inline-toggle" style="width:32px;height:32px;border-radius:50%;background:#1a1a2e;border:1px solid rgba(255,255,255,0.08);color:#f8fafc;font-size:14px;cursor:pointer;display:flex;align-items:center;justify-content:center;margin-inline-start:4px;">💬</button>
</div>
<div id="clock-player" class="chess-clock" style="font-size:18px;font-weight:700;font-family:Inter,monospace;background:#1e1e3a;padding:4px 12px;border-radius:6px;color:#f8fafc;min-width:60px;text-align:center;">${clock.format(tc.time)}</div>
<div id="clock-player" class="chess-clock active">${clock.format(tc.time)}</div>
</div>
<!-- Opening Name + Material -->
<div style="display:flex;justify-content:space-between;align-items:center;padding:2px 12px;background:#0f0f1e;">
<div style="display:flex;justify-content:space-between;align-items:center;padding:2px 14px;">
<div id="opening-name" style="font-size:11px;color:#64748b;font-style:italic;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:70%;"></div>
<div id="material-diff" style="font-size:12px;font-weight:700;font-family:Inter,monospace;color:#E4AC38;"></div>
<div id="material-diff" style="font-size:12px;font-weight:700;font-family:'SF Mono',monospace;color:#E4AC38;"></div>
</div>
<!-- Move List -->
<div id="move-list" style="max-height:44px;overflow-x:auto;white-space:nowrap;padding:3px 12px;background:#0f0f1e;border-top:1px solid rgba(255,255,255,0.05);font-family:Inter,monospace;font-size:12px;color:#94a3b8;display:flex;gap:4px;align-items:center;">
<div id="move-list" style="max-height:40px;overflow-x:auto;white-space:nowrap;padding:4px 14px;font-family:'SF Mono',monospace;font-size:12px;color:#94a3b8;display:flex;gap:4px;align-items:center;">
<span style="color:#475569;">1.</span>
</div>
<!-- Controls — sticky bottom bar for thumb reach -->
<div style="display:flex;gap:8px;padding:10px 12px;background:#0f0f1e;border-top:1px solid rgba(255,255,255,0.05);padding-bottom:max(10px, var(--safe-bottom, 0px));">
<!-- Controls -->
<div style="display:flex;gap:8px;padding:10px 14px;padding-bottom:max(10px, env(safe-area-inset-bottom, 0px));">
<button class="ctrl-btn" id="btn-resign">${emoji('flag', '⚐', 12)} ${t('game.resign')}</button>
<button class="ctrl-btn" id="btn-draw">½ ${t('game.draw')}</button>
<button class="ctrl-btn" id="btn-flip">⟲ ${t('game.flip')}</button>
</div>
</div>
<style>
.ctrl-btn { flex:1;background:#1e1e3a;border:1px solid rgba(255,255,255,0.1);color:#e2e8f0;font-size:13px;font-weight:600;padding:12px 8px;border-radius:10px;cursor:pointer;font-family:inherit;transition:background 0.15s,transform 0.1s;min-height:48px; }
.chess-layout { gap: 0; }
.chess-clock { font-size:18px;font-weight:700;font-family:'SF Mono',Inter,monospace;background:#1a1a2e;padding:6px 14px;border-radius:8px;color:#94a3b8;min-width:64px;text-align:center;border:1px solid rgba(255,255,255,0.06); }
.chess-clock.active { color:#f8fafc;border-color:rgba(255,255,255,0.15); }
.chess-clock.low-time { color:#EF4444!important;animation:clockPulse 1s infinite;border-color:rgba(239,68,68,0.3); }
.ctrl-btn { flex:1;background:#1a1a2e;border:1px solid rgba(255,255,255,0.08);color:#e2e8f0;font-size:13px;font-weight:600;padding:12px 8px;border-radius:10px;cursor:pointer;font-family:inherit;transition:background 0.15s,transform 0.1s;min-height:48px; }
.ctrl-btn:active { background:#2a2a5a;transform:scale(0.95); }
.chess-clock.low-time { color:#EF4444!important;animation:clockPulse 1s infinite; }
@keyframes clockPulse { 0%,100%{opacity:1}50%{opacity:0.5} }
</style>
`;
......@@ -320,9 +319,6 @@ export function mountGame(el, params) {
emoteContainer.insertBefore(emotePanel, playerBar.nextSibling);
}
// Load ad banner
loadAdBanner(el);
bus.emit('game:started', { gameKey: 'chess', matchId, opponent: botId, mode });
}
......@@ -982,18 +978,3 @@ function reportTournamentResult(result) {
}).catch(e => console.warn('[tournament] report error:', e));
}
async function loadAdBanner(el) {
try {
const res = await net.post('ads.php', { action: 'get', slot: 'banner_top', game: 'chess' });
if (!res || res.error || !res.image_url) return;
const wrapper = el.querySelector('#ad-banner-top');
const content = el.querySelector('#ad-content');
if (!wrapper || !content) return;
content.innerHTML = `<img src="${res.image_url}" style="width:100%;height:100%;object-fit:contain;border-radius:12px;-webkit-mask-image:linear-gradient(to right,transparent 0%,black 6%,black 94%,transparent 100%);mask-image:linear-gradient(to right,transparent 0%,black 6%,black 94%,transparent 100%);">`;
wrapper.style.display = 'flex';
window.__adClick = () => {
if (res.click_url) window.open(res.click_url, '_blank');
net.post('ads.php', { action: 'impression', campaign_id: res.id }).catch(() => {});
};
} catch (e) { /* ad load failed silently */ }
}
......@@ -3,8 +3,12 @@ import { mountTable } from './scenes/table.js';
import { mountBotSelect } from './scenes/bot-select.js';
import { mountTimeSelect } from './scenes/time-select.js';
import { mountQueue } from './scenes/queue.js';
import { mountLobby } from './scenes/lobby.js';
import { mountChallenge } from './scenes/challenge.js';
scene.register('play-table', mountTable);
scene.register('play-bot-select', mountBotSelect);
scene.register('play-time-select', mountTimeSelect);
scene.register('play-queue', mountQueue);
scene.register('game-lobby', mountLobby);
scene.register('challenge-friend', mountChallenge);
import * as net from '../../../core/net.js';
import * as scene from '../../../core/scene.js';
import * as audio from '../../../core/audio.js';
import * as store from '../../../core/store.js';
import * as juice from '../../../core/juice.js';
import { emoji } from '../../../core/theme.js';
export function mountChallenge(el) {
el.innerHTML = `
<div class="cf-layout">
<div class="cf-header">
<button id="cf-back" class="cf-back-btn">→</button>
<div class="cf-title">${emoji('challenge_swords', '⚔️', 18)} تحدّي صديق</div>
</div>
<div class="cf-body" id="cf-body">
<div style="text-align:center;padding:32px;color:#64748b;font-size:13px;">${emoji('loading', '⏳', 14)} جاري تحميل الأصدقاء...</div>
</div>
</div>
<style>
.cf-layout { display:flex;flex-direction:column;height:100%;background:#0a0a14; }
.cf-header { display:flex;align-items:center;gap:12px;padding:12px 16px;background:#0f0f1e;border-bottom:1px solid rgba(255,255,255,0.06); }
.cf-back-btn { background:none;border:none;color:#94a3b8;font-size:20px;cursor:pointer;padding:4px 8px;font-family:inherit; }
.cf-title { font-size:16px;font-weight:700;color:#f8fafc; }
.cf-body { flex:1;overflow-y:auto;padding:16px; }
.cf-card { display:flex;align-items:center;gap:12px;padding:14px;background:#1a1a2e;border-radius:14px;margin-bottom:10px;cursor:pointer;transition:transform 0.1s,background 0.15s;border:1px solid rgba(255,255,255,0.04); }
.cf-card:active { transform:scale(0.98);background:#222244; }
.cf-card-avatar { width:48px;height:48px;border-radius:50%;background:#2a2a4a;display:flex;align-items:center;justify-content:center;overflow:hidden;flex-shrink:0;position:relative; }
.cf-card-avatar img { width:100%;height:100%;object-fit:cover;border-radius:50%; }
.cf-card-online { position:absolute;bottom:1px;right:1px;width:12px;height:12px;border-radius:50%;background:#34D399;border:2px solid #1a1a2e; }
.cf-card-info { flex:1;min-width:0; }
.cf-card-name { font-size:14px;font-weight:600;color:#f8fafc;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; }
.cf-card-sub { font-size:11px;color:#64748b;margin-top:2px; }
.cf-card-action { padding:8px 16px;border-radius:10px;background:linear-gradient(135deg,#7c3aed,#a855f7);border:none;color:#fff;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit;transition:transform 0.1s; }
.cf-card-action:active { transform:scale(0.9); }
.cf-section-title { font-size:12px;font-weight:600;color:#64748b;margin-bottom:8px;padding-right:4px; }
</style>
`;
el.querySelector('#cf-back').addEventListener('click', () => {
audio.play('click');
scene.pop();
});
loadFriendsForChallenge(el);
}
async function loadFriendsForChallenge(el) {
const body = el.querySelector('#cf-body');
try {
const data = await net.get('friends.php', { action: 'list' });
const friends = data.friends || [];
if (friends.length === 0) {
body.innerHTML = `
<div style="text-align:center;padding:48px 24px;">
<div style="font-size:48px;margin-bottom:12px;opacity:0.5;">${emoji('people', '👥', 48)}</div>
<div style="font-size:15px;font-weight:700;color:#f8fafc;margin-bottom:6px;">لا يوجد أصدقاء بعد</div>
<div style="font-size:12px;color:#64748b;margin-bottom:16px;">أضف أصدقاء أولاً لتتمكن من تحديهم</div>
<button class="btn btn-primary" id="cf-go-social" style="font-size:13px;padding:10px 24px;">${emoji('search_icon', '🔍', 13)} ابحث عن لاعبين</button>
</div>`;
body.querySelector('#cf-go-social')?.addEventListener('click', () => scene.push('friends'));
return;
}
const online = friends.filter(f => f.is_online);
const offline = friends.filter(f => !f.is_online);
let html = '';
if (online.length > 0) {
html += `<div class="cf-section-title">${emoji('green_circle', '🟢', 11)} متصلين الآن (${online.length})</div>`;
html += online.map(f => renderChallengeCard(f, true)).join('');
}
if (offline.length > 0) {
html += `<div class="cf-section-title" style="margin-top:${online.length > 0 ? '16px' : '0'};">${emoji('gray_circle', '⚪', 11)} غير متصلين</div>`;
html += offline.map(f => renderChallengeCard(f, false)).join('');
}
body.innerHTML = html;
// Bind challenge buttons
body.querySelectorAll('[data-challenge]').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
audio.play('click');
juice.hapticLight();
const uid = btn.dataset.challenge;
const card = btn.closest('.cf-card');
const name = card?.querySelector('.cf-card-name')?.textContent || 'صديق';
const friend = friends.find(f => f.id === uid);
showChallengeOptions(el, uid, name, friend);
});
});
// Bind card tap -> open chat
body.querySelectorAll('.cf-card').forEach(card => {
card.addEventListener('click', () => {
const uid = card.dataset.uid;
const friend = friends.find(f => f.id === uid);
if (friend) {
audio.play('click');
scene.push('friend-chat', { friendId: uid, profile: friend });
}
});
});
} catch (e) {
body.innerHTML = `<div style="text-align:center;color:#ef4444;padding:24px;">فشل التحميل — <button id="cf-retry" style="color:#3b82f6;background:none;border:none;text-decoration:underline;cursor:pointer;font-family:inherit;">حاول مرة أخرى</button></div>`;
body.querySelector('#cf-retry')?.addEventListener('click', () => loadFriendsForChallenge(el));
}
}
function renderChallengeCard(f, isOnline) {
return `
<div class="cf-card" data-uid="${f.id}">
<div class="cf-card-avatar">
${f.avatar_url ? `<img src="${f.avatar_url}">` : emoji('person', '👤', 22)}
${isOnline ? '<div class="cf-card-online"></div>' : ''}
</div>
<div class="cf-card-info">
<div class="cf-card-name">${f.display_name || f.username || 'لاعب'}</div>
<div class="cf-card-sub">${isOnline ? 'متصل الآن' : 'غير متصل'}${f.level ? ` • مستوى ${f.level}` : ''}</div>
</div>
<button class="cf-card-action" data-challenge="${f.id}">${emoji('challenge_swords', '⚔️', 12)} تحدّي</button>
</div>
`;
}
function showChallengeOptions(el, targetId, targetName, friendProfile) {
const existing = document.getElementById('cf-options-dialog');
if (existing) existing.remove();
const dialog = document.createElement('div');
dialog.id = 'cf-options-dialog';
dialog.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:999;display:flex;align-items:flex-end;justify-content:center;padding:0;';
dialog.innerHTML = `
<div style="background:#1a1a2e;border-radius:20px 20px 0 0;padding:24px 20px;width:100%;max-width:400px;animation:slideUp 0.25s ease;">
<div style="width:40px;height:4px;background:rgba(255,255,255,0.15);border-radius:2px;margin:0 auto 16px;"></div>
<div style="text-align:center;margin-bottom:16px;">
<div style="font-size:15px;font-weight:700;color:#f8fafc;">تحدّي ${targetName}</div>
<div style="font-size:12px;color:#64748b;margin-top:4px;">اختر اللعبة ونوع الوقت</div>
</div>
<!-- Game selection -->
<div style="display:flex;gap:8px;margin-bottom:14px;">
<button class="cfo-game active" data-game="chess" style="flex:1;padding:12px;border-radius:12px;background:#2563EB;border:2px solid #2563EB;color:#fff;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;transition:all 0.15s;">♟ شطرنج</button>
<button class="cfo-game" data-game="ludo" style="flex:1;padding:12px;border-radius:12px;background:#1a1a2e;border:2px solid rgba(255,255,255,0.08);color:#94a3b8;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;transition:all 0.15s;">🎲 لودو</button>
</div>
<!-- Time control -->
<div id="cfo-time" style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:20px;">
<button class="cfo-tc" data-tc="bullet_1_0">⚡ 1 د</button>
<button class="cfo-tc active" data-tc="blitz_3_0">🔥 3 د</button>
<button class="cfo-tc" data-tc="blitz_5_0">💨 5 د</button>
<button class="cfo-tc" data-tc="rapid_10_0">🕐 10 د</button>
</div>
<button class="btn btn-primary" id="cfo-send" style="width:100%;min-height:50px;font-size:15px;font-weight:700;border-radius:14px;">⚔️ أرسل التحدي</button>
<button id="cfo-cancel" style="width:100%;margin-top:10px;background:none;border:none;color:#64748b;font-size:13px;cursor:pointer;font-family:inherit;padding:10px;">إلغاء</button>
</div>
<style>
@keyframes slideUp { from{transform:translateY(100%)}to{transform:none} }
.cfo-tc { padding:10px 8px;border-radius:10px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.08);color:#94a3b8;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit;transition:all 0.15s; }
.cfo-tc.active { background:#E4AC38;border-color:#E4AC38;color:#1a1a1a; }
.cfo-tc:active { transform:scale(0.93); }
</style>
`;
document.body.appendChild(dialog);
let selectedGame = 'chess';
let selectedTc = 'blitz_3_0';
dialog.querySelectorAll('.cfo-game').forEach(btn => {
btn.addEventListener('click', () => {
dialog.querySelectorAll('.cfo-game').forEach(b => { b.style.background = '#1a1a2e'; b.style.borderColor = 'rgba(255,255,255,0.08)'; b.style.color = '#94a3b8'; b.classList.remove('active'); });
btn.style.background = '#2563EB'; btn.style.borderColor = '#2563EB'; btn.style.color = '#fff'; btn.classList.add('active');
selectedGame = btn.dataset.game;
dialog.querySelector('#cfo-time').style.display = selectedGame === 'ludo' ? 'none' : 'grid';
});
});
dialog.querySelectorAll('.cfo-tc').forEach(btn => {
btn.addEventListener('click', () => {
dialog.querySelectorAll('.cfo-tc').forEach(b => { b.classList.remove('active'); b.style.background = 'rgba(255,255,255,0.05)'; b.style.borderColor = 'rgba(255,255,255,0.08)'; b.style.color = '#94a3b8'; });
btn.classList.add('active'); btn.style.background = '#E4AC38'; btn.style.borderColor = '#E4AC38'; btn.style.color = '#1a1a1a';
selectedTc = btn.dataset.tc;
});
});
dialog.querySelector('#cfo-send').addEventListener('click', async () => {
const sendBtn = dialog.querySelector('#cfo-send');
sendBtn.disabled = true;
sendBtn.textContent = '⏳ جاري الإرسال...';
try {
const res = await net.post('friends.php', {
action: 'invite',
target_id: targetId,
game_key: selectedGame,
time_control: selectedTc
});
if (res.error) { sendBtn.textContent = res.error; sendBtn.disabled = false; return; }
// Also send a chat message
net.post('chat.php', {
action: 'send',
friend_id: targetId,
content: `أرسل تحدي ${selectedGame === 'ludo' ? 'لودو' : 'شطرنج'}`,
message_type: 'invite',
metadata: { game_key: selectedGame, time_control: selectedTc, match_id: res.match_id }
}).catch(() => {});
audio.play('reward');
juice.hapticSuccess();
dialog.remove();
// Navigate to lobby
scene.push('game-lobby', {
matchId: res.match_id,
color: res.color,
gameKey: selectedGame,
timeControl: selectedTc,
friendId: targetId,
friendProfile: friendProfile || {},
isHost: true
});
} catch (e) {
sendBtn.textContent = 'فشل — حاول مرة أخرى';
sendBtn.disabled = false;
}
});
dialog.querySelector('#cfo-cancel').addEventListener('click', () => dialog.remove());
dialog.addEventListener('click', (e) => { if (e.target === dialog) dialog.remove(); });
}
import * as net from '../../../core/net.js';
import * as scene from '../../../core/scene.js';
import * as audio from '../../../core/audio.js';
import * as store from '../../../core/store.js';
import * as juice from '../../../core/juice.js';
import { emoji } from '../../../core/theme.js';
let pollTimer = null;
let matchId = null;
let lobbyState = 'waiting'; // waiting | ready | starting
export function mountLobby(el, params = {}) {
matchId = params.matchId;
const color = params.color;
const gameKey = params.gameKey || 'chess';
const timeControl = params.timeControl || 'rapid_10_0';
const friendProfile = params.friendProfile || {};
const isHost = params.isHost ?? true;
lobbyState = isHost ? 'waiting' : 'ready';
if (pollTimer) clearInterval(pollTimer);
const myName = store.get('player.display_name') || store.get('player.username') || 'أنت';
const myAvatar = store.get('player.avatar_url');
const friendName = friendProfile?.display_name || friendProfile?.username || 'الخصم';
const friendAvatar = friendProfile?.avatar_url;
const tcLabel = formatTimeControl(timeControl);
const gameLabel = gameKey === 'ludo' ? 'لودو' : gameKey === 'domino' ? 'دومينو' : 'شطرنج';
const gameIcon = gameKey === 'ludo' ? '🎲' : gameKey === 'domino' ? '🁣' : '♟';
el.innerHTML = `
<div class="lobby-layout">
<!-- Header -->
<div class="lobby-header">
<button id="lobby-back" class="lobby-back-btn">←</button>
<div class="lobby-title">${emoji('challenge_swords', '⚔️', 18)} غرفة التحدي</div>
</div>
<!-- Match Info -->
<div class="lobby-match-info">
<div class="lobby-game-badge" style="background:${gameKey === 'chess' ? '#2563EB' : gameKey === 'ludo' ? '#8B5CF6' : '#10B981'};">
<span style="font-size:20px;">${gameIcon}</span>
<span style="font-size:13px;font-weight:600;">${gameLabel}</span>
</div>
${gameKey !== 'ludo' ? `<div class="lobby-time-badge">${emoji('clock', '⏱️', 13)} ${tcLabel}</div>` : ''}
</div>
<!-- Players -->
<div class="lobby-players">
<!-- Host / You -->
<div class="lobby-player-card">
<div class="lobby-avatar ${isHost ? 'host' : ''}">
${myAvatar ? `<img src="${myAvatar}" style="width:100%;height:100%;object-fit:cover;border-radius:50%;">` : emoji('person', '👤', 28)}
</div>
<div class="lobby-player-name">${myName}</div>
<div class="lobby-player-status ready">${emoji('check', '✓', 11)} جاهز</div>
${color ? `<div class="lobby-color" style="background:${color === 'w' ? '#fff' : '#1a1a1a'};border:2px solid ${color === 'w' ? '#e2e8f0' : '#475569'};width:20px;height:20px;border-radius:50%;margin-top:6px;"></div>` : ''}
</div>
<!-- VS divider -->
<div class="lobby-vs">
<div class="lobby-vs-circle">VS</div>
</div>
<!-- Opponent -->
<div class="lobby-player-card">
<div class="lobby-avatar opponent" id="lobby-opponent-avatar">
${friendAvatar ? `<img src="${friendAvatar}" style="width:100%;height:100%;object-fit:cover;border-radius:50%;">` : `<div class="lobby-waiting-pulse">${emoji('hourglass', '⏳', 28)}</div>`}
</div>
<div class="lobby-player-name" id="lobby-opponent-name">${isHost ? (friendName || 'في الانتظار...') : friendName}</div>
<div class="lobby-player-status" id="lobby-opponent-status">${isHost ? 'في انتظار القبول...' : `${emoji('check', '✓', 11)} جاهز`}</div>
${color ? `<div class="lobby-color" style="background:${color === 'w' ? '#1a1a1a' : '#fff'};border:2px solid ${color === 'w' ? '#475569' : '#e2e8f0'};width:20px;height:20px;border-radius:50%;margin-top:6px;"></div>` : ''}
</div>
</div>
<!-- Status -->
<div class="lobby-status" id="lobby-status">
${isHost ? `<div class="lobby-status-text">${emoji('hourglass', '⏳', 14)} في انتظار الخصم...</div><div class="lobby-status-sub">سيتم بدء المباراة تلقائياً عند قبول التحدي</div>` : `<div class="lobby-status-text" style="color:#34D399;">${emoji('check', '✓', 14)} جاهز للبدء!</div>`}
</div>
<!-- Actions -->
<div class="lobby-actions">
${!isHost ? `<button class="btn btn-primary lobby-btn" id="lobby-start" style="background:#34D399;">${emoji('play', '▶', 14)} ابدأ المباراة</button>` : ''}
<button class="btn btn-secondary lobby-btn" id="lobby-cancel">${emoji('exit', '✕', 12)} إلغاء</button>
</div>
</div>
<style>
.lobby-layout { display:flex;flex-direction:column;align-items:center;height:100%;background:#0a0a14;padding:0; }
.lobby-header { display:flex;align-items:center;gap:12px;padding:12px 16px;width:100%;background:#0f0f1e;border-bottom:1px solid rgba(255,255,255,0.06); }
.lobby-back-btn { background:none;border:none;color:#94a3b8;font-size:20px;cursor:pointer;padding:4px 8px; }
.lobby-title { font-size:16px;font-weight:700;color:#f8fafc; }
.lobby-match-info { display:flex;gap:10px;align-items:center;justify-content:center;padding:16px;width:100%; }
.lobby-game-badge { display:flex;align-items:center;gap:6px;padding:8px 16px;border-radius:12px;color:#fff; }
.lobby-time-badge { display:flex;align-items:center;gap:4px;padding:8px 14px;border-radius:10px;background:#1a1a2e;border:1px solid rgba(255,255,255,0.08);color:#e2e8f0;font-size:13px;font-weight:600; }
.lobby-players { display:flex;align-items:center;justify-content:center;gap:16px;padding:20px 16px;width:100%; }
.lobby-player-card { display:flex;flex-direction:column;align-items:center;gap:8px;flex:1;max-width:140px; }
.lobby-avatar { width:72px;height:72px;border-radius:50%;background:#2a2a4a;display:flex;align-items:center;justify-content:center;overflow:hidden;border:3px solid rgba(255,255,255,0.1); }
.lobby-avatar.host { border-color:#E4AC38; }
.lobby-avatar.opponent { border-color:#3B82F6; }
.lobby-player-name { font-size:14px;font-weight:600;color:#f8fafc;text-align:center;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; }
.lobby-player-status { font-size:11px;color:#64748b;text-align:center; }
.lobby-player-status.ready { color:#34D399; }
.lobby-vs { display:flex;align-items:center; }
.lobby-vs-circle { width:40px;height:40px;border-radius:50%;background:linear-gradient(135deg,#E4AC38,#F59E0B);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:800;color:#1a1a1a; }
.lobby-status { text-align:center;padding:16px 24px;width:100%; }
.lobby-status-text { font-size:14px;font-weight:600;color:#E4AC38;margin-bottom:4px; }
.lobby-status-sub { font-size:12px;color:#64748b; }
.lobby-actions { display:flex;flex-direction:column;gap:10px;padding:16px 24px;width:100%;max-width:320px;margin-top:auto;padding-bottom:max(16px, env(safe-area-inset-bottom, 0px)); }
.lobby-btn { width:100%;min-height:48px;font-size:14px;font-weight:600;border-radius:12px; }
.lobby-waiting-pulse { animation:lobbyPulse 2s infinite; }
@keyframes lobbyPulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:0.5;transform:scale(0.9)} }
</style>
`;
// Bind events
el.querySelector('#lobby-back').addEventListener('click', () => cancelAndLeave(el));
el.querySelector('#lobby-cancel').addEventListener('click', () => cancelAndLeave(el));
const startBtn = el.querySelector('#lobby-start');
if (startBtn) {
startBtn.addEventListener('click', () => startGame(el, params));
}
// If host, poll for opponent acceptance
if (isHost) {
pollTimer = setInterval(() => pollMatchStatus(el, params), 2000);
} else {
// Guest already accepted — can start immediately
startGame(el, params);
}
}
async function pollMatchStatus(el, params) {
if (!matchId) return;
try {
const res = await net.post('game.php', { action: 'get', match_id: matchId });
if (!res || res.error) return;
if (res.status === 'in_progress') {
// Opponent accepted!
clearInterval(pollTimer);
pollTimer = null;
lobbyState = 'starting';
const statusEl = el.querySelector('#lobby-status');
const oppStatus = el.querySelector('#lobby-opponent-status');
if (statusEl) {
statusEl.innerHTML = `<div class="lobby-status-text" style="color:#34D399;">${emoji('check', '✓', 14)} الخصم قبل! جاري البدء...</div>`;
}
if (oppStatus) {
oppStatus.innerHTML = `<span style="color:#34D399;">${emoji('check', '✓', 11)} جاهز</span>`;
oppStatus.classList.add('ready');
}
audio.play('reward');
juice.hapticSuccess();
setTimeout(() => startGame(el, params), 1500);
}
} catch (e) {}
}
function startGame(el, params) {
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
const gameKey = params.gameKey || 'chess';
if (gameKey === 'chess') {
scene.replace('chess-game', {
mode: 'live',
matchId: params.matchId,
color: params.color,
timeControl: params.timeControl,
isFriendly: true
});
} else if (gameKey === 'ludo') {
scene.replace('ludo-game', {
mode: 'live',
matchId: params.matchId,
isFriendly: true
});
}
}
async function cancelAndLeave(el) {
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
if (matchId && lobbyState === 'waiting') {
try {
await net.post('friends.php', { action: 'decline-invite', match_id: matchId });
} catch (e) {}
}
audio.play('click');
scene.pop();
}
function formatTimeControl(tc) {
if (tc.includes('bullet_1')) return '1 دقيقة';
if (tc.includes('blitz_3')) return '3 دقائق';
if (tc.includes('blitz_5')) return '5 دقائق';
if (tc.includes('rapid_10')) return '10 دقائق';
if (tc.includes('rapid_15')) return '15 دقيقة';
if (tc.includes('classical')) return '30 دقيقة';
return tc;
}
export function unmountLobby() {
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
matchId = null;
}
......@@ -39,9 +39,9 @@ export function mountTable(el) {
<!-- Quick actions row -->
<div id="daily-widget" style="display:flex;gap:8px;width:100%;max-width:340px;margin-bottom:16px;">
<button class="quick-btn" id="btn-challenges">
<span class="qb-icon" style="background:linear-gradient(135deg,#1e40af,#3b82f6);">${emoji('lightning', '⚡', 20)}</span>
<span class="qb-label">تحديات</span>
<button class="quick-btn" id="btn-challenge-friend">
<span class="qb-icon" style="background:linear-gradient(135deg,#7c3aed,#a855f7);">${emoji('challenge_swords', '⚔️', 20)}</span>
<span class="qb-label">تحدّي صديق</span>
</button>
<button class="quick-btn" id="btn-achievements">
<span class="qb-icon" style="background:linear-gradient(135deg,#854d0e,#ca8a04);">${emoji('trophy', '🏆', 20)}</span>
......@@ -228,9 +228,9 @@ export function mountTable(el) {
const menu = el.querySelector('#game-menu');
// Daily widget buttons
el.querySelector('#btn-challenges')?.addEventListener('click', () => {
el.querySelector('#btn-challenge-friend')?.addEventListener('click', () => {
audio.play('click');
scene.push('daily-challenges');
scene.push('challenge-friend');
});
el.querySelector('#btn-achievements')?.addEventListener('click', () => {
audio.play('click');
......
......@@ -2,7 +2,9 @@ import * as scene from '../../core/scene.js';
import { mountFriends } from './scenes/friends.js';
import { mountNotifications } from './scenes/notifications.js';
import { mountActivity } from './scenes/activity.js';
import { mountChat } from './scenes/chat.js';
scene.register('friends', mountFriends);
scene.register('notifications', mountNotifications);
scene.register('activity-feed', mountActivity);
scene.register('friend-chat', mountChat);
import * as net from '../../../core/net.js';
import * as scene from '../../../core/scene.js';
import * as audio from '../../../core/audio.js';
import * as store from '../../../core/store.js';
import * as juice from '../../../core/juice.js';
import { emoji } from '../../../core/theme.js';
let pollTimer = null;
let friendId = null;
let friendProfile = null;
let messages = [];
let isLoading = false;
export function mountChat(el, params = {}) {
friendId = params.friendId;
friendProfile = params.profile || null;
messages = [];
isLoading = false;
if (pollTimer) clearInterval(pollTimer);
const name = friendProfile?.display_name || friendProfile?.username || 'صديق';
const avatar = friendProfile?.avatar_url;
el.innerHTML = `
<div class="chat-layout">
<!-- Header -->
<div class="chat-header">
<button id="chat-back" class="chat-back-btn">→</button>
<div class="chat-header-avatar">
${avatar ? `<img src="${avatar}" style="width:100%;height:100%;object-fit:cover;border-radius:50%;">` : emoji('person', '👤', 16)}
</div>
<div class="chat-header-info">
<div class="chat-header-name">${name}</div>
<div class="chat-header-status" id="chat-status">${friendProfile?.is_online ? 'متصل الآن' : 'غير متصل'}</div>
</div>
<button id="chat-invite" class="chat-action-btn">${emoji('challenge_swords', '⚔️', 16)}</button>
<button id="chat-profile" class="chat-action-btn">${emoji('person', '👤', 14)}</button>
</div>
<!-- Messages -->
<div class="chat-messages" id="chat-messages">
<div class="chat-loading" id="chat-loading">${emoji('loading', '⏳', 16)} جاري التحميل...</div>
</div>
<!-- Input -->
<div class="chat-input-bar">
<input type="text" id="chat-input" class="chat-input" placeholder="اكتب رسالة..." maxlength="500" autocomplete="off">
<button id="chat-send" class="chat-send-btn">${emoji('send', '📤', 18)}</button>
</div>
</div>
<style>
.chat-layout { display:flex;flex-direction:column;height:100%;background:#0a0a14; }
.chat-header { display:flex;align-items:center;gap:10px;padding:10px 14px;background:#0f0f1e;border-bottom:1px solid rgba(255,255,255,0.06); }
.chat-back-btn { background:none;border:none;color:#94a3b8;font-size:20px;cursor:pointer;padding:4px 8px;font-family:inherit; }
.chat-header-avatar { width:36px;height:36px;border-radius:50%;background:#2a2a4a;display:flex;align-items:center;justify-content:center;overflow:hidden;flex-shrink:0; }
.chat-header-info { flex:1;min-width:0; }
.chat-header-name { font-size:14px;font-weight:600;color:#f8fafc;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; }
.chat-header-status { font-size:11px;color:#64748b; }
.chat-action-btn { width:36px;height:36px;border-radius:50%;background:#1a1a2e;border:1px solid rgba(255,255,255,0.08);display:flex;align-items:center;justify-content:center;cursor:pointer;transition:transform 0.1s; }
.chat-action-btn:active { transform:scale(0.9); }
.chat-messages { flex:1;overflow-y:auto;padding:12px 14px;display:flex;flex-direction:column;gap:6px; }
.chat-loading { text-align:center;color:#64748b;font-size:13px;padding:24px; }
.chat-bubble { max-width:80%;padding:10px 14px;border-radius:16px;font-size:13px;line-height:1.5;word-wrap:break-word;animation:bubbleIn 0.2s ease; }
.chat-bubble.mine { align-self:flex-end;background:#2563EB;color:#fff;border-bottom-left-radius:16px;border-bottom-right-radius:4px; }
.chat-bubble.theirs { align-self:flex-start;background:#1a1a2e;color:#e2e8f0;border-bottom-left-radius:4px;border-bottom-right-radius:16px; }
.chat-bubble.system { align-self:center;background:rgba(228,172,56,0.1);color:#E4AC38;font-size:12px;border-radius:12px;padding:6px 14px; }
.chat-bubble .chat-time { font-size:10px;opacity:0.6;margin-top:2px; }
.chat-bubble.mine .chat-time { text-align:left; }
.chat-bubble.theirs .chat-time { text-align:right; }
.chat-input-bar { display:flex;gap:8px;padding:10px 14px;background:#0f0f1e;border-top:1px solid rgba(255,255,255,0.06);padding-bottom:max(10px, env(safe-area-inset-bottom, 0px)); }
.chat-input { flex:1;background:#1a1a2e;border:1px solid rgba(255,255,255,0.08);border-radius:20px;padding:10px 16px;color:#f8fafc;font-size:14px;font-family:inherit;outline:none;transition:border-color 0.2s; }
.chat-input:focus { border-color:rgba(37,99,235,0.5); }
.chat-send-btn { width:44px;height:44px;border-radius:50%;background:#2563EB;border:none;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:transform 0.1s,opacity 0.2s; }
.chat-send-btn:active { transform:scale(0.9); }
.chat-send-btn:disabled { opacity:0.4;cursor:default; }
.chat-day-divider { text-align:center;font-size:11px;color:#475569;padding:8px 0;position:relative; }
.chat-day-divider::before,.chat-day-divider::after { content:'';position:absolute;top:50%;width:30%;height:1px;background:rgba(255,255,255,0.06); }
.chat-day-divider::before { right:0; }
.chat-day-divider::after { left:0; }
@keyframes bubbleIn { from{opacity:0;transform:translateY(8px)scale(0.95)}to{opacity:1;transform:none} }
</style>
`;
// Bind events
el.querySelector('#chat-back').addEventListener('click', () => {
cleanup();
scene.pop();
});
el.querySelector('#chat-invite').addEventListener('click', () => {
audio.play('click');
showInviteFromChat(el);
});
el.querySelector('#chat-profile').addEventListener('click', () => {
audio.play('click');
scene.push('profile-view', { playerId: friendId });
});
const input = el.querySelector('#chat-input');
const sendBtn = el.querySelector('#chat-send');
sendBtn.addEventListener('click', () => sendMessage(el));
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage(el);
}
});
// Load history
loadMessages(el);
// Poll for new messages every 3s
pollTimer = setInterval(() => pollNewMessages(el), 3000);
}
async function loadMessages(el) {
try {
const data = await net.get('chat.php', { action: 'history', friend_id: friendId });
messages = data.messages || [];
renderMessages(el);
} catch (e) {
const container = el.querySelector('#chat-messages');
container.innerHTML = '<div class="chat-loading" style="color:#ef4444;">فشل تحميل الرسائل</div>';
}
}
async function pollNewMessages(el) {
if (isLoading || !friendId) return;
try {
const lastTime = messages.length > 0 ? messages[messages.length - 1].created_at : null;
const data = await net.get('chat.php', { action: 'history', friend_id: friendId, limit: 20 });
const newMsgs = data.messages || [];
if (newMsgs.length > messages.length) {
const oldIds = new Set(messages.map(m => m.id));
const fresh = newMsgs.filter(m => !oldIds.has(m.id));
if (fresh.length > 0) {
messages = newMsgs;
renderMessages(el, true);
if (fresh.some(m => m.sender_id !== store.get('auth.userId'))) {
audio.play('notification');
}
}
}
} catch (e) {}
}
async function sendMessage(el) {
const input = el.querySelector('#chat-input');
const content = input.value.trim();
if (!content || isLoading) return;
isLoading = true;
input.value = '';
try {
const res = await net.post('chat.php', {
action: 'send',
friend_id: friendId,
content
});
if (res.error) {
input.value = content;
return;
}
const msg = res.message;
if (msg && msg.id) {
messages.push(msg);
} else {
messages.push({
id: 'temp-' + Date.now(),
sender_id: store.get('auth.userId'),
content,
message_type: 'text',
created_at: new Date().toISOString()
});
}
renderMessages(el, true);
audio.play('click');
juice.hapticLight();
} catch (e) {
input.value = content;
} finally {
isLoading = false;
}
}
function renderMessages(el, scrollToBottom = false) {
const container = el.querySelector('#chat-messages');
if (!container) return;
const myId = store.get('auth.userId');
if (messages.length === 0) {
container.innerHTML = `
<div style="flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;opacity:0.6;">
<div style="font-size:32px;">${emoji('wave', '👋', 32)}</div>
<div style="font-size:13px;color:#64748b;">ابدأ المحادثة مع صديقك!</div>
</div>
`;
return;
}
let html = '';
let lastDate = '';
for (const msg of messages) {
const msgDate = new Date(msg.created_at).toLocaleDateString('ar-EG');
if (msgDate !== lastDate) {
lastDate = msgDate;
html += `<div class="chat-day-divider">${msgDate}</div>`;
}
const isMine = msg.sender_id === myId;
const time = new Date(msg.created_at).toLocaleTimeString('ar-EG', { hour: '2-digit', minute: '2-digit' });
if (msg.message_type === 'system') {
html += `<div class="chat-bubble system">${escapeHtml(msg.content)}</div>`;
} else if (msg.message_type === 'invite') {
const meta = typeof msg.metadata === 'string' ? JSON.parse(msg.metadata || '{}') : (msg.metadata || {});
const gameLabel = meta.game_key === 'ludo' ? '🎲 لودو' : '♟ شطرنج';
html += `
<div class="chat-bubble ${isMine ? 'mine' : 'theirs'}" style="background:${isMine ? 'rgba(37,99,235,0.3)' : 'rgba(228,172,56,0.15)'};border:1px solid ${isMine ? 'rgba(37,99,235,0.3)' : 'rgba(228,172,56,0.3)'};">
<div style="font-size:12px;font-weight:600;margin-bottom:4px;">${emoji('challenge_swords', '⚔️', 13)} تحدي ${gameLabel}</div>
<div style="font-size:12px;opacity:0.8;">${escapeHtml(msg.content)}</div>
<div class="chat-time">${time}</div>
</div>
`;
} else {
html += `
<div class="chat-bubble ${isMine ? 'mine' : 'theirs'}">
${escapeHtml(msg.content)}
<div class="chat-time">${time}</div>
</div>
`;
}
}
container.innerHTML = html;
if (scrollToBottom || true) {
container.scrollTop = container.scrollHeight;
}
}
function showInviteFromChat(el) {
const existing = document.getElementById('chat-invite-dialog');
if (existing) { existing.remove(); return; }
const name = friendProfile?.display_name || friendProfile?.username || 'صديق';
const dialog = document.createElement('div');
dialog.id = 'chat-invite-dialog';
dialog.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:999;display:flex;align-items:center;justify-content:center;padding:24px;';
dialog.innerHTML = `
<div style="background:#1a1a2e;border-radius:16px;padding:24px;width:100%;max-width:300px;text-align:center;">
<div style="font-size:24px;margin-bottom:8px;">${emoji('challenge_swords', '⚔️', 24)}</div>
<div style="font-size:15px;font-weight:700;color:#f8fafc;margin-bottom:4px;">تحدّي ${name}</div>
<div style="font-size:12px;color:#64748b;margin-bottom:16px;">اختر اللعبة</div>
<div style="display:flex;gap:8px;justify-content:center;margin-bottom:12px;">
<button class="cig active" data-game="chess" style="flex:1;padding:10px;border-radius:10px;background:#2563EB;border:2px solid #2563EB;color:#fff;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;">♟ شطرنج</button>
<button class="cig" data-game="ludo" style="flex:1;padding:10px;border-radius:10px;background:#1a1a2e;border:2px solid rgba(255,255,255,0.08);color:#94a3b8;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;">🎲 لودو</button>
</div>
<div id="cig-time" style="display:flex;gap:6px;justify-content:center;margin-bottom:16px;flex-wrap:wrap;">
<button class="cit" data-tc="bullet_1_0" style="padding:6px 10px;border-radius:8px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.08);color:#94a3b8;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;">1 د</button>
<button class="cit active" data-tc="blitz_3_0" style="padding:6px 10px;border-radius:8px;background:#E4AC38;border:none;color:#1a1a1a;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;">3 د</button>
<button class="cit" data-tc="blitz_5_0" style="padding:6px 10px;border-radius:8px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.08);color:#94a3b8;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;">5 د</button>
<button class="cit" data-tc="rapid_10_0" style="padding:6px 10px;border-radius:8px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.08);color:#94a3b8;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;">10 د</button>
</div>
<button class="btn btn-primary" id="cig-send" style="width:100%;font-size:14px;padding:12px;">أرسل التحدي</button>
<button id="cig-cancel" style="margin-top:8px;background:none;border:none;color:#64748b;font-size:12px;cursor:pointer;font-family:inherit;">إلغاء</button>
</div>
`;
document.body.appendChild(dialog);
let selectedGame = 'chess';
let selectedTc = 'blitz_3_0';
dialog.querySelectorAll('.cig').forEach(btn => {
btn.addEventListener('click', () => {
dialog.querySelectorAll('.cig').forEach(b => { b.style.background = '#1a1a2e'; b.style.borderColor = 'rgba(255,255,255,0.08)'; b.style.color = '#94a3b8'; b.classList.remove('active'); });
btn.style.background = '#2563EB'; btn.style.borderColor = '#2563EB'; btn.style.color = '#fff'; btn.classList.add('active');
selectedGame = btn.dataset.game;
dialog.querySelector('#cig-time').style.display = selectedGame === 'ludo' ? 'none' : 'flex';
});
});
dialog.querySelectorAll('.cit').forEach(btn => {
btn.addEventListener('click', () => {
dialog.querySelectorAll('.cit').forEach(b => { b.style.background = 'rgba(255,255,255,0.05)'; b.style.border = '1px solid rgba(255,255,255,0.08)'; b.style.color = '#94a3b8'; b.classList.remove('active'); });
btn.style.background = '#E4AC38'; btn.style.border = 'none'; btn.style.color = '#1a1a1a'; btn.classList.add('active');
selectedTc = btn.dataset.tc;
});
});
dialog.querySelector('#cig-send').addEventListener('click', async () => {
const sendBtn = dialog.querySelector('#cig-send');
sendBtn.disabled = true;
sendBtn.textContent = 'جاري الإرسال...';
try {
const res = await net.post('friends.php', {
action: 'invite',
target_id: friendId,
game_key: selectedGame,
time_control: selectedTc
});
if (res.error) { sendBtn.textContent = res.error; sendBtn.disabled = false; return; }
// Send a chat message about the invite
await net.post('chat.php', {
action: 'send',
friend_id: friendId,
content: `أرسل تحدي ${selectedGame === 'ludo' ? 'لودو' : 'شطرنج'}`,
message_type: 'invite',
metadata: { game_key: selectedGame, time_control: selectedTc, match_id: res.match_id }
});
audio.play('reward');
juice.hapticSuccess();
dialog.remove();
// Navigate to lobby
scene.push('game-lobby', {
matchId: res.match_id,
color: res.color,
gameKey: selectedGame,
timeControl: selectedTc,
friendId,
friendProfile,
isHost: true
});
} catch (e) {
sendBtn.textContent = 'فشل — حاول مرة أخرى';
sendBtn.disabled = false;
}
});
dialog.querySelector('#cig-cancel').addEventListener('click', () => dialog.remove());
dialog.addEventListener('click', (e) => { if (e.target === dialog) dialog.remove(); });
}
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function cleanup() {
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
friendId = null;
friendProfile = null;
messages = [];
}
export function unmountChat() {
cleanup();
}
......@@ -124,14 +124,15 @@ async function checkInvites(el) {
if (res.error) { btn.textContent = 'خطأ'; return; }
audio.play('reward');
juice.hapticSuccess();
// Navigate to game
// Navigate to lobby then game
const inv = invites.find(i => i.match_id === btn.dataset.acceptInvite);
scene.push('chess-game', {
mode: 'live',
scene.push('game-lobby', {
matchId: res.match_id,
color: res.color,
gameKey: inv?.game_key || 'chess',
timeControl: inv?.time_control || 'rapid_10_0',
isFriendly: true
friendId: inv?.from_id,
isHost: false
});
} catch (e) {
btn.textContent = 'فشل';
......@@ -333,7 +334,7 @@ async function loadOnline(content) {
function renderFriendCard(f) {
return `
<div class="friend-card" data-uid="${f.id}">
<div class="friend-card" data-uid="${f.id}" data-profile='${JSON.stringify({id:f.id, display_name:f.display_name, username:f.username, avatar_url:f.avatar_url, level:f.level, is_online:f.is_online}).replace(/'/g, '&#39;')}'>
<div class="friend-avatar">
${f.avatar_url ? `<img src="${f.avatar_url}">` : emoji('person', '👤', 18)}
${f.is_online ? '<div class="online-dot"></div>' : ''}
......@@ -343,6 +344,7 @@ function renderFriendCard(f) {
<div style="font-size:11px;color:${f.is_online ? '#34D399' : '#64748b'};">${f.is_online ? 'متصل الآن' : 'غير متصل'}${f.level ? ` — مستوى ${f.level}` : ''}</div>
</div>
<div class="friend-actions">
<div class="friend-action" data-chat="${f.id}" title="محادثة" style="background:rgba(37,99,235,0.15);border-color:rgba(37,99,235,0.3);color:#3B82F6;">💬</div>
${f.is_online ? `<div class="friend-action" data-invite="${f.id}" title="تحدّي" style="background:rgba(228,172,56,0.15);border-color:rgba(228,172,56,0.3);color:#E4AC38;">${emoji('challenge_swords', '⚔️', 14)}</div>` : ''}
<div class="friend-action" data-remove="${f.id}" title="إزالة" style="font-size:11px;color:#64748b;">✕</div>
</div>
......@@ -351,8 +353,20 @@ function renderFriendCard(f) {
}
function bindFriendActions(content) {
content.querySelectorAll('[data-chat]').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
audio.play('click');
const card = btn.closest('.friend-card');
let profile = null;
try { profile = JSON.parse(card?.dataset?.profile || '{}'); } catch(e) {}
scene.push('friend-chat', { friendId: btn.dataset.chat, profile });
});
});
content.querySelectorAll('[data-invite]').forEach(btn => {
btn.addEventListener('click', () => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
audio.play('click');
juice.hapticLight();
const uid = btn.dataset.invite;
......@@ -473,18 +487,18 @@ function showInviteDialog(content, targetId, targetName) {
sendBtn.textContent = '✓ تم إرسال التحدي!';
sendBtn.style.background = '#34D399';
// Wait for opponent to accept — navigate to game
// Navigate to lobby
setTimeout(() => {
dialog.remove();
scene.push('chess-game', {
mode: 'live',
scene.push('game-lobby', {
matchId: res.match_id,
color: res.color,
gameKey: selectedGame,
timeControl: selectedTc,
isFriendly: true,
waitingForOpponent: true
friendId: targetId,
isHost: true
});
}, 1000);
}, 800);
} catch (e) {
sendBtn.textContent = 'فشل — حاول مرة أخرى';
sendBtn.disabled = false;
......
qr-code.png

7.66 KB

import puppeteer from 'puppeteer';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const SHOTS = path.join(__dirname, 'test-screenshots', 'swiss-flow');
const BASE_URL = 'https://el3ab-player.caprover.al-arcade.com';
const ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjE4OTM0NTYwMDB9.31PF6PvP-pSrvRuQwLFptQoejR0W1A7o53lZhEbnz84';
const AUTH_URL = 'https://safe-supabase-kong.caprover.al-arcade.com/auth/v1';
const TEST_EMAIL = 'mahmoudaglan@al-arcade.com';
const TEST_PASS = 'TestTour123!';
const TOURNAMENT_ID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
const ROUND_ID = 'a1111111-1111-1111-1111-111111111111';
const delay = (ms) => new Promise(r => setTimeout(r, ms));
let step = 0;
async function shot(page, name) {
step++;
const filename = `${String(step).padStart(2, '0')}-${name}.png`;
await page.screenshot({ path: path.join(SHOTS, filename), fullPage: true });
console.log(` 📸 ${filename}`);
}
async function getAuthToken() {
const res = await fetch(`${AUTH_URL}/token?grant_type=password`, {
method: 'POST',
headers: { 'apikey': ANON_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ email: TEST_EMAIL, password: TEST_PASS })
});
const data = await res.json();
if (!data.access_token) throw new Error('Login failed: ' + JSON.stringify(data));
return data;
}
(async () => {
console.log('🏆 Swiss Tournament UI Test\n');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
// Create screenshots directory
const { mkdirSync } = await import('fs');
mkdirSync(SHOTS, { recursive: true });
// Get auth token first
console.log('\n1️⃣ Authenticating as aglan...');
const authData = await getAuthToken();
console.log(` ✅ Logged in as ${authData.user.email} (${authData.user.id})`);
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
await page.setViewport({ width: 390, height: 844, deviceScaleFactor: 2 });
// Collect console errors
const errors = [];
page.on('console', msg => {
if (msg.type() === 'error') errors.push(msg.text());
});
page.on('pageerror', err => errors.push(err.message));
// Step 1: Load app and inject auth
console.log('\n2️⃣ Loading app and injecting auth session...');
await page.goto(BASE_URL, { waitUntil: 'networkidle2', timeout: 30000 });
await delay(1000);
// Inject the auth token into localStorage (key is 'el3ab_state')
await page.evaluate((auth) => {
const storeData = JSON.parse(localStorage.getItem('el3ab_state') || '{}');
storeData.auth = {
token: auth.access_token,
refreshToken: auth.refresh_token,
userId: auth.user.id
};
localStorage.setItem('el3ab_state', JSON.stringify(storeData));
}, authData);
// Reload to pick up the auth
await page.reload({ waitUntil: 'networkidle2', timeout: 30000 });
await delay(2000);
await shot(page, 'home-authenticated');
// Verify we're logged in
const isLoggedIn = await page.evaluate(() => {
const store = JSON.parse(localStorage.getItem('el3ab_state') || '{}');
return !!store.auth?.token;
});
console.log(` Auth status: ${isLoggedIn ? '✅ Logged in' : '❌ Not logged in'}`);
// Step 2: Navigate to Rank tab
console.log('\n3️⃣ Navigating to Rank tab...');
const rankClicked = await page.evaluate(() => {
const tab = document.querySelector('[data-world="rank"]');
if (tab) { tab.click(); return true; }
return false;
});
await delay(1500);
await shot(page, 'rank-tab');
console.log(` Rank tab clicked: ${rankClicked ? '✅' : '❌'}`);
// Step 3: Navigate to tournaments list
console.log('\n4️⃣ Opening tournaments list...');
const tourClicked = await page.evaluate(() => {
const btns = document.querySelectorAll('button, a, [data-scene]');
for (const b of btns) {
if (b.textContent.includes('بطولات') || b.dataset?.scene === 'tournaments') {
b.click();
return true;
}
}
return false;
});
if (!tourClicked) {
// Push scene directly
await page.evaluate(async () => {
const scene = await import('/public/js/core/scene.js');
scene.push('tournaments');
});
}
await delay(2000);
await shot(page, 'tournaments-list');
// Check if our tournament appears
const tourCards = await page.evaluate((tid) => {
const content = document.querySelector('#tour-content') || document.body;
const text = content.innerText;
return {
hasSwissTournament: text.includes('سويسرية تجريبية') || text.includes(tid),
cardCount: document.querySelectorAll('[data-tournament-id]').length,
allText: text.substring(0, 500)
};
}, TOURNAMENT_ID);
console.log(` Tournament cards: ${tourCards.cardCount}`);
console.log(` Swiss tournament visible: ${tourCards.hasSwissTournament ? '✅' : '⚠️ Not found in list'}`);
// Step 4: Open tournament detail directly
console.log('\n5️⃣ Opening tournament detail...');
await page.evaluate(async (tid) => {
const scene = await import('/public/js/core/scene.js');
scene.push('tournament-detail', { tournamentId: tid });
}, TOURNAMENT_ID);
await delay(2500);
await shot(page, 'tournament-detail-info');
// Check tournament loaded
const tourInfo = await page.evaluate(() => {
const title = document.querySelector('#tour-title');
const content = document.querySelector('#tour-content');
return {
title: title?.textContent || '',
hasContent: content?.children?.length > 0,
contentText: content?.innerText?.substring(0, 300) || ''
};
});
console.log(` Title: ${tourInfo.title}`);
console.log(` Content loaded: ${tourInfo.hasContent ? '✅' : '❌'}`);
// Step 5: Check Standings tab
console.log('\n6️⃣ Checking Standings tab...');
await page.evaluate(() => {
const tab = document.querySelector('[data-tab="standings"]');
if (tab) tab.click();
});
await delay(1500);
await shot(page, 'tournament-standings');
const standingsContent = await page.evaluate(() => {
return document.querySelector('#tour-content')?.innerText?.substring(0, 200) || '';
});
console.log(` Standings: ${standingsContent.substring(0, 80)}...`);
// Step 6: Check Rounds tab — this should show our pairing with Play button
console.log('\n7️⃣ Checking Rounds tab (should show our pairing)...');
await page.evaluate(() => {
const tab = document.querySelector('[data-tab="rounds"]');
if (tab) tab.click();
});
await delay(1500);
await shot(page, 'tournament-rounds');
// Click to expand the round pairings
const roundExpanded = await page.evaluate(async (roundId) => {
const roundEl = document.querySelector(`#round-pairings-${roundId}`);
if (roundEl) {
roundEl.click();
return true;
}
// Try any round pairings element
const anyRound = document.querySelector('[id^="round-pairings-"]');
if (anyRound) {
anyRound.click();
return true;
}
return false;
}, ROUND_ID);
await delay(2000);
await shot(page, 'tournament-rounds-expanded');
console.log(` Round expanded: ${roundExpanded ? '✅' : '❌'}`);
// Check if Play button appeared
const playBtnVisible = await page.evaluate(() => {
const btn = document.querySelector('.play-pairing-btn');
return btn ? { text: btn.textContent, roundId: btn.dataset.roundId, idx: btn.dataset.idx } : null;
});
console.log(` Play button: ${playBtnVisible ? `✅ "${playBtnVisible.text}" (round: ${playBtnVisible.roundId}, idx: ${playBtnVisible.idx})` : '❌ Not found'}`);
// Step 7: Check My Games tab
console.log('\n8️⃣ Checking My Games tab...');
await page.evaluate(() => {
const tab = document.querySelector('[data-tab="my-games"]');
if (tab) tab.click();
});
await delay(2000);
await shot(page, 'tournament-my-games');
const myGamesContent = await page.evaluate(() => {
const content = document.querySelector('#tour-content');
const playBtn = content?.querySelector('.play-pending-btn');
return {
text: content?.innerText?.substring(0, 300) || '',
hasPlayBtn: !!playBtn,
playBtnText: playBtn?.textContent || ''
};
});
console.log(` My Games content: ${myGamesContent.text.substring(0, 100)}`);
console.log(` Play pending button: ${myGamesContent.hasPlayBtn ? `✅ "${myGamesContent.playBtnText}"` : '❌ Not found'}`);
// Step 8: Try launching the tournament match
console.log('\n9️⃣ Launching tournament match (create-or-join)...');
// First try clicking the play button if it exists
let matchLaunched = false;
if (myGamesContent.hasPlayBtn) {
await page.evaluate(() => {
const btn = document.querySelector('.play-pending-btn');
if (btn) btn.click();
});
await delay(3000);
matchLaunched = true;
} else if (playBtnVisible) {
// Go back to rounds tab and click
await page.evaluate(() => {
const tab = document.querySelector('[data-tab="rounds"]');
if (tab) tab.click();
});
await delay(1500);
await page.evaluate(async (roundId) => {
const roundEl = document.querySelector(`#round-pairings-${roundId}`) || document.querySelector('[id^="round-pairings-"]');
if (roundEl) roundEl.click();
}, ROUND_ID);
await delay(2000);
await page.evaluate(() => {
const btn = document.querySelector('.play-pairing-btn');
if (btn) btn.click();
});
await delay(3000);
matchLaunched = true;
} else {
// Try direct API call via scene push
console.log(' No play button found, launching match directly...');
await page.evaluate(async (tournamentId, roundId) => {
const net = await import('/public/js/core/net.js');
const scene = await import('/public/js/core/scene.js');
try {
const data = await net.post('tournament-match.php', {
action: 'create-or-join',
tournament_id: tournamentId,
round_id: roundId,
pairing_index: 0
});
console.log('[test] create-or-join result:', JSON.stringify(data));
if (data.match_id) {
scene.push('chess-game', {
mode: 'live',
matchId: data.match_id,
color: data.color || 'white',
timeControl: 'blitz_5_0',
tournamentId: tournamentId,
tournamentRound: 1
});
}
} catch (e) {
console.error('[test] create-or-join error:', e.message);
}
}, TOURNAMENT_ID, ROUND_ID);
await delay(3000);
matchLaunched = true;
}
await shot(page, 'match-launched');
// Check if we're in a chess game scene
const inChessGame = await page.evaluate(() => {
const board = document.querySelector('#chess-board') || document.querySelector('[data-chess-board]') || document.querySelector('canvas');
const timer = document.querySelector('[id*="clock"]') || document.querySelector('[id*="timer"]');
return {
hasBoard: !!board,
hasTimer: !!timer,
bodyText: document.body.innerText.substring(0, 200)
};
});
console.log(` Chess board visible: ${inChessGame.hasBoard ? '✅' : '❌'}`);
console.log(` Timer visible: ${inChessGame.hasTimer ? '✅' : '❌'}`);
if (inChessGame.hasBoard) {
await shot(page, 'chess-game-tournament-mode');
// Check tournament indicator
const tourIndicator = await page.evaluate(() => {
const body = document.body.innerText;
return body.includes('جولة') || body.includes('بطولة') || body.includes('🏆');
});
console.log(` Tournament indicator: ${tourIndicator ? '✅' : '⚠️'}`);
}
// Step 9: Test the tournament toast notification
console.log('\n🔟 Testing tournament toast notification...');
await page.evaluate(async () => {
const bus = await import('/public/js/core/bus.js');
bus.emit('tournament:paired', {
tournamentId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
roundNumber: 2,
opponentId: '2c2a5770-f080-4134-8557-b24901635638',
pairingIndex: 0
});
});
await delay(1500);
await shot(page, 'toast-notification');
const toastVisible = await page.evaluate(() => {
const toasts = document.querySelectorAll('[style*="position:fixed"], [style*="position: fixed"]');
for (const t of toasts) {
if (t.textContent.includes('جولة') || t.textContent.includes('بطولة') || t.textContent.includes('العب')) {
return t.textContent.substring(0, 100);
}
}
return null;
});
console.log(` Toast: ${toastVisible ? `✅ "${toastVisible}"` : '⚠️ Not visible'}`);
// Step 10: Test HUD badge
console.log('\n1️⃣1️⃣ Testing HUD tournament badge...');
await page.evaluate(async () => {
const bus = await import('/public/js/core/bus.js');
bus.emit('tournament:pending-updated', [
{ tournament_id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', round_number: 1 }
]);
});
await delay(500);
await shot(page, 'hud-badge');
const badgeVisible = await page.evaluate(() => {
const dots = document.querySelectorAll('[style*="background"][style*="border-radius"]');
for (const d of dots) {
if (d.offsetWidth <= 10 && d.offsetHeight <= 10) return true;
}
return false;
});
console.log(` HUD badge dot: ${badgeVisible ? '✅' : '⚠️ Not detected'}`);
// Step 11: Test Live tournament scene with our tournament
console.log('\n1️⃣2️⃣ Testing Live tournament scene...');
await page.evaluate(async (tid) => {
const scene = await import('/public/js/core/scene.js');
scene.push('tournament-live', { tournamentId: tid, tournamentName: 'بطولة سويسرية تجريبية' });
}, TOURNAMENT_ID);
await delay(3000);
await shot(page, 'tournament-live');
const liveContent = await page.evaluate(() => {
return document.querySelector('#live-content')?.innerText?.substring(0, 200) || '';
});
console.log(` Live content: ${liveContent.substring(0, 80)}`);
// Final summary
console.log('\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log(`✅ ${step} screenshots saved to test-screenshots/swiss-flow/`);
console.log(`\n📊 Console Errors (${errors.length}):`);
if (errors.length > 0) {
errors.slice(0, 10).forEach(e => console.log(` ❌ ${e.substring(0, 120)}`));
if (errors.length > 10) console.log(` ... and ${errors.length - 10} more`);
} else {
console.log(' None! 🎉');
}
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
await browser.close();
})().catch(err => {
console.error('❌ Test failed:', err.message);
console.error(err.stack);
process.exit(1);
});
import puppeteer from 'puppeteer';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const SHOTS = path.join(__dirname, 'test-screenshots');
const BASE_URL = 'https://el3ab-player.caprover.al-arcade.com';
const delay = (ms) => new Promise(r => setTimeout(r, ms));
let step = 0;
async function shot(page, name) {
step++;
const filename = `${String(step).padStart(2, '0')}-${name}.png`;
await page.screenshot({ path: path.join(SHOTS, filename), fullPage: true });
console.log(`📸 ${filename}`);
}
(async () => {
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
await page.setViewport({ width: 390, height: 844, deviceScaleFactor: 2 });
console.log('🚀 Starting tournament UI test...\n');
// Step 1: Load app
console.log('1. Loading app...');
await page.goto(BASE_URL, { waitUntil: 'networkidle2', timeout: 30000 });
await delay(2000);
await shot(page, 'app-load');
// Step 2: Check if we need to login
const isLoggedIn = await page.evaluate(() => {
return !!localStorage.getItem('el3ab_store') &&
JSON.parse(localStorage.getItem('el3ab_store') || '{}')?.auth?.token;
});
if (!isLoggedIn) {
console.log('2. Need to login...');
await shot(page, 'login-screen');
// Look for login form
const loginVisible = await page.$('#login-pass');
if (!loginVisible) {
// Try navigating to login
const loginBtn = await page.$('[data-scene="auth-login"]') || await page.$('button');
if (loginBtn) {
await loginBtn.click();
await delay(1500);
await shot(page, 'login-form');
}
}
// Fill login credentials
const emailInput = await page.$('input[type="email"]') || await page.$('#login-email');
const passInput = await page.$('#login-pass') || await page.$('input[type="password"]');
if (emailInput && passInput) {
await emailInput.click({ clickCount: 3 });
await emailInput.type('test_puppet@el3ab.com');
await passInput.click({ clickCount: 3 });
await passInput.type('Test123!');
await shot(page, 'login-filled');
// Submit
const submitBtn = await page.$('#login-btn') || await page.$('button[type="submit"]');
if (submitBtn) {
await submitBtn.click();
await delay(3000);
await shot(page, 'post-login');
}
} else {
console.log(' ⚠️ Could not find login form, trying to register...');
// Try register flow
const regLink = await page.$('[data-action="register"]') || await page.$('a[href*="register"]');
if (regLink) await regLink.click();
await delay(1000);
const regEmail = await page.$('input[type="email"]');
const regPass = await page.$('input[type="password"]');
const regName = await page.$('input[name="display_name"]') || await page.$('input[placeholder*="اسم"]');
if (regEmail && regPass) {
if (regName) { await regName.click({ clickCount: 3 }); await regName.type('TestBot'); }
await regEmail.click({ clickCount: 3 });
await regEmail.type('test_puppet@el3ab.com');
await regPass.click({ clickCount: 3 });
await regPass.type('Test123!');
await shot(page, 'register-filled');
const regBtn = await page.$('button[type="submit"]');
if (regBtn) { await regBtn.click(); await delay(3000); }
await shot(page, 'post-register');
}
}
} else {
console.log('2. Already logged in ✓');
}
// Step 3: Verify we're on the home screen
await delay(1500);
await shot(page, 'home-screen');
// Step 4: Navigate to Rank tab (tournaments)
console.log('3. Navigating to Rank tab...');
const rankTab = await page.$('[data-world="rank"]');
if (rankTab) {
await rankTab.click();
await delay(1500);
await shot(page, 'rank-tab');
} else {
console.log(' ⚠️ Rank tab not found, trying alternative...');
await page.evaluate(() => {
const tabs = document.querySelectorAll('.tab-item');
tabs.forEach(t => { if (t.textContent.includes('ترتيب') || t.dataset.world === 'rank') t.click(); });
});
await delay(1500);
await shot(page, 'rank-tab-alt');
}
// Step 5: Navigate to Tournaments list
console.log('4. Looking for tournaments scene...');
// Check if there's a tournaments button/link on leaderboard
const tourBtn = await page.evaluate(() => {
const btns = document.querySelectorAll('button, a, [data-scene]');
for (const b of btns) {
if (b.textContent.includes('بطولات') || b.textContent.includes('tournament') || b.dataset?.scene === 'tournaments') {
b.click();
return true;
}
}
return false;
});
if (tourBtn) {
await delay(1500);
await shot(page, 'tournaments-list');
} else {
// Try pushing scene directly
console.log(' Pushing tournaments scene directly...');
await page.evaluate(async () => {
const scene = await import('/public/js/core/scene.js');
scene.push('tournaments');
});
await delay(1500);
await shot(page, 'tournaments-list-direct');
}
// Step 6: Check tournament list content
console.log('5. Checking tournament list content...');
const tourCards = await page.evaluate(() => {
const cards = document.querySelectorAll('[data-tournament-id], .tour-card, [style*="border-radius"]');
return cards.length;
});
console.log(` Found ${tourCards} potential tournament cards`);
await shot(page, 'tournaments-content');
// Step 7: Try to open a tournament detail (click first card or push directly)
console.log('6. Opening tournament detail...');
const openedDetail = await page.evaluate(async () => {
// First try clicking a tournament card
const cards = document.querySelectorAll('[data-tournament-id]');
if (cards.length > 0) {
cards[0].click();
return 'clicked';
}
// Try pushing directly with a known tournament or any clickable element
const clickables = document.querySelectorAll('[onclick], [style*="cursor:pointer"]');
for (const el of clickables) {
if (el.closest('#tour-content') || el.textContent.includes('بطولة')) {
el.click();
return 'clicked-alt';
}
}
return 'none';
});
console.log(` Detail open: ${openedDetail}`);
await delay(2000);
await shot(page, 'tournament-detail');
// Step 8: Check detail tabs
console.log('7. Testing tournament detail tabs...');
// Info tab (should be loaded)
await shot(page, 'detail-info-tab');
// Standings tab
const standingsTab = await page.$('[data-tab="standings"]');
if (standingsTab) {
await standingsTab.click();
await delay(1500);
await shot(page, 'detail-standings-tab');
}
// Rounds tab
const roundsTab = await page.$('[data-tab="rounds"]');
if (roundsTab) {
await roundsTab.click();
await delay(1500);
await shot(page, 'detail-rounds-tab');
// Try to expand a round
const roundEl = await page.$('[id^="round-pairings-"]');
if (roundEl) {
await roundEl.click();
await delay(1500);
await shot(page, 'detail-rounds-expanded');
}
}
// My Games tab
const myGamesTab = await page.$('[data-tab="my-games"]');
if (myGamesTab) {
await myGamesTab.click();
await delay(1500);
await shot(page, 'detail-mygames-tab');
}
// Bracket tab (if visible)
const bracketTab = await page.$('#tab-bracket');
if (bracketTab) {
const isVisible = await page.evaluate(el => el.style.display !== 'none', bracketTab);
if (isVisible) {
await bracketTab.click();
await delay(2000);
await shot(page, 'bracket-view');
// Go back
const backBtn = await page.$('#back-btn');
if (backBtn) { await backBtn.click(); await delay(1000); }
}
}
// Arena tab (if visible)
const arenaTab = await page.$('#tab-arena');
if (arenaTab) {
const isVisible = await page.evaluate(el => el.style.display !== 'none', arenaTab);
if (isVisible) {
await arenaTab.click();
await delay(2000);
await shot(page, 'arena-view');
const backBtn = await page.$('#back-btn');
if (backBtn) { await backBtn.click(); await delay(1000); }
}
}
// Step 9: Test direct scene pushes for new scenes
console.log('8. Testing new scenes directly...');
// Tournament Bracket scene
console.log(' Testing bracket scene...');
await page.evaluate(async () => {
const scene = await import('/public/js/core/scene.js');
scene.push('tournament-bracket', { tournamentId: '00000000-0000-0000-0000-000000000000' });
});
await delay(2000);
await shot(page, 'scene-bracket');
// Go back
const backBtnBracket = await page.$('#back-btn');
if (backBtnBracket) { await backBtnBracket.click(); await delay(1000); }
// Tournament Arena scene
console.log(' Testing arena scene...');
await page.evaluate(async () => {
const scene = await import('/public/js/core/scene.js');
scene.push('tournament-arena', { tournamentId: '00000000-0000-0000-0000-000000000000', tournamentName: 'أرينا تجريبية' });
});
await delay(2000);
await shot(page, 'scene-arena');
const backBtnArena = await page.$('#back-btn');
if (backBtnArena) { await backBtnArena.click(); await delay(1000); }
// Tournament Lobby scene
console.log(' Testing lobby scene...');
await page.evaluate(async () => {
const scene = await import('/public/js/core/scene.js');
scene.push('tournament-lobby', {
tournamentId: '00000000-0000-0000-0000-000000000000',
tournamentName: 'بطولة الربيع 2026',
startsAt: new Date(Date.now() + 600000).toISOString(),
timeControl: 'blitz_5_3',
format: 'swiss',
prizePool: '1000'
});
});
await delay(2000);
await shot(page, 'scene-lobby');
const backBtnLobby = await page.$('#leave-lobby');
if (backBtnLobby) { await backBtnLobby.click(); await delay(1000); }
// Tournament Live scene
console.log(' Testing live scene...');
await page.evaluate(async () => {
const scene = await import('/public/js/core/scene.js');
scene.push('tournament-live', { tournamentId: '00000000-0000-0000-0000-000000000000', tournamentName: 'بطولة مباشرة' });
});
await delay(2000);
await shot(page, 'scene-live');
const backBtnLive = await page.$('#back-btn');
if (backBtnLive) { await backBtnLive.click(); await delay(1000); }
// Step 10: Test tournament-session toast notification
console.log('9. Testing tournament toast notification...');
await page.evaluate(async () => {
const bus = await import('/public/js/core/bus.js');
bus.emit('tournament:paired', {
tournamentId: 'test-123',
roundNumber: 2,
opponentId: 'opp-456',
pairingIndex: 0
});
});
await delay(1000);
await shot(page, 'toast-notification');
// Step 11: Check HUD badge
console.log('10. Testing HUD tournament badge...');
await page.evaluate(async () => {
const bus = await import('/public/js/core/bus.js');
bus.emit('tournament:pending-updated', [{ tournament_id: '1' }, { tournament_id: '2' }]);
});
await delay(500);
await shot(page, 'hud-badge');
// Step 12: Navigate to chess game in tournament mode
console.log('11. Testing chess game in tournament mode...');
await page.evaluate(async () => {
const scene = await import('/public/js/core/scene.js');
scene.push('chess-game', {
mode: 'bot',
botId: 'amina',
timeControl: 'blitz_5_0',
tournamentId: 'test-tournament-123',
tournamentRound: 1
});
});
await delay(3000);
await shot(page, 'chess-game-tournament');
// Step 13: Final overview
console.log('12. Test complete!\n');
await shot(page, 'final-state');
// Summary
console.log('═══════════════════════════════════════');
console.log(`✅ ${step} screenshots saved to test-screenshots/`);
console.log('═══════════════════════════════════════');
await browser.close();
})().catch(err => {
console.error('❌ Test failed:', err.message);
process.exit(1);
});
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