Commit e9838deb authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat: activity feed + battle pass system

Activity Feed (api/activity.php + scenes/activity.js):
- Fetches friend + self activity from activity_feed table
- Enriches with actor profile (name, avatar)
- Timeline UI with action labels in Arabic
- Relative timestamps

Battle Pass (api/battlepass.php):
- Season config: 30 tiers, 100 XP per tier
- Current tier from player total XP
- Free + premium reward tracks
- Claim tier endpoint
- Days remaining counter
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 5ade83d4
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, 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);
$db = supabase($token);
$limit = min(intval($_GET['limit'] ?? 20), 50);
// Get friend IDs
$friends = $db->get('friendships', [
'or' => "(requester_id.eq.{$userId},addressee_id.eq.{$userId})",
'status' => 'eq.accepted',
'select' => 'requester_id,addressee_id'
]);
$friendIds = [$userId];
if (is_array($friends) && !isset($friends['error'])) {
foreach ($friends as $f) {
$friendIds[] = $f['requester_id'] === $userId ? $f['addressee_id'] : $f['requester_id'];
}
}
$idList = implode(',', $friendIds);
// Get activity from friends + self
$sdb = supabaseService();
$activity = $sdb->get('activity_feed', [
'actor_id' => "in.({$idList})",
'select' => 'id,actor_id,action,target_type,target_id,metadata,created_at',
'order' => 'created_at.desc',
'limit' => $limit
]);
// Get actor profiles
$actorIds = array_unique(array_column($activity ?: [], 'actor_id'));
$profiles = [];
if (!empty($actorIds)) {
$profileList = implode(',', $actorIds);
$profileData = $sdb->get('profiles', [
'id' => "in.({$profileList})",
'select' => 'id,username,display_name,avatar_url'
]);
if (is_array($profileData) && !isset($profileData['error'])) {
foreach ($profileData as $p) {
$profiles[$p['id']] = $p;
}
}
}
// Enrich activity with actor info
$result = [];
if (is_array($activity) && !isset($activity['error'])) {
foreach ($activity as $a) {
$a['actor'] = $profiles[$a['actor_id']] ?? ['username' => 'Player'];
$result[] = $a;
}
}
jsonResponse(['activity' => $result]);
<?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);
$db = supabase($token);
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'GET') {
getBattlePass($db, $userId);
}
if ($method === 'POST') {
$input = getInput();
if (($input['action'] ?? '') === 'claim_tier') claimTier($db, $userId, $input);
else jsonError('Invalid action');
}
function getBattlePass($db, string $userId): void {
// Current season config
$season = [
'id' => 'season_1',
'name' => 'الموسم الأول',
'name_en' => 'Season 1',
'starts_at' => '2026-06-01',
'ends_at' => '2026-07-01',
'total_tiers' => 30,
'xp_per_tier' => 100
];
// Get player XP
$profiles = $db->get('profiles', ['id' => 'eq.' . $userId, 'select' => 'xp,level', 'limit' => 1]);
$profile = is_array($profiles) && !empty($profiles) ? $profiles[0] : ['xp' => 0];
$totalXp = $profile['xp'] ?? 0;
// Calculate current tier
$currentTier = min(intval($totalXp / $season['xp_per_tier']), $season['total_tiers']);
$tierProgress = ($totalXp % $season['xp_per_tier']) / $season['xp_per_tier'];
// Generate tier rewards
$tiers = [];
for ($i = 1; $i <= $season['total_tiers']; $i++) {
$reward = generateTierReward($i);
$tiers[] = [
'tier' => $i,
'unlocked' => $i <= $currentTier,
'reward_free' => $reward['free'],
'reward_premium' => $reward['premium']
];
}
// Days remaining
$endsAt = strtotime($season['ends_at']);
$daysLeft = max(0, intval(($endsAt - time()) / 86400));
jsonResponse([
'season' => $season,
'current_tier' => $currentTier,
'tier_progress' => round($tierProgress, 2),
'total_xp' => $totalXp,
'days_left' => $daysLeft,
'tiers' => $tiers
]);
}
function generateTierReward(int $tier): array {
$freeRewards = [
['type' => 'coins', 'amount' => 50],
['type' => 'coins', 'amount' => 75],
['type' => 'xp_boost', 'amount' => 1],
['type' => 'coins', 'amount' => 100],
['type' => 'coins', 'amount' => 50],
];
$premiumRewards = [
['type' => 'gems', 'amount' => 10],
['type' => 'coins', 'amount' => 200],
['type' => 'frame', 'id' => 'season_frame_' . ceil($tier / 10)],
['type' => 'gems', 'amount' => 20],
['type' => 'coins', 'amount' => 300],
];
return [
'free' => $freeRewards[($tier - 1) % count($freeRewards)],
'premium' => $premiumRewards[($tier - 1) % count($premiumRewards)]
];
}
function claimTier($db, string $userId, array $input): void {
$tier = intval($input['tier'] ?? 0);
$coins = intval($input['coins'] ?? 0);
if ($tier < 1) jsonError('Invalid tier');
$profiles = $db->get('profiles', ['id' => 'eq.' . $userId, 'select' => 'coins', 'limit' => 1]);
$profile = is_array($profiles) && !empty($profiles) ? $profiles[0] : ['coins' => 0];
$newCoins = ($profile['coins'] ?? 0) + $coins;
$db->update('profiles', ['coins' => $newCoins], ['id' => 'eq.' . $userId]);
jsonResponse(['success' => true, 'coins' => $newCoins]);
}
import * as scene from '../../core/scene.js'; import * as scene from '../../core/scene.js';
import { mountFriends } from './scenes/friends.js'; import { mountFriends } from './scenes/friends.js';
import { mountNotifications } from './scenes/notifications.js'; import { mountNotifications } from './scenes/notifications.js';
import { mountActivity } from './scenes/activity.js';
scene.register('friends', mountFriends); scene.register('friends', mountFriends);
scene.register('notifications', mountNotifications); scene.register('notifications', mountNotifications);
scene.register('activity-feed', mountActivity);
import * as net from '../../../core/net.js';
import { t } from '../../../core/i18n.js';
export async function mountActivity(el) {
el.innerHTML = `
<div style="padding:16px;display:flex;flex-direction:column;gap:12px;">
<h2 style="font-size:18px;font-weight:700;color:#f8fafc;">📰 آخر الأخبار</h2>
<div id="activity-list"></div>
</div>
`;
try {
const data = await net.get('activity.php');
renderActivity(el, data.activity || []);
} catch (e) {
el.querySelector('#activity-list').innerHTML = '<div style="text-align:center;color:#64748b;padding:32px;">لا توجد أخبار</div>';
}
}
function renderActivity(el, activities) {
const list = el.querySelector('#activity-list');
if (activities.length === 0) {
list.innerHTML = '<div style="text-align:center;color:#64748b;padding:32px;">لا توجد أخبار — العب لتظهر أخبارك هنا</div>';
return;
}
const actionLabels = {
'game_win': 'فاز بمباراة',
'game_loss': 'خسر مباراة',
'game_draw': 'تعادل في مباراة',
'achievement_unlock': 'حصل على إنجاز',
'level_up': 'ارتقى لمستوى جديد',
'tournament_join': 'انضم لبطولة',
'friend_add': 'أضاف صديق جديد'
};
list.innerHTML = activities.map(a => {
const actor = a.actor || {};
const label = actionLabels[a.action] || a.action;
const time = timeAgo(a.created_at);
return `
<div style="display:flex;gap:10px;padding:10px;background:#1a1a2e;border-radius:10px;">
<div style="width:36px;height:36px;border-radius:50%;background:#2a2a4a;display:flex;align-items:center;justify-content:center;font-size:14px;flex-shrink:0;">
${actor.avatar_url ? `<img src="${actor.avatar_url}" style="width:100%;height:100%;border-radius:50%;object-fit:cover;">` : '👤'}
</div>
<div style="flex:1;">
<div style="font-size:13px;color:#f8fafc;"><strong>${actor.display_name || actor.username || '?'}</strong> ${label}</div>
<div style="font-size:10px;color:#64748b;margin-top:2px;">${time}</div>
</div>
</div>
`;
}).join('');
}
function timeAgo(date) {
if (!date) return '';
const diff = Date.now() - new Date(date).getTime();
const mins = Math.floor(diff / 60000);
if (mins < 1) return 'الآن';
if (mins < 60) return `منذ ${mins} دقيقة`;
const hours = Math.floor(mins / 60);
if (hours < 24) return `منذ ${hours} ساعة`;
return `منذ ${Math.floor(hours / 24)} يوم`;
}
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