set_charset('utf8mb4'); if ($db->connect_error) { http_response_code(500); echo json_encode(['status'=>'error','message'=>'DB error']); exit; } // ── Helpers ────────────────────────────────────────────────────── function ok($data, $msg='Success', $code=200) { http_response_code($code); echo json_encode(['status'=>'success','message'=>$msg,'data'=>$data]); exit; } function err($msg, $code=400, $extra=[]) { http_response_code($code); echo json_encode(['status'=>'error','message'=>$msg] + $extra); exit; } function body() { $raw = file_get_contents('php://input'); $json = json_decode($raw, true); return is_array($json) ? $json : $_POST; } function esc($v) { global $db; return $db->real_escape_string((string)$v); } function now() { return date('Y-m-d H:i:s'); } function q($sql) { global $db; return $db->query($sql); } function rows($sql) { $r=q($sql); $a=[]; if($r) while($row=$r->fetch_assoc()) $a[]=$row; return $a; } function row($sql) { $r=q($sql); return $r ? $r->fetch_assoc() : null; } function insert($table, $d) { global $db; $wo_tables = ["Wo_Users","Wo_USERS","Wo_Posts","Wo_POSTS","user_daily_missions","user_gamification","user_settings","coin_transactions","xp_transactions","user_follows","user_achievements","user_lesson_progress","user_answers","user_refresh_tokens","match_requests","notifications"]; $is_wo = in_array($table, $wo_tables); if(!$is_wo) $d['created_at'] = $d['created_at'] ?? now(); if(!$is_wo) $d['updated_at'] = $d['updated_at'] ?? now(); $cols = '`'.implode('`,`',array_keys($d)).'`'; $vals = implode("','", array_map(fn($v)=>esc($v), array_values($d))); if($is_wo){unset($d['created_at']);unset($d['updated_at']);$cols='`'.implode('`,`',array_keys($d)).'`';$vals=implode("','",array_map(fn($v)=>esc($v),array_values($d)));} if($is_wo){unset($d['created_at']);unset($d['updated_at']);$cols='`'.implode('`,`',array_keys($d)).'`';$vals=implode("','",array_map(fn($v)=>esc($v),array_values($d)));} $db->query("INSERT INTO `$table` ($cols) VALUES ('$vals')"); return $db->insert_id; } function update($table, $where_col, $where_val, $d) { $d['updated_at'] = now(); $sets = implode(',', array_map(fn($k,$v)=>"`$k`='".esc($v)."'", array_keys($d), $d)); q("UPDATE `$table` SET $sets WHERE `$where_col`='".esc($where_val)."'"); } function avatar($user) { if (!empty($user['avatar'])) return 'https://arvanz.com/uploads/avatars/'.$user['avatar']; $n = urlencode(substr(($user['first_name'].' '.$user['last_name']) ?? $user['username'] ?? 'U', 0, 1)); return "https://ui-avatars.com/api/?name=$n&size=100&background=6C3CE1&color=ffffff&bold=true"; } function get_ip() { foreach(['HTTP_CF_CONNECTING_IP','HTTP_X_FORWARDED_FOR','REMOTE_ADDR'] as $k) if(!empty($_SERVER[$k])) return explode(',',$_SERVER[$k])[0]; return '0.0.0.0'; } // ── JWT Auth ────────────────────────────────────────────────────── $JWT_SECRET = 'arvanz_nexus_jwt_secret_2026_change_this'; function jwt_create($userId) { global $JWT_SECRET; $h = base64_encode(json_encode(['alg'=>'HS256','typ'=>'JWT'])); $p = base64_encode(json_encode(['sub'=>$userId,'iat'=>time(),'exp'=>time()+86400])); $s = base64_encode(hash_hmac('sha256',"$h.$p",$JWT_SECRET,true)); return "$h.$p.$s"; } function jwt_verify($token) { global $JWT_SECRET; $parts = explode('.', $token); if (count($parts)!==3) return false; [$h,$p,$s] = $parts; $expected = base64_encode(hash_hmac('sha256',"$h.$p",$JWT_SECRET,true)); if (!hash_equals($expected,$s)) return false; $data = json_decode(base64_decode($p),true); if (!$data || $data['exp'] < time()) return false; return $data; } function get_auth_user() { global $db; $token = null; $headers = getallheaders(); if (!empty($headers['Authorization'])) { if (preg_match('/Bearer\s+(.+)/i',$headers['Authorization'],$m)) $token=$m[1]; } if (!$token) return null; $payload = jwt_verify($token); if (!$payload) return null; $uid = (int)$payload['sub']; return row("SELECT * FROM Wo_Users WHERE user_id=$uid AND active='1' AND banned=0"); } function require_auth() { $u = get_auth_user(); if (!$u) err('Unauthorized', 401); return $u; } // ── XP Helper ──────────────────────────────────────────────────── function award_xp($userId, $amount, $source='lesson_complete') { $g = row("SELECT * FROM user_gamification WHERE user_id=$userId"); if (!$g) return; $newXP = $g['total_xp'] + $amount; $levels = [0,100,250,450,700,1000,1400,1900,2500,3200,4000,5000,6500,8000,10000,12500,15500,19000,23000,28000]; $level = 1; foreach($levels as $i=>$req) { if($newXP>=$req) $level=$i+1; else break; } $level = min($level, count($levels)); $lStart = $levels[min($level-1,count($levels)-1)]; $lEnd = $levels[min($level,count($levels)-1)]; q("UPDATE user_gamification SET total_xp=$newXP,current_level=$level,current_level_xp=".($newXP-$lStart).",next_level_xp=".($lEnd-$lStart).",updated_at='".now()."' WHERE user_id=$userId"); q("INSERT INTO xp_transactions (user_id,amount,balance_after,source_type,created_at) VALUES ($userId,$amount,$newXP,'".esc($source)."','".now()."')"); return $newXP; } function award_coins($userId, $amount, $source='lesson_complete') { $g = row("SELECT * FROM user_gamification WHERE user_id=$userId"); if (!$g) return; $newBal = $g['coin_balance'] + $amount; q("UPDATE user_gamification SET coin_balance=$newBal,total_coins_earned=total_coins_earned+$amount WHERE user_id=$userId"); q("INSERT INTO coin_transactions (user_id,amount,balance_after,transaction_type,source_type,created_at) VALUES ($userId,$amount,$newBal,'credit','".esc($source)."','".now()."')"); return $newBal; } function update_streak($userId) { $g = row("SELECT * FROM user_gamification WHERE user_id=$userId"); if (!$g) return; $today = date('Y-m-d'); $yesterday = date('Y-m-d',strtotime('-1 day')); if ($g['last_activity_date']===$today) return $g['current_streak']; $streak = ($g['last_activity_date']===$yesterday) ? $g['current_streak']+1 : 1; $longest = max($streak, $g['longest_streak']); q("UPDATE user_gamification SET current_streak=$streak,longest_streak=$longest,last_activity_date='$today' WHERE user_id=$userId"); return $streak; } function send_notif($userId, $type, $msg, $icon='🔔', $refId=null) { insert('notifications',['user_id'=>$userId,'type'=>$type,'message'=>$msg,'icon'=>$icon,'ref_id'=>$refId??0,'is_read'=>0]); } // ── Router ──────────────────────────────────────────────────────── $method = $_SERVER['REQUEST_METHOD']; $route = trim($_GET['route'] ?? '', '/'); // ================================================================ // AUTH ROUTES // ================================================================ if ($route==='auth/register' && $method==='POST') { $d = body(); $username = trim($d['username'] ?? ''); $email = strtolower(trim($d['email'] ?? '')); $password = $d['password'] ?? ''; $display_name = trim($d['display_name'] ?? ''); if (!$username||!$email||!$password||!$display_name) err('All fields required'); if (!filter_var($email,FILTER_VALIDATE_EMAIL)) err('Invalid email'); if (strlen($password)<8) err('Password must be at least 8 characters'); if (row("SELECT user_id FROM Wo_Users WHERE email='".esc($email)."'")) err('Email already taken'); if (row("SELECT user_id FROM Wo_Users WHERE username='".esc($username)."'")) err('Username already taken'); $hash = password_hash($password, PASSWORD_BCRYPT); $uid = insert('Wo_Users',[ 'username'=>$username,'email'=>$email,'password'=>$hash, 'first_name'=>explode(' ',$display_name)[0],'last_name'=>(explode(' ',$display_name)[1]??''),'active'=>'1','banned'=>0,'is_pro'=>0,'registered'=>date('d/Y'),'joined'=>time(),'ip_address'=>($_SERVER["REMOTE_ADDR"]??''),'type'=>'user','src'=>'arvanz_nexus','email_code'=>bin2hex(random_bytes(8)) ]); insert('user_gamification',['user_id'=>$uid,'total_xp'=>0,'current_level'=>1,'coin_balance'=>100,'current_streak'=>0,'longest_streak'=>0,'next_level_xp'=>100]); insert('user_settings',['user_id'=>$uid]); insert('coin_transactions',['user_id'=>$uid,'amount'=>100,'balance_after'=>100,'transaction_type'=>'credit','source_type'=>'admin_grant','description'=>'Welcome bonus']); $user = row("SELECT * FROM Wo_Users WHERE user_id=$uid"); $token = jwt_create($uid); ok(['user'=>['id'=>$uid,'username'=>$username,'display_name'=>$display_name,'email'=>$email,'subscription_plan'=>'free'],'access_token'=>$token,'token_type'=>'Bearer','expires_in'=>86400],'Welcome to Arvanz Nexus! 🎉',201); } if ($route==='auth/login' && $method==='POST') { $d = body(); $email = strtolower(trim($d['email'] ?? '')); $password = $d['password'] ?? ''; if (!$email||!$password) err('Email and password required'); $user = row("SELECT * FROM Wo_Users WHERE email='".esc($email)."'"); if (!$user || !password_verify($password, $user['password'])) err('Invalid credentials', 401); if ($user['banned']==1) err('Account banned', 403); q("UPDATE Wo_Users SET lastseen=".time().",ip_address='".esc(get_ip())."' WHERE user_id=".(int)$user['user_id']); $g = row("SELECT * FROM user_gamification WHERE user_id=".(int)$user['user_id']); $token = jwt_create($user['user_id']); ok([ 'user'=>['id'=>(int)$user['user_id'],'username'=>$user['username'],'display_name'=>($user['first_name'].' '.$user['last_name']),'email'=>$user['email'],'is_pro'=>$user['is_pro'],'avatar_url'=>avatar($user)], 'gamification'=>$g ? ['total_xp'=>(int)$g['total_xp'],'current_level'=>(int)$g['current_level'],'coin_balance'=>(int)$g['coin_balance'],'current_streak'=>(int)$g['current_streak'],'longest_streak'=>(int)$g['longest_streak'],'current_league'=>$g['current_league']] : null, 'access_token'=>$token,'token_type'=>'Bearer','expires_in'=>86400 ],'Welcome back!'); } if ($route==='auth/me' && $method==='GET') { $user = require_auth(); $g = row("SELECT * FROM user_gamification WHERE user_id=".(int)$user['user_id']); ok(['user'=>['id'=>(int)$user['user_id'],'username'=>$user['username'],'display_name'=>($user['first_name'].' '.$user['last_name']),'email'=>$user['email'],'is_pro'=>$user['is_pro'],'avatar_url'=>avatar($user),'country_id'=>$user['country_id']],'gamification'=>$g]); } // ================================================================ // LEARNING ROUTES // ================================================================ if ($route==='learning/categories' && $method==='GET') { $cats = rows("SELECT * FROM learning_categories WHERE is_active=1 ORDER BY sort_order ASC"); ok(['categories'=>$cats]); } if ($route==='learning/courses' && $method==='GET') { $page = max(1,(int)($_GET['page']??1)); $limit = 20; $offset = ($page-1)*$limit; $where = "WHERE c.status='published'"; if (!empty($_GET['category_id'])) $where .= " AND c.category_id=".(int)$_GET['category_id']; if (!empty($_GET['level'])) $where .= " AND c.level='".esc($_GET['level'])."'"; if (!empty($_GET['q'])) $where .= " AND c.title LIKE '%".esc($_GET['q'])."%'"; $total = row("SELECT COUNT(*) as cnt FROM courses c $where")['cnt'] ?? 0; $courses = rows("SELECT c.*,CONCAT(u.first_name,' ',u.last_name) as display_name_raw,u.username as creator_name FROM courses c LEFT JOIN Wo_Users u ON u.user_id=c.creator_id $where ORDER BY c.enrolled_count DESC LIMIT $limit OFFSET $offset"); ok(['courses'=>$courses,'total'=>(int)$total,'page'=>$page]); } if (preg_match('#^learning/courses/(\d+)$#',$route,$m) && $method==='GET') { $id = (int)$m[1]; $course = row("SELECT c.*,CONCAT(u.first_name,' ',u.last_name) as display_name_raw,u.username as creator_name,u.about as creator_bio FROM courses c LEFT JOIN Wo_Users u ON u.user_id=c.creator_id WHERE c.id=$id AND c.status='published'"); if (!$course) err('Course not found',404); $sections = rows("SELECT * FROM course_sections WHERE course_id=$id ORDER BY sort_order ASC"); foreach($sections as &$s) { $s['lessons'] = rows("SELECT id,title,lesson_type,duration_minutes,xp_reward,is_preview,sort_order FROM course_lessons WHERE section_id={$s['id']} AND status='published' ORDER BY sort_order ASC"); } // Also get lessons without sections if(empty($sections)) { $direct_lessons = rows("SELECT id,title,lesson_type,duration_minutes,xp_reward,is_preview,sort_order FROM course_lessons WHERE course_id=$id AND (section_id IS NULL OR section_id=0) AND status='published' ORDER BY sort_order ASC"); if($direct_lessons) $sections = [['id'=>0,'title'=>'Course Content','lessons'=>$direct_lessons]]; } $user = get_auth_user(); $enrolled = $user ? (bool)row("SELECT id FROM course_enrollments WHERE course_id=$id AND user_id=".(int)$user['user_id']) : false; ok(['course'=>$course,'sections'=>$sections,'enrolled'=>$enrolled]); } if (preg_match('#^learning/courses/(\d+)/enroll$#',$route,$m) && $method==='POST') { $user = require_auth(); $uid = (int)$user['user_id']; $cid = (int)$m[1]; $course = row("SELECT * FROM courses WHERE user_id=$cid AND status='published'"); if (!$course) err('Not found',404); if (row("SELECT id FROM course_enrollments WHERE course_id=$cid AND user_id=$uid")) err('Already enrolled'); if (!$course['is_free']) err('Purchase required',402); insert('course_enrollments',['course_id'=>$cid,'user_id'=>$uid,'enrolled_at'=>now()]); q("UPDATE courses SET enrolled_count=enrolled_count+1 WHERE user_id=$cid"); ok(['enrolled'=>true],'Enrolled successfully!'); } if (preg_match('#^learning/lessons/(\d+)/start$#',$route,$m) && $method==='POST') { $user = require_auth(); $uid=(int)$user['user_id']; $lid=(int)$m[1]; $lesson = row("SELECT * FROM course_lessons WHERE id=$lid"); if (!$lesson) err('Not found',404); if (!row("SELECT id FROM user_lesson_progress WHERE user_id=$uid AND lesson_id=$lid")) { insert('user_lesson_progress',['user_id'=>$uid,'lesson_id'=>$lid,'course_id'=>$lesson['course_id']??0,'status'=>'in_progress','started_at'=>now()]); } ok(['lesson_id'=>$lid,'started_at'=>now()]); } if (preg_match('#^learning/lessons/(\d+)/complete$#',$route,$m) && $method==='POST') { $user = require_auth(); $uid=(int)$user['user_id']; $lid=(int)$m[1]; $d = body(); $accuracy = min(100,max(0,(int)($d['accuracy']??0))); $time = max(0,(int)($d['time_spent']??0)); $lesson = row("SELECT * FROM course_lessons WHERE id=$lid"); $xp = $accuracy>=90?50:($accuracy>=70?35:($accuracy>=50?25:15)); $coins = max(5,(int)($xp/5)); $now = now(); $exists = row("SELECT id FROM user_lesson_progress WHERE user_id=$uid AND lesson_id=$lid"); if ($exists) q("UPDATE user_lesson_progress SET status='completed',accuracy=$accuracy,time_spent=$time,completed_at='$now',updated_at='$now' WHERE user_id=".(int)$exists['id']); else { $cid=$lesson['course_id']??0; insert('user_lesson_progress',['user_id'=>$uid,'lesson_id'=>$lid,'course_id'=>$cid,'status'=>'completed','accuracy'=>$accuracy,'time_spent'=>$time,'completed_at'=>$now]); } $newXP = award_xp($uid,$xp,'lesson_complete'); $newCoins = award_coins($uid,$coins,'lesson_complete'); $streak = update_streak($uid); ok(['xp_earned'=>$xp,'coins_earned'=>$coins,'accuracy'=>$accuracy,'new_xp_balance'=>$newXP,'new_coin_balance'=>$newCoins,'streak'=>$streak]); } if ($route==='learning/answer' && $method==='POST') { $user = require_auth(); $uid=(int)$user['user_id']; $d = body(); $qid = (int)($d['question_id']??0); $ans = trim($d['answer']??''); $q_row = row("SELECT * FROM lesson_questions WHERE user_id=$qid"); if (!$q_row) err('Not found',404); $correct = strtolower(trim($ans))===strtolower(trim($q_row['correct_answer'])); ok(['correct'=>$correct,'correct_answer'=>$q_row['correct_answer'],'explanation'=>$q_row['explanation']??'','xp_earned'=>$correct?(int)($q_row['xp_reward']??10):0]); } if ($route==='learning/recommendations' && $method==='GET') { $user = get_auth_user(); $exclude = ''; if ($user) { $enrolled = rows("SELECT course_id FROM course_enrollments WHERE user_id=".(int)$user['user_id']); $ids = array_column($enrolled,'course_id'); if ($ids) $exclude = 'AND id NOT IN ('.implode(',',$ids).')'; } $courses = rows("SELECT * FROM courses WHERE status='published' $exclude ORDER BY average_rating DESC,enrolled_count DESC LIMIT 6"); ok(['recommendations'=>$courses]); } if (preg_match('#^learning/progress/(\d+)$#',$route,$m) && $method==='GET') { $user = require_auth(); $uid=(int)$user['user_id']; $cid=(int)$m[1]; $total = (int)(row("SELECT COUNT(*) as cnt FROM course_lessons WHERE course_id=$cid AND status='published'")['cnt']??0); $done = (int)(row("SELECT COUNT(*) as cnt FROM user_lesson_progress WHERE course_id=$cid AND user_id=$uid AND status='completed'")['cnt']??0); ok(['total_lessons'=>$total,'completed_lessons'=>$done,'completion_percent'=>$total>0?round(($done/$total)*100):0]); } // ================================================================ // GAMIFICATION ROUTES // ================================================================ if ($route==='gamification/stats' && $method==='GET') { $user = require_auth(); $uid=(int)$user['user_id']; $g = row("SELECT * FROM user_gamification WHERE user_id=$uid"); if (!$g) err('Not found',404); $weeklyXP = row("SELECT COALESCE(SUM(amount),0) as total FROM xp_transactions WHERE user_id=$uid AND created_at>=DATE_SUB(NOW(),INTERVAL 7 DAY)")['total']??0; ok(array_merge($g,['weekly_xp'=>(int)$weeklyXP])); } if ($route==='gamification/leaderboard' && $method==='GET') { $type = $_GET['type']??'global'; $period = $_GET['period']??'weekly'; $page = max(1,(int)($_GET['page']??1)); $offset = ($page-1)*50; $dateF = $period==='weekly' ? "AND x.created_at>=DATE_SUB(NOW(),INTERVAL 7 DAY)" : ($period==='monthly' ? "AND x.created_at>=DATE_SUB(NOW(),INTERVAL 30 DAY)" : ''); $entries = rows("SELECT u.user_id as user_id,CONCAT(u.first_name,' ',u.last_name) as display_name_raw,u.username,u.username,u.avatar,u.country_id,ug.current_level,ug.current_streak,ug.current_league,COALESCE(SUM(x.amount),0) as xp FROM Wo_Users u JOIN user_gamification ug ON ug.user_id=u.user_id LEFT JOIN xp_transactions x ON x.user_id=u.user_id $dateF WHERE u.active='1' AND banned=0 GROUP BY u.user_id ORDER BY xp DESC LIMIT 50 OFFSET $offset"); $authUser = get_auth_user(); foreach($entries as $i=>&$e) { $e['rank']=$offset+$i+1; $e['avatar_url']=avatar($e); $e['is_you']=$authUser&&$authUser['user_id']==$e['user_id']; } ok(['entries'=>$entries,'type'=>$type,'period'=>$period]); } if ($route==='gamification/achievements' && $method==='GET') { $user = require_auth(); $uid=(int)$user['user_id']; $all = rows("SELECT * FROM achievements WHERE is_active=1"); $earned = rows("SELECT * FROM user_achievements WHERE user_id=$uid"); $earnedMap = array_column($earned,null,'achievement_id'); $result = array_map(fn($a)=>array_merge($a,['earned'=>isset($earnedMap[$a['id']]),'earned_at'=>$earnedMap[$a['id']]['earned_at']??null]),$all); ok(['achievements'=>$result,'earned_count'=>count($earnedMap),'total_count'=>count($all)]); } if ($route==='gamification/missions/daily' && $method==='GET') { $user = require_auth(); $uid=(int)$user['user_id']; $today=date('Y-m-d'); $existing = rows("SELECT * FROM user_daily_missions WHERE user_id=$uid AND assigned_date='$today'"); if (empty($existing)) { $missions = rows("SELECT * FROM daily_missions WHERE is_active=1 ORDER BY RAND() LIMIT 3"); foreach($missions as $m) insert('user_daily_missions',['user_id'=>$uid,'mission_id'=>$m['id'],'assigned_date'=>$today,'current_progress'=>0,'is_completed'=>0]); $existing = rows("SELECT * FROM user_daily_missions WHERE user_id=$uid AND assigned_date='$today'"); } $result = []; foreach($existing as $um) { $m = row("SELECT * FROM daily_missions WHERE id=".(int)$um['mission_id']); if ($m) $result[] = array_merge($m,$um,['is_completed'=>(bool)$um['is_completed']]); } ok(['missions'=>$result,'date'=>$today,'completed'=>count(array_filter($result,fn($m)=>$m['is_completed']))]); } if (preg_match('#^gamification/missions/(\d+)/complete$#',$route,$m) && $method==='POST') { $user = require_auth(); $uid=(int)$user['user_id']; $mid=(int)$m[1]; $um = row("SELECT udm.*,dm.xp_reward,dm.coin_reward,dm.name FROM user_daily_missions udm JOIN daily_missions dm ON dm.id=udm.mission_id WHERE udm.id=$mid AND udm.user_id=$uid"); if (!$um) err('Not found',404); if ($um['is_completed']) err('Already completed'); q("UPDATE user_daily_missions SET is_completed=1,completed_at='".now()."' WHERE user_id=$mid"); award_xp($uid,(int)$um['xp_reward'],'mission_complete'); award_coins($uid,(int)$um['coin_reward'],'mission_complete'); ok(['completed'=>true,'xp_reward'=>(int)$um['xp_reward'],'coin_reward'=>(int)$um['coin_reward']]); } if ($route==='gamification/streak/update' && $method==='POST') { $user = require_auth(); $uid=(int)$user['user_id']; $streak = update_streak($uid); ok(['streak'=>$streak]); } // ================================================================ // COMMUNITY ROUTES // ================================================================ if ($route==='community/feed' && $method==='GET') { $user = require_auth(); $uid=(int)$user['user_id']; $page = max(1,(int)($_GET['page']??1)); $offset=($page-1)*20; $filter= $_GET['filter']??'trending'; if ($filter==='following') { $posts = rows("SELECT p.*,CONCAT(u.first_name,' ',u.last_name) as display_name_raw,u.username,u.username,u.avatar,(SELECT COUNT(*) FROM post_likes pl WHERE pl.post_id=p.id) as like_count,(SELECT COUNT(*) FROM post_comments pc WHERE pc.post_id=p.id) as comment_count,(SELECT COUNT(*) FROM post_likes pl2 WHERE pl2.post_id=p.id AND pl2.user_id=$uid) as is_liked FROM community_posts p JOIN Wo_Users u ON u.user_id=p.user_id WHERE p.user_id IN (SELECT following_id FROM user_follows WHERE follower_id=$uid UNION SELECT $uid) AND p.deleted_at IS NULL ORDER BY p.created_at DESC LIMIT 20 OFFSET $offset"); } else { $posts = rows("SELECT p.*,CONCAT(u.first_name,' ',u.last_name) as display_name_raw,u.username,u.username,u.avatar,(SELECT COUNT(*) FROM post_likes pl WHERE pl.post_id=p.id) as like_count,(SELECT COUNT(*) FROM post_comments pc WHERE pc.post_id=p.id) as comment_count,(SELECT COUNT(*) FROM post_likes pl2 WHERE pl2.post_id=p.id AND pl2.user_id=$uid) as is_liked FROM community_posts p JOIN Wo_Users u ON u.user_id=p.user_id WHERE p.deleted_at IS NULL ORDER BY p.created_at DESC LIMIT 20 OFFSET $offset"); } foreach($posts as &$p) { $p['avatar_url']=avatar($p); $p['is_liked']=(bool)$p['is_liked']; $p['tags']=json_decode($p['tags']??'[]',true); } ok(['posts'=>$posts,'page'=>$page]); } if ($route==='community/posts' && $method==='POST') { $user = require_auth(); $uid=(int)$user['user_id']; $d = body(); if (empty($d['content'])) err('Content required'); $pid = insert('community_posts',['user_id'=>$uid,'content'=>$d['content'],'post_type'=>$d['post_type']??'text','tags'=>json_encode($d['tags']??[]),'community_id'=>$d['community_id']??null,'media'=>'[]']); award_xp($uid,20,'community_post'); ok(['post'=>row("SELECT * FROM community_posts WHERE user_id=$pid")],'Post created!',201); } if (preg_match('#^community/posts/(\d+)/like$#',$route,$m) && $method==='POST') { $user = require_auth(); $uid=(int)$user['user_id']; $pid=(int)$m[1]; $exists = row("SELECT id FROM post_likes WHERE post_id=$pid AND user_id=$uid"); if ($exists) { q("DELETE FROM post_likes WHERE user_id=".(int)$exists['id']); $liked=false; } else { insert('post_likes',['post_id'=>$pid,'user_id'=>$uid]); $liked=true; } $count = (int)(row("SELECT COUNT(*) as cnt FROM post_likes WHERE post_id=$pid")['cnt']??0); ok(['liked'=>$liked,'count'=>$count]); } if (preg_match('#^community/posts/(\d+)/comments$#',$route,$m) && $method==='POST') { $user = require_auth(); $uid=(int)$user['user_id']; $pid=(int)$m[1]; $d = body(); if (empty($d['content'])) err('Content required'); $cid = insert('post_comments',['post_id'=>$pid,'user_id'=>$uid,'content'=>$d['content']]); award_xp($uid,10,'community_comment'); $comment = row("SELECT pc.*,CONCAT(u.first_name,' ',u.last_name) as display_name_raw,u.username,u.username FROM post_comments pc JOIN Wo_Users u ON u.user_id=pc.user_id WHERE pc.id=$cid"); ok(['comment'=>$comment],201); } if (preg_match('#^community/posts/(\d+)/comments$#',$route,$m) && $method==='GET') { $pid=(int)$m[1]; $page=max(1,(int)($_GET['page']??1)); $comments = rows("SELECT pc.*,CONCAT(u.first_name,' ',u.last_name) as display_name_raw,u.username,u.username,u.avatar FROM post_comments pc JOIN Wo_Users u ON u.user_id=pc.user_id WHERE pc.post_id=$pid AND pc.deleted_at IS NULL ORDER BY pc.created_at ASC LIMIT 20 OFFSET ".(($page-1)*20)); foreach($comments as &$c) $c['avatar_url']=avatar($c); ok(['comments'=>$comments]); } if ($route==='community/communities' && $method==='GET') { $page = max(1,(int)($_GET['page']??1)); $offset=($page-1)*20; $where = "WHERE is_public=1"; if (!empty($_GET['q'])) $where .= " AND name LIKE '%".esc($_GET['q'])."%'"; $communities = rows("SELECT * FROM communities $where ORDER BY member_count DESC LIMIT 20 OFFSET $offset"); $user = get_auth_user(); foreach($communities as &$c) { $c['is_member'] = $user ? (bool)row("SELECT id FROM community_members WHERE community_id={$c['id']} AND user_id=".(int)$user['user_id']) : false; } ok(['communities'=>$communities]); } if (preg_match('#^community/communities/(\d+)/join$#',$route,$m) && $method==='POST') { $user = require_auth(); $uid=(int)$user['user_id']; $cid=(int)$m[1]; if (row("SELECT id FROM community_members WHERE community_id=$cid AND user_id=$uid")) err('Already a member'); insert('community_members',['community_id'=>$cid,'user_id'=>$uid,'role'=>'member']); q("UPDATE communities SET member_count=member_count+1 WHERE user_id=$cid"); ok(['joined'=>true]); } // ================================================================ // USER ROUTES // ================================================================ if ($route==='user/me' && $method==='GET') { $user = require_auth(); $uid=(int)$user['user_id']; $g = row("SELECT * FROM user_gamification WHERE user_id=$uid"); ok(['user'=>array_merge($user,['avatar_url'=>avatar($user)]),'gamification'=>$g]); } if (preg_match('#^user/([a-zA-Z0-9_]+)$#',$route,$m) && $method==='GET') { $username = esc($m[1]); $u = row("SELECT * FROM Wo_Users WHERE username='$username' AND active='1' AND banned=0"); if (!$u) err('User not found',404); $uid = (int)$u['user_id']; $g = row("SELECT * FROM user_gamification WHERE user_id=$uid"); $followers = (int)(row("SELECT COUNT(*) as cnt FROM user_follows WHERE following_id=$uid")['cnt']??0); $following = (int)(row("SELECT COUNT(*) as cnt FROM user_follows WHERE follower_id=$uid")['cnt']??0); $authUser = get_auth_user(); $isFollowing = $authUser ? (bool)row("SELECT id FROM user_follows WHERE follower_id=".(int)$authUser['id']." AND following_id=$uid") : false; ok(['user'=>array_merge($u,['avatar_url'=>avatar($u)]),'gamification'=>$g,'follower_count'=>$followers,'following_count'=>$following,'is_following'=>$isFollowing]); } if (preg_match('#^user/(\d+)/follow$#',$route,$m) && $method==='POST') { $user = require_auth(); $uid=(int)$user['user_id']; $tid=(int)$m[1]; if ($uid===$tid) err('Cannot follow yourself'); if (row("SELECT id FROM user_follows WHERE follower_id=$uid AND following_id=$tid")) err('Already following'); insert('user_follows',['follower_id'=>$uid,'following_id'=>$tid]); send_notif($tid,'new_follower',($user['first_name'].' '.$user['last_name']).' started following you!','👤',$uid); ok(['following'=>true]); } if (preg_match('#^user/(\d+)/unfollow$#',$route,$m) && $method==='POST') { $user = require_auth(); $uid=(int)$user['user_id']; $tid=(int)$m[1]; q("DELETE FROM user_follows WHERE follower_id=$uid AND following_id=$tid"); ok(['following'=>false]); } // ================================================================ // NOTIFICATIONS ROUTES // ================================================================ if ($route==='notifications' && $method==='GET') { $user = require_auth(); $uid=(int)$user['user_id']; $page = max(1,(int)($_GET['page']??1)); $notifs = rows("SELECT * FROM notifications WHERE user_id=$uid ORDER BY created_at DESC LIMIT 20 OFFSET ".(($page-1)*20)); ok(['notifications'=>$notifs,'page'=>$page]); } if ($route==='notifications/unread-count' && $method==='GET') { $user = require_auth(); $uid=(int)$user['user_id']; $count = (int)(row("SELECT COUNT(*) as cnt FROM notifications WHERE user_id=$uid AND is_read=0")['cnt']??0); ok(['count'=>$count]); } if ($route==='notifications/read-all' && $method==='POST') { $user = require_auth(); $uid=(int)$user['user_id']; q("UPDATE notifications SET is_read=1 WHERE user_id=$uid"); ok(['marked'=>true]); } if (preg_match('#^notifications/(\d+)/read$#',$route,$m)) { $user = require_auth(); $uid=(int)$user['user_id']; $nid=(int)$m[1]; q("UPDATE notifications SET is_read=1 WHERE user_id=$nid AND user_id=$uid"); ok([]); } // ================================================================ // SEARCH ROUTES // ================================================================ if ($route==='search/quick' && $method==='GET') { $q = esc($_GET['q']??$_GET['route_q']??''); // Also check if q is in route string if(empty($q) && isset($_SERVER['QUERY_STRING'])) { parse_str($_SERVER['QUERY_STRING'],$qs); $q=esc($qs['q']??''); } if (strlen($q)<2) ok(['results'=>[]]); $users = rows("SELECT id,display_name,username,avatar FROM Wo_Users WHERE (display_name LIKE '%$q%' OR username LIKE '%$q%') AND active='1' AND banned=0 LIMIT 4"); $courses = rows("SELECT id,title,thumbnail_url FROM courses WHERE title LIKE '%$q%' AND status='published' LIMIT 4"); $results = []; foreach($users as $u) $results[] = ['type'=>'user','id'=>$u['id'],'title'=>($u['first_name'].' '.$u['last_name']),'subtitle'=>'@'.$u['username'],'image'=>avatar($u),'url'=>'https://arvanz.com/profile/'.$u['username']]; foreach($courses as $c) $results[] = ['type'=>'course','id'=>$c['id'],'title'=>$c['title'],'subtitle'=>'Course','image'=>$c['thumbnail_url'],'url'=>'https://arvanz.com/courses/'.$c['id']]; ok(['results'=>$results]); } if ($route==='search' && $method==='POST') { $d = body(); $q = esc($d['query']??''); if (strlen($q)<2) err('Query too short'); $users = rows("SELECT id,display_name,username,avatar FROM Wo_Users WHERE (display_name LIKE '%$q%' OR username LIKE '%$q%') AND active='1' AND banned=0 LIMIT 5"); $courses = rows("SELECT id,title,thumbnail_url,level FROM courses WHERE title LIKE '%$q%' AND status='published' LIMIT 5"); $communities = rows("SELECT id,name,avatar_url,member_count FROM communities WHERE name LIKE '%$q%' AND is_public=1 LIMIT 5"); $results = []; foreach($users as $u) $results[] = ['type'=>'user','id'=>$u['id'],'title'=>($u['first_name'].' '.$u['last_name']),'subtitle'=>'@'.$u['username'],'image'=>avatar($u),'url'=>'https://arvanz.com/profile/'.$u['username']]; foreach($courses as $c) $results[] = ['type'=>'course','id'=>$c['id'],'title'=>$c['title'],'subtitle'=>ucfirst($c['level']),'image'=>$c['thumbnail_url'],'url'=>'https://arvanz.com/courses/'.$c['id']]; foreach($communities as $c) $results[] = ['type'=>'community','id'=>$c['id'],'title'=>$c['name'],'subtitle'=>$c['member_count'].' members','image'=>$c['avatar_url'],'url'=>'https://arvanz.com/communities/'.$c['id']]; ok(['results'=>$results,'query'=>$_GET['q']??$d['query']??'']); } // ================================================================ // MARKETPLACE ROUTES // ================================================================ if ($route==='marketplace/listings' && $method==='GET') { $page = max(1,(int)($_GET['page']??1)); $offset=($page-1)*20; $where = "WHERE ml.status='active'"; if (!empty($_GET['type'])) $where .= " AND ml.type='".esc($_GET['type'])."'"; if (!empty($_GET['q'])) $where .= " AND ml.title LIKE '%".esc($_GET['q'])."%'"; $listings = rows("SELECT ml.*,CONCAT(u.first_name,' ',u.last_name) as display_name_raw,u.username as seller_name FROM marketplace_listings ml LEFT JOIN Wo_Users u ON u.user_id=ml.seller_id $where ORDER BY ml.sales_count DESC LIMIT 20 OFFSET $offset"); $total = (int)(row("SELECT COUNT(*) as cnt FROM marketplace_listings ml $where")['cnt']??0); ok(['listings'=>$listings,'total'=>$total,'page'=>$page]); } // ================================================================ // MATCHING ROUTES // ================================================================ if ($route==='matching/study-partners' && $method==='POST') { $user = require_auth(); $uid=(int)$user['user_id']; $partners = rows("SELECT u.user_id,CONCAT(u.first_name,' ',u.last_name) as display_name_raw,u.username,u.username,u.bio,u.country_id,u.avatar,ug.current_level,ug.current_streak FROM Wo_Users u LEFT JOIN user_gamification ug ON ug.user_id=u.user_id WHERE u.id!=$uid AND u.active='1' AND banned=0 ORDER BY RAND() LIMIT 10"); foreach($partners as &$p) $p['avatar_url']=avatar($p); ok(['partners'=>$partners]); } if ($route==='matching/request' && $method==='POST') { $user = require_auth(); $uid=(int)$user['user_id']; $d = body(); $tid=(int)($d['target_user_id']??0); if (!$tid||$tid===$uid) err('Invalid target'); if (row("SELECT id FROM match_requests WHERE requester_id=$uid AND target_user_id=$tid")) err('Request already sent'); $rid = insert('match_requests',['requester_id'=>$uid,'target_user_id'=>$tid,'type'=>$d['type']??'study','message'=>$d['message']??'','status'=>'pending','expires_at'=>date('Y-m-d H:i:s',strtotime('+7 days'))]); send_notif($tid,'match_request',($user['first_name'].' '.$user['last_name']).' wants to be your study partner!','🤝',$rid); ok(['request_id'=>$rid],'Match request sent!',201); } if (preg_match('#^matching/(\d+)/accept$#',$route,$m) && $method==='POST') { $user = require_auth(); $uid=(int)$user['user_id']; $rid=(int)$m[1]; $req = row("SELECT * FROM match_requests WHERE user_id=$rid AND target_user_id=$uid AND status='pending'"); if (!$req) err('Not found',404); q("UPDATE match_requests SET status='accepted' WHERE user_id=$rid"); send_notif((int)$req['requester_id'],'match_accepted',($user['first_name'].' '.$user['last_name']).' accepted your match request! 🎉','✅'); ok(['accepted'=>true]); } // ================================================================ // SUBSCRIPTION ROUTES // ================================================================ if ($route==='subscription/plans' && $method==='GET') { ok(['plans'=>[ ['id'=>'free','name'=>'Explorer','price'=>0,'features'=>['5 lessons/day','3 AI conversations/week','Join 3 communities']], ['id'=>'pro','name'=>'Learner','price'=>9.99,'features'=>['Unlimited lessons','30 AI conversations/month','No ads','Streak shields']], ['id'=>'premium','name'=>'Master','price'=>19.99,'features'=>['Everything in Pro','Unlimited AI','Pronunciation coach','Writing coach']], ['id'=>'creator','name'=>'Creator','price'=>29.99,'features'=>['Everything in Premium','Course builder','70% revenue share','Creator analytics']], ]]); } if ($route==='subscription/status' && $method==='GET') { $user = require_auth(); ok(['plan'=>$user['is_pro']??'free','expires_at'=>$user['subscription_expires_at']??null,'is_active'=>true]); } // ================================================================ // DEFAULT — 404 // ================================================================ err("Route not found: $method /$route", 404);