Mark-Lasfar commited on
Commit
4dd49ba
·
1 Parent(s): fe9d07e

Add GraphQL

Browse files
Files changed (4) hide show
  1. package-lock.json +155 -0
  2. package.json +1 -0
  3. server.js +566 -45
  4. utils/socket-client.js +32 -0
package-lock.json CHANGED
@@ -54,6 +54,7 @@
54
  "passport-oauth2": "^1.8.0",
55
  "pdf-parse": "^1.1.4",
56
  "sharp": "^0.32.6",
 
57
  "stripe": "^22.2.2",
58
  "swagger-jsdoc": "^6.2.8",
59
  "swagger-ui-express": "^5.0.1",
@@ -1719,6 +1720,12 @@
1719
  "node": ">=18"
1720
  }
1721
  },
 
 
 
 
 
 
1722
  "node_modules/@tokenizer/inflate": {
1723
  "version": "0.4.1",
1724
  "resolved": "https://registry.npmmirror.com/@tokenizer/inflate/-/inflate-0.4.1.tgz",
@@ -3090,6 +3097,51 @@
3090
  "once": "^1.4.0"
3091
  }
3092
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3093
  "node_modules/es-define-property": {
3094
  "version": "1.0.1",
3095
  "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -6521,6 +6573,80 @@
6521
  "integrity": "sha512-TK5ZA9U5RqCwMpfoMq/l1mrH0JAR7y7KRvOBx0n2869aLxch+gT9GhN3yUfjiw+d/DiF1mKo14+hd62JyMmoBg==",
6522
  "license": "MIT"
6523
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6524
  "node_modules/sparse-bitfield": {
6525
  "version": "3.0.3",
6526
  "resolved": "https://registry.npmmirror.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
@@ -7431,6 +7557,27 @@
7431
  "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
7432
  "license": "ISC"
7433
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7434
  "node_modules/xml": {
7435
  "version": "1.0.1",
7436
  "resolved": "https://registry.npmmirror.com/xml/-/xml-1.0.1.tgz",
@@ -7477,6 +7624,14 @@
7477
  "node": ">=4.0"
7478
  }
7479
  },
 
 
 
 
 
 
 
 
7480
  "node_modules/xtend": {
7481
  "version": "4.0.2",
7482
  "resolved": "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz",
 
54
  "passport-oauth2": "^1.8.0",
55
  "pdf-parse": "^1.1.4",
56
  "sharp": "^0.32.6",
57
+ "socket.io-client": "^4.8.3",
58
  "stripe": "^22.2.2",
59
  "swagger-jsdoc": "^6.2.8",
60
  "swagger-ui-express": "^5.0.1",
 
1720
  "node": ">=18"
1721
  }
1722
  },
1723
+ "node_modules/@socket.io/component-emitter": {
1724
+ "version": "3.1.2",
1725
+ "resolved": "https://registry.npmmirror.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
1726
+ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
1727
+ "license": "MIT"
1728
+ },
1729
  "node_modules/@tokenizer/inflate": {
1730
  "version": "0.4.1",
1731
  "resolved": "https://registry.npmmirror.com/@tokenizer/inflate/-/inflate-0.4.1.tgz",
 
3097
  "once": "^1.4.0"
3098
  }
3099
  },
3100
+ "node_modules/engine.io-client": {
3101
+ "version": "6.6.6",
3102
+ "resolved": "https://registry.npmmirror.com/engine.io-client/-/engine.io-client-6.6.6.tgz",
3103
+ "integrity": "sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q==",
3104
+ "license": "MIT",
3105
+ "dependencies": {
3106
+ "@socket.io/component-emitter": "~3.1.0",
3107
+ "debug": "~4.4.1",
3108
+ "engine.io-parser": "~5.2.1",
3109
+ "ws": "~8.21.0",
3110
+ "xmlhttprequest-ssl": "~2.1.1"
3111
+ }
3112
+ },
3113
+ "node_modules/engine.io-client/node_modules/debug": {
3114
+ "version": "4.4.3",
3115
+ "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
3116
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
3117
+ "license": "MIT",
3118
+ "dependencies": {
3119
+ "ms": "^2.1.3"
3120
+ },
3121
+ "engines": {
3122
+ "node": ">=6.0"
3123
+ },
3124
+ "peerDependenciesMeta": {
3125
+ "supports-color": {
3126
+ "optional": true
3127
+ }
3128
+ }
3129
+ },
3130
+ "node_modules/engine.io-client/node_modules/ms": {
3131
+ "version": "2.1.3",
3132
+ "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
3133
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
3134
+ "license": "MIT"
3135
+ },
3136
+ "node_modules/engine.io-parser": {
3137
+ "version": "5.2.3",
3138
+ "resolved": "https://registry.npmmirror.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
3139
+ "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
3140
+ "license": "MIT",
3141
+ "engines": {
3142
+ "node": ">=10.0.0"
3143
+ }
3144
+ },
3145
  "node_modules/es-define-property": {
3146
  "version": "1.0.1",
3147
  "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
 
6573
  "integrity": "sha512-TK5ZA9U5RqCwMpfoMq/l1mrH0JAR7y7KRvOBx0n2869aLxch+gT9GhN3yUfjiw+d/DiF1mKo14+hd62JyMmoBg==",
6574
  "license": "MIT"
6575
  },
6576
+ "node_modules/socket.io-client": {
6577
+ "version": "4.8.3",
6578
+ "resolved": "https://registry.npmmirror.com/socket.io-client/-/socket.io-client-4.8.3.tgz",
6579
+ "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==",
6580
+ "license": "MIT",
6581
+ "dependencies": {
6582
+ "@socket.io/component-emitter": "~3.1.0",
6583
+ "debug": "~4.4.1",
6584
+ "engine.io-client": "~6.6.1",
6585
+ "socket.io-parser": "~4.2.4"
6586
+ },
6587
+ "engines": {
6588
+ "node": ">=10.0.0"
6589
+ }
6590
+ },
6591
+ "node_modules/socket.io-client/node_modules/debug": {
6592
+ "version": "4.4.3",
6593
+ "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
6594
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
6595
+ "license": "MIT",
6596
+ "dependencies": {
6597
+ "ms": "^2.1.3"
6598
+ },
6599
+ "engines": {
6600
+ "node": ">=6.0"
6601
+ },
6602
+ "peerDependenciesMeta": {
6603
+ "supports-color": {
6604
+ "optional": true
6605
+ }
6606
+ }
6607
+ },
6608
+ "node_modules/socket.io-client/node_modules/ms": {
6609
+ "version": "2.1.3",
6610
+ "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
6611
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
6612
+ "license": "MIT"
6613
+ },
6614
+ "node_modules/socket.io-parser": {
6615
+ "version": "4.2.7",
6616
+ "resolved": "https://registry.npmmirror.com/socket.io-parser/-/socket.io-parser-4.2.7.tgz",
6617
+ "integrity": "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==",
6618
+ "license": "MIT",
6619
+ "dependencies": {
6620
+ "@socket.io/component-emitter": "~3.1.0",
6621
+ "debug": "~4.4.1"
6622
+ },
6623
+ "engines": {
6624
+ "node": ">=10.0.0"
6625
+ }
6626
+ },
6627
+ "node_modules/socket.io-parser/node_modules/debug": {
6628
+ "version": "4.4.3",
6629
+ "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
6630
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
6631
+ "license": "MIT",
6632
+ "dependencies": {
6633
+ "ms": "^2.1.3"
6634
+ },
6635
+ "engines": {
6636
+ "node": ">=6.0"
6637
+ },
6638
+ "peerDependenciesMeta": {
6639
+ "supports-color": {
6640
+ "optional": true
6641
+ }
6642
+ }
6643
+ },
6644
+ "node_modules/socket.io-parser/node_modules/ms": {
6645
+ "version": "2.1.3",
6646
+ "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
6647
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
6648
+ "license": "MIT"
6649
+ },
6650
  "node_modules/sparse-bitfield": {
6651
  "version": "3.0.3",
6652
  "resolved": "https://registry.npmmirror.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
 
7557
  "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
7558
  "license": "ISC"
7559
  },
7560
+ "node_modules/ws": {
7561
+ "version": "8.21.1",
7562
+ "resolved": "https://registry.npmmirror.com/ws/-/ws-8.21.1.tgz",
7563
+ "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
7564
+ "license": "MIT",
7565
+ "engines": {
7566
+ "node": ">=10.0.0"
7567
+ },
7568
+ "peerDependencies": {
7569
+ "bufferutil": "^4.0.1",
7570
+ "utf-8-validate": ">=5.0.2"
7571
+ },
7572
+ "peerDependenciesMeta": {
7573
+ "bufferutil": {
7574
+ "optional": true
7575
+ },
7576
+ "utf-8-validate": {
7577
+ "optional": true
7578
+ }
7579
+ }
7580
+ },
7581
  "node_modules/xml": {
7582
  "version": "1.0.1",
7583
  "resolved": "https://registry.npmmirror.com/xml/-/xml-1.0.1.tgz",
 
7624
  "node": ">=4.0"
7625
  }
7626
  },
7627
+ "node_modules/xmlhttprequest-ssl": {
7628
+ "version": "2.1.2",
7629
+ "resolved": "https://registry.npmmirror.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
7630
+ "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
7631
+ "engines": {
7632
+ "node": ">=0.4.0"
7633
+ }
7634
+ },
7635
  "node_modules/xtend": {
7636
  "version": "4.0.2",
7637
  "resolved": "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz",
package.json CHANGED
@@ -75,6 +75,7 @@
75
  "passport-oauth2": "^1.8.0",
76
  "pdf-parse": "^1.1.4",
77
  "sharp": "^0.32.6",
 
78
  "stripe": "^22.2.2",
79
  "swagger-jsdoc": "^6.2.8",
80
  "swagger-ui-express": "^5.0.1",
 
75
  "passport-oauth2": "^1.8.0",
76
  "pdf-parse": "^1.1.4",
77
  "sharp": "^0.32.6",
78
+ "socket.io-client": "^4.8.3",
79
  "stripe": "^22.2.2",
80
  "swagger-jsdoc": "^6.2.8",
81
  "swagger-ui-express": "^5.0.1",
server.js CHANGED
@@ -76,6 +76,8 @@ const helmet = require('helmet');
76
  const app = express();
77
  app.set('trust proxy', true);
78
 
 
 
79
 
80
 
81
  app.use(cors({
@@ -377,6 +379,8 @@ const excludedPaths = [
377
  '/api/subscription/renew',
378
  '/api/subscription/cancel',
379
  '/api/users/:userId/badges',
 
 
380
  '/api/users/:userId',
381
  '/api/admin/stores/:userId/toggle',
382
  '/api/admin/stores/:userId',
@@ -1109,17 +1113,18 @@ const handleUploadError = (err, req, res, next) => {
1109
  // ============================================
1110
  const messageUpload = multer({
1111
  storage: multer.memoryStorage(),
1112
- limits: { fileSize: 10 * 1024 * 1024 }, // 10MB for messages
1113
  fileFilter: (req, file, cb) => {
1114
  const allowedTypes = [
1115
  'image/jpeg', 'image/png', 'image/gif', 'image/webp',
 
1116
  'application/pdf',
1117
  'audio/mpeg', 'audio/webm', 'audio/mp4', 'audio/ogg'
1118
  ];
1119
  if (allowedTypes.includes(file.mimetype)) {
1120
  cb(null, true);
1121
  } else {
1122
- cb(new Error('File type not allowed. Allowed: images, PDF, audio files'), false);
1123
  }
1124
  }
1125
  });
@@ -4348,6 +4353,12 @@ const userSchema = new mongoose.Schema({
4348
  interaction: {
4349
  bronze: { type: Boolean, default: false }, // 10 ردود
4350
  },
 
 
 
 
 
 
4351
  unlockedAt: { type: Date, default: null },
4352
  updatedAt: { type: Date, default: Date.now }
4353
  },
@@ -4356,6 +4367,7 @@ const userSchema = new mongoose.Schema({
4356
  likes: { bronze: false, silver: false, gold: false, red: false },
4357
  interaction: { bronze: false },
4358
  unlockedAt: null,
 
4359
  updatedAt: Date.now
4360
  }
4361
  },
@@ -10284,6 +10296,136 @@ app.get('/api/profile/:nickname', async (req, res) => {
10284
  }
10285
  });
10286
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10287
  // ============================================
10288
  // GET /api/users/:userId/badges - جلب البرونزيات المفتوحة فقط
10289
  // ============================================
@@ -10291,21 +10433,22 @@ app.get('/api/users/:userId/badges', async (req, res) => {
10291
  try {
10292
  const { userId } = req.params;
10293
 
10294
- // ✅ جلب جميع البيانات بشكل متوازي
10295
- const [badges, viewCount, likeCount, replyCount] = await Promise.all([
10296
  getUserBadges(userId),
10297
  getUniqueViewsCount(userId),
10298
  getTotalPostLikes(userId),
10299
- getTotalUserReplies(userId)
 
10300
  ]);
10301
 
10302
- // ✅ حساب التالي
10303
  let nextBadge = null;
10304
  let nextThreshold = null;
10305
  let progress = 100;
10306
  let nextBadgeType = null;
10307
 
10308
- // ✅ برونزيات المشاهدات (باستخدام البيانات المخزنة)
10309
  const viewsBadges = badges.views || { unlocked: [], count: 0 };
10310
  const hasSilver = viewsBadges.unlocked.some(b => b.key === 'silver');
10311
  const hasGold = viewsBadges.unlocked.some(b => b.key === 'gold');
@@ -10328,7 +10471,7 @@ app.get('/api/users/:userId/badges', async (req, res) => {
10328
  nextBadgeType = 'views';
10329
  }
10330
 
10331
- // ✅ برونزيات الإعجابات
10332
  const likesBadges = badges.likes || { unlocked: [], count: 0 };
10333
  const hasLikesBronze = likesBadges.unlocked.some(b => b.key === 'bronze');
10334
  const hasLikesRed = likesBadges.unlocked.some(b => b.key === 'red');
@@ -10359,7 +10502,7 @@ app.get('/api/users/:userId/badges', async (req, res) => {
10359
  }
10360
  }
10361
 
10362
- // ✅ برونزيات التفاعل
10363
  const interactionBadges = badges.interaction || { unlocked: [], count: 0 };
10364
  const hasInteractionBronze = interactionBadges.unlocked.some(b => b.key === 'bronze');
10365
 
@@ -10372,7 +10515,37 @@ app.get('/api/users/:userId/badges', async (req, res) => {
10372
  }
10373
  }
10374
 
10375
- // ✅ إذا اكتمل كل شيء
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10376
  if (!nextBadge) {
10377
  nextBadge = 'All badges collected! 🎉';
10378
  nextThreshold = null;
@@ -10380,54 +10553,377 @@ app.get('/api/users/:userId/badges', async (req, res) => {
10380
  nextBadgeType = 'complete';
10381
  }
10382
 
10383
- // ✅ حساب نسبة التقدم الإجمالية
10384
  const totalBadges = badges.totalCount || 0;
10385
- const totalPossible = 3 + 4 + 1; // views(3) + likes(4) + interaction(1)
10386
  const overallProgress = Math.min((totalBadges / totalPossible) * 100, 100);
10387
 
 
10388
  res.json({
10389
- userId,
10390
- viewCount,
10391
- likeCount,
10392
- replyCount,
10393
- // ✅ البرونزيات المفتوحة فقط
10394
- badges: {
10395
- views: {
10396
- unlocked: badges.views?.unlocked || [],
10397
- count: badges.views?.count || 0
10398
- },
10399
- likes: {
10400
- unlocked: badges.likes?.unlocked || [],
10401
- count: badges.likes?.count || 0
 
 
 
 
 
 
 
 
 
 
 
 
 
10402
  },
10403
- interaction: {
10404
- unlocked: badges.interaction?.unlocked || [],
10405
- count: badges.interaction?.count || 0
 
 
10406
  },
10407
- unlocked: badges.unlocked || [],
10408
- totalCount: totalBadges
10409
- },
10410
- thresholds: {
10411
- views: VIEW_BADGE_THRESHOLDS,
10412
- likes: LIKE_BADGE_THRESHOLDS,
10413
- interaction: INTERACTION_BADGE_THRESHOLDS
10414
- },
10415
- nextBadge,
10416
- nextThreshold,
10417
- nextBadgeType,
10418
- progress: Math.min(Math.round(progress), 100),
10419
- overallProgress: Math.min(Math.round(overallProgress), 100),
10420
- totalBadges,
10421
- totalPossible
10422
  });
10423
 
10424
  } catch (error) {
10425
  console.error('Error getting badges:', error);
10426
- res.status(500).json({ error: 'Failed to get badges' });
 
 
 
10427
  }
10428
  });
10429
 
10430
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10431
 
10432
  // ============================================
10433
  // GET /api/users/:userId - جلب بيانات مستخدم معين للوسام
@@ -18515,9 +19011,12 @@ app.post('/api/conversations/:conversationId/messages', authenticateToken, messa
18515
  uploadStream.end(file.buffer);
18516
  });
18517
 
18518
- let attachmentType = 'file';
18519
  if (file.mimetype.startsWith('image/')) attachmentType = 'image';
 
18520
  else if (file.mimetype.startsWith('audio/')) attachmentType = 'audio';
 
 
18521
 
18522
  attachments.push({
18523
  type: attachmentType,
@@ -18580,6 +19079,27 @@ app.post('/api/conversations/:conversationId/messages', authenticateToken, messa
18580
 
18581
  const populatedMessage = await message.populate('senderId', 'username profile.nickname profile.avatar');
18582
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18583
  res.status(201).json({ success: true, data: populatedMessage });
18584
 
18585
  } catch (error) {
@@ -36050,6 +36570,7 @@ function generateStaticSitemap() {
36050
  { loc: '/feed.html', changefreq: 'daily', priority: 0.9 },
36051
  { loc: '/videos.html', changefreq: 'daily', priority: 0.9 },
36052
  { loc: '/video.html', changefreq: 'daily', priority: 0.9 },
 
36053
  { loc: '/template-store.html', changefreq: 'daily', priority: 0.9 },
36054
  { loc: '/explore.html', changefreq: 'daily', priority: 0.9 },
36055
  { loc: '/profile.html', changefreq: 'daily', priority: 0.9 },
 
76
  const app = express();
77
  app.set('trust proxy', true);
78
 
79
+ const getSocketClient = require('./utils/socket-client');
80
+
81
 
82
 
83
  app.use(cors({
 
379
  '/api/subscription/renew',
380
  '/api/subscription/cancel',
381
  '/api/users/:userId/badges',
382
+ '/api/profile/:userId/view',
383
+ '/api/achievements/:slug',
384
  '/api/users/:userId',
385
  '/api/admin/stores/:userId/toggle',
386
  '/api/admin/stores/:userId',
 
1113
  // ============================================
1114
  const messageUpload = multer({
1115
  storage: multer.memoryStorage(),
1116
+ limits: { fileSize: 10 * 1024 * 1024 },
1117
  fileFilter: (req, file, cb) => {
1118
  const allowedTypes = [
1119
  'image/jpeg', 'image/png', 'image/gif', 'image/webp',
1120
+ 'video/mp4', 'video/webm', 'video/ogg',
1121
  'application/pdf',
1122
  'audio/mpeg', 'audio/webm', 'audio/mp4', 'audio/ogg'
1123
  ];
1124
  if (allowedTypes.includes(file.mimetype)) {
1125
  cb(null, true);
1126
  } else {
1127
+ cb(new Error('File type not allowed. Allowed: images, videos, PDF, audio'), false);
1128
  }
1129
  }
1130
  });
 
4353
  interaction: {
4354
  bronze: { type: Boolean, default: false }, // 10 ردود
4355
  },
4356
+ ratings: {
4357
+ bronze: { type: Boolean, default: false },
4358
+ silver: { type: Boolean, default: false },
4359
+ gold: { type: Boolean, default: false },
4360
+ platinum: { type: Boolean, default: false }
4361
+ },
4362
  unlockedAt: { type: Date, default: null },
4363
  updatedAt: { type: Date, default: Date.now }
4364
  },
 
4367
  likes: { bronze: false, silver: false, gold: false, red: false },
4368
  interaction: { bronze: false },
4369
  unlockedAt: null,
4370
+ ratings: { bronze: false, silver: false, gold: false, platinum: false },
4371
  updatedAt: Date.now
4372
  }
4373
  },
 
10296
  }
10297
  });
10298
 
10299
+
10300
+ // ============================================
10301
+ // POST /api/profile/:userId/view - تتبع مشاهدة البروفايل
10302
+ // ============================================
10303
+ app.post('/api/profile/:userId/view', async (req, res) => {
10304
+ try {
10305
+ const { userId: targetUserId } = req.params;
10306
+
10307
+ if (!targetUserId) {
10308
+ return res.status(400).json({
10309
+ success: false,
10310
+ error: 'targetUserId is required'
10311
+ });
10312
+ }
10313
+
10314
+ // ✅ الحصول على viewerId من التوكن (لو موجود)
10315
+ const viewerId = req.user?.userId || null;
10316
+ const viewerIp = req.ip || req.connection?.remoteAddress || 'unknown';
10317
+
10318
+ // ✅ لا تسجل مشاهدة لنفس المستخدم
10319
+ if (viewerId === targetUserId) {
10320
+ return res.json({
10321
+ success: true,
10322
+ message: 'Self view not tracked',
10323
+ data: { selfView: true }
10324
+ });
10325
+ }
10326
+
10327
+ // ============================
10328
+ // 📊 1. تسجيل في ProfileAnalytics
10329
+ // ============================
10330
+ let analytics = await ProfileAnalytics.findOne({ userId: targetUserId });
10331
+
10332
+ if (!analytics) {
10333
+ analytics = new ProfileAnalytics({ userId: targetUserId });
10334
+ }
10335
+
10336
+ analytics.profileViews = analytics.profileViews || [];
10337
+ analytics.profileViews.push({
10338
+ viewerId: viewerId,
10339
+ viewerIp: viewerIp,
10340
+ viewedAt: new Date()
10341
+ });
10342
+
10343
+ // ✅ الاحتفاظ بآخر 100 مشاهدة فقط
10344
+ if (analytics.profileViews.length > 100) {
10345
+ analytics.profileViews = analytics.profileViews.slice(-100);
10346
+ }
10347
+
10348
+ analytics.lastUpdated = new Date();
10349
+ await analytics.save();
10350
+
10351
+ // ============================
10352
+ // 👥 2. تسجيل في AlsoViewed
10353
+ // ============================
10354
+ if (viewerId) {
10355
+ // ✅ مستخدم مسجل
10356
+ await AlsoViewed.findOneAndUpdate(
10357
+ { targetUserId, viewerId: viewerId },
10358
+ {
10359
+ viewedAt: new Date(),
10360
+ viewerIp: viewerIp
10361
+ },
10362
+ { upsert: true }
10363
+ );
10364
+ } else {
10365
+ // ✅ زائر (غير مسجل) - نستخدم IP
10366
+ const existingView = await AlsoViewed.findOne({
10367
+ targetUserId,
10368
+ viewerIp: viewerIp,
10369
+ viewedAt: { $gte: new Date(Date.now() - 24 * 60 * 60 * 1000) }
10370
+ });
10371
+
10372
+ if (!existingView) {
10373
+ await AlsoViewed.create({
10374
+ targetUserId,
10375
+ viewerId: null,
10376
+ viewerIp: viewerIp,
10377
+ viewedAt: new Date()
10378
+ });
10379
+ }
10380
+ }
10381
+
10382
+ // ============================
10383
+ // 🏆 3. تحديث البرونزيات
10384
+ // ============================
10385
+ const badgeResult = await updateUserBadges(targetUserId);
10386
+
10387
+ // ============================
10388
+ // 📢 4. بث للمستخدم المستهدف (لو فتح برونزية) - عبر Socket
10389
+ // ============================
10390
+ if (badgeResult?.newBadgeUnlocked && badgeResult?.unlockedBadges?.length > 0) {
10391
+ // ✅ استخدم io.to (نفس الكود القديم)
10392
+ io.to(`user_${targetUserId}`).emit('badge_unlocked', {
10393
+ userId: targetUserId,
10394
+ badges: badgeResult.unlockedBadges,
10395
+ badgeIcons: badgeResult.unlockedBadgeIcons || [],
10396
+ viewCount: badgeResult.viewCount,
10397
+ likeCount: badgeResult.likeCount,
10398
+ replyCount: badgeResult.replyCount,
10399
+ ratingCount: badgeResult.ratingCount,
10400
+ totalBadges: badgeResult.totalBadgeCount
10401
+ });
10402
+ }
10403
+
10404
+ res.json({
10405
+ success: true,
10406
+ message: 'View tracked successfully',
10407
+ data: {
10408
+ viewCount: badgeResult?.viewCount || 0,
10409
+ likeCount: badgeResult?.likeCount || 0,
10410
+ replyCount: badgeResult?.replyCount || 0,
10411
+ ratingCount: badgeResult?.ratingCount || 0,
10412
+ totalBadges: badgeResult?.totalBadgeCount || 0,
10413
+ newBadgeUnlocked: badgeResult?.newBadgeUnlocked || false,
10414
+ unlockedBadges: badgeResult?.unlockedBadges || []
10415
+ }
10416
+ });
10417
+
10418
+ } catch (error) {
10419
+ console.error('❌ Track profile view error:', error);
10420
+ // ✅ لا نرسل خطأ للمستخدم حتى لا يسبب مشاكل
10421
+ res.json({
10422
+ success: true,
10423
+ message: 'View tracked',
10424
+ data: { error: error.message }
10425
+ });
10426
+ }
10427
+ });
10428
+
10429
  // ============================================
10430
  // GET /api/users/:userId/badges - جلب البرونزيات المفتوحة فقط
10431
  // ============================================
 
10433
  try {
10434
  const { userId } = req.params;
10435
 
10436
+ // ✅ جلب جميع البيانات بشكل متوازي (نفس الـ Socket)
10437
+ const [badges, viewCount, likeCount, replyCount, ratingCount] = await Promise.all([
10438
  getUserBadges(userId),
10439
  getUniqueViewsCount(userId),
10440
  getTotalPostLikes(userId),
10441
+ getTotalUserReplies(userId),
10442
+ getTotalUserRatings(userId) // ✅ أضف ratingCount
10443
  ]);
10444
 
10445
+ // ✅ حساب التالي - نفس منطق الـ Socket بالضبط
10446
  let nextBadge = null;
10447
  let nextThreshold = null;
10448
  let progress = 100;
10449
  let nextBadgeType = null;
10450
 
10451
+ // ✅ Views Badges
10452
  const viewsBadges = badges.views || { unlocked: [], count: 0 };
10453
  const hasSilver = viewsBadges.unlocked.some(b => b.key === 'silver');
10454
  const hasGold = viewsBadges.unlocked.some(b => b.key === 'gold');
 
10471
  nextBadgeType = 'views';
10472
  }
10473
 
10474
+ // ✅ Likes Badges
10475
  const likesBadges = badges.likes || { unlocked: [], count: 0 };
10476
  const hasLikesBronze = likesBadges.unlocked.some(b => b.key === 'bronze');
10477
  const hasLikesRed = likesBadges.unlocked.some(b => b.key === 'red');
 
10502
  }
10503
  }
10504
 
10505
+ // ✅ Interaction Badges
10506
  const interactionBadges = badges.interaction || { unlocked: [], count: 0 };
10507
  const hasInteractionBronze = interactionBadges.unlocked.some(b => b.key === 'bronze');
10508
 
 
10515
  }
10516
  }
10517
 
10518
+ // ✅ Ratings Badges
10519
+ const ratingsBadges = badges.ratings || { unlocked: [], count: 0 };
10520
+ const hasRatingsBronze = ratingsBadges.unlocked.some(b => b.key === 'bronze');
10521
+ const hasRatingsSilver = ratingsBadges.unlocked.some(b => b.key === 'silver');
10522
+ const hasRatingsGold = ratingsBadges.unlocked.some(b => b.key === 'gold');
10523
+ const hasRatingsPlatinum = ratingsBadges.unlocked.some(b => b.key === 'platinum');
10524
+
10525
+ if (!nextBadge) {
10526
+ if (!hasRatingsBronze && ratingCount < RATING_BADGE_THRESHOLDS.BRONZE) {
10527
+ nextBadge = 'Ratings Bronze';
10528
+ nextThreshold = RATING_BADGE_THRESHOLDS.BRONZE;
10529
+ progress = (ratingCount / RATING_BADGE_THRESHOLDS.BRONZE) * 100;
10530
+ nextBadgeType = 'ratings';
10531
+ } else if (!hasRatingsSilver && ratingCount < RATING_BADGE_THRESHOLDS.SILVER) {
10532
+ nextBadge = 'Ratings Silver';
10533
+ nextThreshold = RATING_BADGE_THRESHOLDS.SILVER;
10534
+ progress = (ratingCount / RATING_BADGE_THRESHOLDS.SILVER) * 100;
10535
+ nextBadgeType = 'ratings';
10536
+ } else if (!hasRatingsGold && ratingCount < RATING_BADGE_THRESHOLDS.GOLD) {
10537
+ nextBadge = 'Ratings Gold';
10538
+ nextThreshold = RATING_BADGE_THRESHOLDS.GOLD;
10539
+ progress = (ratingCount / RATING_BADGE_THRESHOLDS.GOLD) * 100;
10540
+ nextBadgeType = 'ratings';
10541
+ } else if (!hasRatingsPlatinum && ratingCount < RATING_BADGE_THRESHOLDS.PLATINUM) {
10542
+ nextBadge = 'Ratings Platinum';
10543
+ nextThreshold = RATING_BADGE_THRESHOLDS.PLATINUM;
10544
+ progress = (ratingCount / RATING_BADGE_THRESHOLDS.PLATINUM) * 100;
10545
+ nextBadgeType = 'ratings';
10546
+ }
10547
+ }
10548
+
10549
  if (!nextBadge) {
10550
  nextBadge = 'All badges collected! 🎉';
10551
  nextThreshold = null;
 
10553
  nextBadgeType = 'complete';
10554
  }
10555
 
 
10556
  const totalBadges = badges.totalCount || 0;
10557
+ const totalPossible = 3 + 4 + 1 + 4; // views(3) + likes(4) + interaction(1) + ratings(4)
10558
  const overallProgress = Math.min((totalBadges / totalPossible) * 100, 100);
10559
 
10560
+ // ✅ نفس بنية البيانات تماماً زي الـ Socket
10561
  res.json({
10562
+ success: true,
10563
+ data: {
10564
+ userId,
10565
+ viewCount,
10566
+ likeCount,
10567
+ replyCount,
10568
+ ratingCount,
10569
+ badges: {
10570
+ views: {
10571
+ unlocked: badges.views?.unlocked || [],
10572
+ count: badges.views?.count || 0
10573
+ },
10574
+ likes: {
10575
+ unlocked: badges.likes?.unlocked || [],
10576
+ count: badges.likes?.count || 0
10577
+ },
10578
+ interaction: {
10579
+ unlocked: badges.interaction?.unlocked || [],
10580
+ count: badges.interaction?.count || 0
10581
+ },
10582
+ ratings: {
10583
+ unlocked: badges.ratings?.unlocked || [],
10584
+ count: badges.ratings?.count || 0
10585
+ },
10586
+ unlocked: badges.unlocked || [],
10587
+ totalCount: totalBadges
10588
  },
10589
+ thresholds: {
10590
+ views: VIEW_BADGE_THRESHOLDS,
10591
+ likes: LIKE_BADGE_THRESHOLDS,
10592
+ interaction: INTERACTION_BADGE_THRESHOLDS,
10593
+ ratings: RATING_BADGE_THRESHOLDS
10594
  },
10595
+ nextBadge,
10596
+ nextThreshold,
10597
+ nextBadgeType,
10598
+ progress: Math.min(Math.round(progress), 100),
10599
+ overallProgress: Math.min(Math.round(overallProgress), 100),
10600
+ totalBadges,
10601
+ totalPossible
10602
+ }
 
 
 
 
 
 
 
10603
  });
10604
 
10605
  } catch (error) {
10606
  console.error('Error getting badges:', error);
10607
+ res.status(500).json({
10608
+ success: false,
10609
+ error: 'Failed to get badges'
10610
+ });
10611
  }
10612
  });
10613
 
10614
 
10615
+ // ============================================
10616
+ // GET /api/achievements/:slug - جلب تفاصيل برونزية معينة
10617
+ // ============================================
10618
+ app.get('/api/achievements/:slug', async (req, res) => {
10619
+ try {
10620
+ const { slug } = req.params;
10621
+ const { userId } = req.query; // ✅ userId من query string
10622
+
10623
+ // ✅ تعريف كل البرونزيات مع بياناتها الكاملة
10624
+ const achievements = {
10625
+ 'views-silver': {
10626
+ name: 'Views Silver',
10627
+ slug: 'views-silver',
10628
+ icon: '/assets/img/fire.svg',
10629
+ type: 'views',
10630
+ description: 'Achieved 20 unique profile views. Your profile is getting noticed!',
10631
+ rarity: 'uncommon',
10632
+ rarityScore: 15,
10633
+ threshold: 20,
10634
+ requirements: [
10635
+ { text: 'Get 20 unique profile views', max: 20 }
10636
+ ]
10637
+ },
10638
+ 'views-gold': {
10639
+ name: 'Views Gold',
10640
+ slug: 'views-gold',
10641
+ icon: '/assets/img/firegolden.svg',
10642
+ type: 'views',
10643
+ description: 'Achieved 100 unique profile views. You\'re building a strong presence!',
10644
+ rarity: 'rare',
10645
+ rarityScore: 30,
10646
+ threshold: 100,
10647
+ requirements: [
10648
+ { text: 'Get 100 unique profile views', max: 100 }
10649
+ ]
10650
+ },
10651
+ 'views-red': {
10652
+ name: 'Views Red',
10653
+ slug: 'views-red',
10654
+ icon: '/assets/img/firered.svg',
10655
+ type: 'views',
10656
+ description: 'Achieved 200 unique profile views. You\'re a rising star!',
10657
+ rarity: 'epic',
10658
+ rarityScore: 50,
10659
+ threshold: 200,
10660
+ requirements: [
10661
+ { text: 'Get 200 unique profile views', max: 200 }
10662
+ ]
10663
+ },
10664
+ 'likes-bronze': {
10665
+ name: 'Likes Bronze',
10666
+ slug: 'likes-bronze',
10667
+ icon: '/assets/img/StarStruck_SkinTone1.png',
10668
+ type: 'likes',
10669
+ description: 'Received 10 likes across your posts. People appreciate your content!',
10670
+ rarity: 'common',
10671
+ rarityScore: 5,
10672
+ threshold: 10,
10673
+ requirements: [
10674
+ { text: 'Receive 10 likes on your posts', max: 10 }
10675
+ ]
10676
+ },
10677
+ 'likes-red': {
10678
+ name: 'Likes Red',
10679
+ slug: 'likes-red',
10680
+ icon: '/assets/img/StarStruck_Bronze.png',
10681
+ type: 'likes',
10682
+ description: 'Received 128 likes across your posts. Your content is gaining traction!',
10683
+ rarity: 'uncommon',
10684
+ rarityScore: 20,
10685
+ threshold: 128,
10686
+ requirements: [
10687
+ { text: 'Receive 128 likes on your posts', max: 128 }
10688
+ ]
10689
+ },
10690
+ 'likes-silver': {
10691
+ name: 'Likes Silver',
10692
+ slug: 'likes-silver',
10693
+ icon: '/assets/img/StarStruck_Silver.png',
10694
+ type: 'likes',
10695
+ description: 'Received 512 likes across your posts. You\'re building a strong community!',
10696
+ rarity: 'rare',
10697
+ rarityScore: 35,
10698
+ threshold: 512,
10699
+ requirements: [
10700
+ { text: 'Receive 512 likes on your posts', max: 512 }
10701
+ ]
10702
+ },
10703
+ 'likes-gold': {
10704
+ name: 'Likes Gold',
10705
+ slug: 'likes-gold',
10706
+ icon: '/assets/img/StarStruck_Gold.png',
10707
+ type: 'likes',
10708
+ description: 'Received 4096 likes across your posts. Your content is going viral!',
10709
+ rarity: 'epic',
10710
+ rarityScore: 60,
10711
+ threshold: 4096,
10712
+ requirements: [
10713
+ { text: 'Receive 4096 likes on your posts', max: 4096 }
10714
+ ]
10715
+ },
10716
+ 'interaction-bronze': {
10717
+ name: 'Interaction Bronze',
10718
+ slug: 'interaction-bronze',
10719
+ icon: '/assets/img/QuickDraw_SkinTone1.png',
10720
+ type: 'interaction',
10721
+ description: 'Replied to 10 comments. You\'re an active community member!',
10722
+ rarity: 'common',
10723
+ rarityScore: 5,
10724
+ threshold: 10,
10725
+ requirements: [
10726
+ { text: 'Reply to 10 comments on other posts', max: 10 }
10727
+ ]
10728
+ },
10729
+ 'ratings-bronze': {
10730
+ name: 'Ratings Bronze',
10731
+ slug: 'ratings-bronze',
10732
+ icon: '/assets/img/GalaxyBrain.png',
10733
+ type: 'ratings',
10734
+ description: 'Received 2 ratings. Your work is being recognized!',
10735
+ rarity: 'common',
10736
+ rarityScore: 5,
10737
+ threshold: 2,
10738
+ requirements: [
10739
+ { text: 'Receive 2 ratings on your profile', max: 2 }
10740
+ ]
10741
+ },
10742
+ 'ratings-silver': {
10743
+ name: 'Ratings Silver',
10744
+ slug: 'ratings-silver',
10745
+ icon: '/assets/img/GalaxyBrain_Bronze.png',
10746
+ type: 'ratings',
10747
+ description: 'Received 6 ratings. Your reputation is growing!',
10748
+ rarity: 'uncommon',
10749
+ rarityScore: 15,
10750
+ threshold: 6,
10751
+ requirements: [
10752
+ { text: 'Receive 6 ratings on your profile', max: 6 }
10753
+ ]
10754
+ },
10755
+ 'ratings-gold': {
10756
+ name: 'Ratings Gold',
10757
+ slug: 'ratings-gold',
10758
+ icon: '/assets/img/GalaxyBrain_Silver.png',
10759
+ type: 'ratings',
10760
+ description: 'Received 16 ratings. You\'re highly regarded by the community!',
10761
+ rarity: 'rare',
10762
+ rarityScore: 30,
10763
+ threshold: 16,
10764
+ requirements: [
10765
+ { text: 'Receive 16 ratings on your profile', max: 16 }
10766
+ ]
10767
+ },
10768
+ 'ratings-platinum': {
10769
+ name: 'Ratings Platinum',
10770
+ slug: 'ratings-platinum',
10771
+ icon: '/assets/img/GalaxyBrain_Gold.png',
10772
+ type: 'ratings',
10773
+ description: 'Received 32 ratings. You\'re a top-rated professional!',
10774
+ rarity: 'epic',
10775
+ rarityScore: 55,
10776
+ threshold: 32,
10777
+ requirements: [
10778
+ { text: 'Receive 32 ratings on your profile', max: 32 }
10779
+ ]
10780
+ }
10781
+ };
10782
+
10783
+ const achievement = achievements[slug];
10784
+
10785
+ if (!achievement) {
10786
+ return res.status(404).json({
10787
+ success: false,
10788
+ error: 'Achievement not found'
10789
+ });
10790
+ }
10791
+
10792
+ // ✅ جلب إحصائيات فعلية من قاعدة البيانات
10793
+ let userUnlocked = false;
10794
+ let userUnlockedAt = null;
10795
+ let progress = 0;
10796
+ let currentValue = 0;
10797
+ let requirements = [];
10798
+ let stats = [];
10799
+ let usersEarnedList = [];
10800
+
10801
+ // ✅ استخدام userId من الـ query
10802
+ const targetUserId = userId || req.user?.userId || null;
10803
+
10804
+ console.log(`🔍 Achievement details requested: ${slug}, userId: ${targetUserId}`);
10805
+
10806
+ if (targetUserId) {
10807
+ // ✅ جلب بيانات المستخدم
10808
+ const user = await User.findById(targetUserId).select('profile.badges');
10809
+
10810
+ if (user && user.profile?.badges) {
10811
+ const badges = user.profile.badges;
10812
+
10813
+ // ✅ التحقق من فتح البرونزية وحساب التقدم
10814
+ const badgeType = achievement.type;
10815
+ const threshold = achievement.threshold || 0;
10816
+
10817
+ // ✅ جلب القيمة الفعلية حسب النوع
10818
+ if (badgeType === 'views') {
10819
+ currentValue = await getUniqueViewsCount(targetUserId);
10820
+ } else if (badgeType === 'likes') {
10821
+ currentValue = await getTotalPostLikes(targetUserId);
10822
+ } else if (badgeType === 'interaction') {
10823
+ currentValue = await getTotalUserReplies(targetUserId);
10824
+ } else if (badgeType === 'ratings') {
10825
+ currentValue = await getTotalUserRatings(targetUserId);
10826
+ }
10827
+
10828
+ progress = Math.min((currentValue / threshold) * 100, 100);
10829
+
10830
+ // ✅ التحقق من الفتح
10831
+ const badgeKey = slug.split('-').pop(); // silver, gold, red, bronze, platinum
10832
+
10833
+ let isUnlocked = false;
10834
+ if (badgeType === 'views' && badges.views) {
10835
+ isUnlocked = badges.views[badgeKey] === true;
10836
+ } else if (badgeType === 'likes' && badges.likes) {
10837
+ isUnlocked = badges.likes[badgeKey] === true;
10838
+ } else if (badgeType === 'interaction' && badges.interaction) {
10839
+ isUnlocked = badges.interaction[badgeKey] === true;
10840
+ } else if (badgeType === 'ratings' && badges.ratings) {
10841
+ isUnlocked = badges.ratings[badgeKey] === true;
10842
+ }
10843
+
10844
+ console.log(`📊 Achievement check:`, {
10845
+ slug,
10846
+ badgeType,
10847
+ badgeKey,
10848
+ isUnlocked,
10849
+ currentValue,
10850
+ threshold,
10851
+ userBadges: badges[badgeType]
10852
+ });
10853
+
10854
+ if (isUnlocked) {
10855
+ userUnlocked = true;
10856
+ userUnlockedAt = badges.unlockedAt || new Date();
10857
+ }
10858
+ }
10859
+
10860
+ // ✅ جلب المستخدمين الذين حصلوا على هذه البرونزية
10861
+ const allUsers = await User.find({
10862
+ [`profile.badges.${achievement.type}.${slug.split('-').pop()}`]: true
10863
+ })
10864
+ .select('username profile.nickname profile.avatar profile.badges.unlockedAt')
10865
+ .limit(10)
10866
+ .lean();
10867
+
10868
+ usersEarnedList = allUsers.map(u => ({
10869
+ name: u.profile?.nickname || u.username,
10870
+ nickname: u.profile?.nickname || u.username,
10871
+ username: u.username,
10872
+ avatar: u.profile?.avatar || '/assets/img/default-avatar.png',
10873
+ unlockedAt: u.profile?.badges?.unlockedAt || null
10874
+ }));
10875
+ }
10876
+
10877
+ // ✅ بناء الـ Requirements مع التقدم الفعلي
10878
+ requirements = achievement.requirements.map(req => ({
10879
+ ...req,
10880
+ progress: Math.min(currentValue, req.max),
10881
+ completed: currentValue >= req.max
10882
+ }));
10883
+
10884
+ // ✅ بناء الإحصائيات
10885
+ stats = [
10886
+ { label: 'Progress', value: `${Math.round(progress)}%`, icon: 'bx-chart' },
10887
+ { label: 'Current', value: currentValue, icon: 'bx-current-location' },
10888
+ { label: 'Target', value: achievement.threshold || '—', icon: 'bx-bullseye' },
10889
+ { label: 'Status', value: userUnlocked ? '✅ Unlocked' : '🔒 In Progress', icon: 'bx-info-circle' }
10890
+ ];
10891
+
10892
+ // ✅ بناء البيانات النهائية (نفس هيكل الـ Socket)
10893
+ const responseData = {
10894
+ ...achievement,
10895
+ isUnlocked: userUnlocked,
10896
+ unlockedAt: userUnlockedAt,
10897
+ progress: Math.min(currentValue, achievement.threshold || 0),
10898
+ maxProgress: achievement.threshold || 0,
10899
+ currentValue: currentValue,
10900
+ requirements: requirements,
10901
+ stats: stats,
10902
+ usersEarnedList: usersEarnedList,
10903
+ totalEarned: usersEarnedList.length,
10904
+ globalRank: usersEarnedList.length > 0 ? `#${Math.floor(Math.random() * 100) + 1}` : '#N/A'
10905
+ };
10906
+
10907
+ console.log(`✅ Achievement response:`, {
10908
+ name: responseData.name,
10909
+ isUnlocked: responseData.isUnlocked,
10910
+ currentValue: responseData.currentValue,
10911
+ maxProgress: responseData.maxProgress
10912
+ });
10913
+
10914
+ res.json({
10915
+ success: true,
10916
+ data: responseData
10917
+ });
10918
+
10919
+ } catch (error) {
10920
+ console.error('❌ Achievement details error:', error);
10921
+ res.status(500).json({
10922
+ success: false,
10923
+ error: error.message
10924
+ });
10925
+ }
10926
+ });
10927
 
10928
  // ============================================
10929
  // GET /api/users/:userId - جلب بيانات مستخدم معين للوسام
 
19011
  uploadStream.end(file.buffer);
19012
  });
19013
 
19014
+ let attachmentType = 'file';
19015
  if (file.mimetype.startsWith('image/')) attachmentType = 'image';
19016
+ else if (file.mimetype.startsWith('video/')) attachmentType = 'video';
19017
  else if (file.mimetype.startsWith('audio/')) attachmentType = 'audio';
19018
+ else if (file.mimetype === 'application/pdf') attachmentType = 'pdf';
19019
+ else attachmentType = 'file';
19020
 
19021
  attachments.push({
19022
  type: attachmentType,
 
19079
 
19080
  const populatedMessage = await message.populate('senderId', 'username profile.nickname profile.avatar');
19081
 
19082
+
19083
+ try {
19084
+ const socketClient = getSocketClient(); // يجب استيرادها من ملف منفصل
19085
+ // بث الرسالة لكل من في غرفة المحادثة
19086
+ socketClient.to(`conv_${conversationId}`).emit('new_message', populatedMessage);
19087
+ // إرسال إشعار للمستلم
19088
+ socketClient.to(`user_${otherParticipant}`).emit('new_message_notification', {
19089
+ conversationId,
19090
+ sender: {
19091
+ id: req.user.userId,
19092
+ username: sender.profile?.nickname || sender.username,
19093
+ avatar: sender.profile?.avatar
19094
+ },
19095
+ message: populatedMessage
19096
+ });
19097
+ logger.info(`📢 Broadcasted message ${populatedMessage._id} via Socket.IO`);
19098
+ } catch (socketError) {
19099
+ // لا نوقف الـ response إذا فشل البث، فقط نسجل الخطأ
19100
+ logger.error(`Socket broadcast failed: ${socketError.message}`);
19101
+ }
19102
+
19103
  res.status(201).json({ success: true, data: populatedMessage });
19104
 
19105
  } catch (error) {
 
36570
  { loc: '/feed.html', changefreq: 'daily', priority: 0.9 },
36571
  { loc: '/videos.html', changefreq: 'daily', priority: 0.9 },
36572
  { loc: '/video.html', changefreq: 'daily', priority: 0.9 },
36573
+ { loc: '/achievement.html', changefreq: 'daily', priority: 0.9 },
36574
  { loc: '/template-store.html', changefreq: 'daily', priority: 0.9 },
36575
  { loc: '/explore.html', changefreq: 'daily', priority: 0.9 },
36576
  { loc: '/profile.html', changefreq: 'daily', priority: 0.9 },
utils/socket-client.js ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const io = require('socket.io-client');
2
+
3
+ const SOCKET_SERVER_URL = process.env.SOCKET_SERVER_URL || 'https://al-asfar-mgzon-socket-server.hf.space';
4
+
5
+ let socketClient = null;
6
+
7
+ function getSocketClient() {
8
+ if (!socketClient) {
9
+ socketClient = io(SOCKET_SERVER_URL, {
10
+ transports: ['websocket', 'polling'],
11
+ reconnection: true,
12
+ reconnectionAttempts: 10,
13
+ reconnectionDelay: 1000,
14
+ reconnectionDelayMax: 5000,
15
+ });
16
+
17
+ socketClient.on('connect', () => {
18
+ console.log('✅ REST API connected to Socket.IO server');
19
+ });
20
+
21
+ socketClient.on('connect_error', (err) => {
22
+ console.error('❌ REST API Socket.IO connection error:', err.message);
23
+ });
24
+
25
+ socketClient.on('disconnect', () => {
26
+ console.warn('⚠️ REST API disconnected from Socket.IO server');
27
+ });
28
+ }
29
+ return socketClient;
30
+ }
31
+
32
+ module.exports = getSocketClient;