Mark-Lasfar commited on
Commit ·
c0a8271
1
Parent(s): 41e454e
Add GraphQL
Browse files- graphql/resolvers.js +7 -3
- server.js +510 -6
graphql/resolvers.js
CHANGED
|
@@ -36,7 +36,8 @@ const {
|
|
| 36 |
PlatformPaymentMethod,
|
| 37 |
SubscriptionPlan,
|
| 38 |
StoreSettings,
|
| 39 |
-
StoreTheme
|
|
|
|
| 40 |
} = require('../server');
|
| 41 |
|
| 42 |
// دوال مساعدة
|
|
@@ -8096,7 +8097,8 @@ const Mutation = {
|
|
| 8096 |
if (!userId) return [];
|
| 8097 |
|
| 8098 |
try {
|
| 8099 |
-
|
|
|
|
| 8100 |
.sort({ viewedAt: -1 })
|
| 8101 |
.limit(10)
|
| 8102 |
.lean();
|
|
@@ -8160,12 +8162,14 @@ const Mutation = {
|
|
| 8160 |
}
|
| 8161 |
|
| 8162 |
try {
|
|
|
|
| 8163 |
const analytics = await ProfileAnalytics.findOne({ userId });
|
| 8164 |
|
| 8165 |
const weekAgo = new Date();
|
| 8166 |
weekAgo.setDate(weekAgo.getDate() - 7);
|
| 8167 |
|
| 8168 |
-
|
|
|
|
| 8169 |
targetUserId: userId,
|
| 8170 |
viewedAt: { $gte: weekAgo }
|
| 8171 |
});
|
|
|
|
| 36 |
PlatformPaymentMethod,
|
| 37 |
SubscriptionPlan,
|
| 38 |
StoreSettings,
|
| 39 |
+
StoreTheme,
|
| 40 |
+
AlsoViewed
|
| 41 |
} = require('../server');
|
| 42 |
|
| 43 |
// دوال مساعدة
|
|
|
|
| 8097 |
if (!userId) return [];
|
| 8098 |
|
| 8099 |
try {
|
| 8100 |
+
// ✅ استخدم AlsoViewed بدلاً من ProfileView
|
| 8101 |
+
const views = await AlsoViewed.find({ targetUserId: userId })
|
| 8102 |
.sort({ viewedAt: -1 })
|
| 8103 |
.limit(10)
|
| 8104 |
.lean();
|
|
|
|
| 8162 |
}
|
| 8163 |
|
| 8164 |
try {
|
| 8165 |
+
// ✅ استخدم AlsoViewed بدلاً من ProfileView
|
| 8166 |
const analytics = await ProfileAnalytics.findOne({ userId });
|
| 8167 |
|
| 8168 |
const weekAgo = new Date();
|
| 8169 |
weekAgo.setDate(weekAgo.getDate() - 7);
|
| 8170 |
|
| 8171 |
+
// ✅ استخدم AlsoViewed بدلاً من ProfileView
|
| 8172 |
+
const weeklyViews = await AlsoViewed.countDocuments({
|
| 8173 |
targetUserId: userId,
|
| 8174 |
viewedAt: { $gte: weekAgo }
|
| 8175 |
});
|
server.js
CHANGED
|
@@ -376,6 +376,7 @@ const excludedPaths = [
|
|
| 376 |
'/api/subscription/check',
|
| 377 |
'/api/subscription/renew',
|
| 378 |
'/api/subscription/cancel',
|
|
|
|
| 379 |
'/api/users/:userId',
|
| 380 |
'/api/admin/stores/:userId/toggle',
|
| 381 |
'/api/admin/stores/:userId',
|
|
@@ -4318,6 +4319,38 @@ const userSchema = new mongoose.Schema({
|
|
| 4318 |
|
| 4319 |
storeEnabled: { type: Boolean, default: false },
|
| 4320 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4321 |
// ✅ إعدادات إظهار/إخفاء الأقسام
|
| 4322 |
sectionVisibility: {
|
| 4323 |
about: true,
|
|
@@ -8118,7 +8151,6 @@ app.get('/api/trending/posts', authenticateToken, async (req, res) => {
|
|
| 8118 |
});
|
| 8119 |
|
| 8120 |
|
| 8121 |
-
|
| 8122 |
app.put('/api/users/:userId', authenticateToken, isAdmin, [
|
| 8123 |
param('userId').isMongoId().withMessage('Invalid user ID'),
|
| 8124 |
body('role').isIn(['User', 'Admin']).withMessage('Role must be either User or Admin')
|
|
@@ -9372,7 +9404,7 @@ app.get('/api/profile/me', authenticateToken, async (req, res) => {
|
|
| 9372 |
|
| 9373 |
} catch (error) {
|
| 9374 |
logger.error(`Error fetching profile/me: ${error.message}`);
|
| 9375 |
-
res.status(500).json({ error: '
|
| 9376 |
}
|
| 9377 |
});
|
| 9378 |
|
|
@@ -10152,6 +10184,100 @@ app.get('/api/profile/:nickname', async (req, res) => {
|
|
| 10152 |
}
|
| 10153 |
});
|
| 10154 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10155 |
|
| 10156 |
// ============================================
|
| 10157 |
// GET /api/users/:userId - جلب بيانات مستخدم معين للوسام
|
|
@@ -19210,7 +19336,6 @@ app.post('/api/analytics/track-view', async (req, res) => {
|
|
| 19210 |
);
|
| 19211 |
} else {
|
| 19212 |
// ✅ زائر (غير مسجل) - نستخدم IP
|
| 19213 |
-
// نتحقق إذا كان هذا الـ IP شاهد خلال آخر 24 ساعة
|
| 19214 |
const existingView = await AlsoViewed.findOne({
|
| 19215 |
targetUserId,
|
| 19216 |
viewerIp: viewerIp,
|
|
@@ -19227,7 +19352,24 @@ app.post('/api/analytics/track-view', async (req, res) => {
|
|
| 19227 |
}
|
| 19228 |
}
|
| 19229 |
|
| 19230 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19231 |
|
| 19232 |
} catch (error) {
|
| 19233 |
logger.error(`Error tracking profile view: ${error.message}`);
|
|
@@ -19235,8 +19377,6 @@ app.post('/api/analytics/track-view', async (req, res) => {
|
|
| 19235 |
res.json({ success: true });
|
| 19236 |
}
|
| 19237 |
});
|
| 19238 |
-
|
| 19239 |
-
|
| 19240 |
// ============================================
|
| 19241 |
// POST /api/ratings - تقييم مستخدم
|
| 19242 |
// ============================================
|
|
@@ -19662,6 +19802,370 @@ app.post('/api/websocket/token', authenticateToken, async (req, res) => {
|
|
| 19662 |
|
| 19663 |
|
| 19664 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19665 |
|
| 19666 |
// ============================================
|
| 19667 |
// ============================================
|
|
|
|
| 376 |
'/api/subscription/check',
|
| 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',
|
|
|
|
| 4319 |
|
| 4320 |
storeEnabled: { type: Boolean, default: false },
|
| 4321 |
|
| 4322 |
+
|
| 4323 |
+
badges: {
|
| 4324 |
+
type: {
|
| 4325 |
+
// 🥇 برونزيات المشاهدات (Profile Views)
|
| 4326 |
+
views: {
|
| 4327 |
+
silver: { type: Boolean, default: false }, // 20 مشاهدة
|
| 4328 |
+
gold: { type: Boolean, default: false }, // 100 مشاهدة
|
| 4329 |
+
red: { type: Boolean, default: false }, // 200 مشاهدة
|
| 4330 |
+
},
|
| 4331 |
+
// ❤️ برونزيات الإعجابات (Post Likes)
|
| 4332 |
+
likes: {
|
| 4333 |
+
bronze: { type: Boolean, default: false }, // 10 إعجابات
|
| 4334 |
+
silver: { type: Boolean, default: false }, // 30 إعجاب
|
| 4335 |
+
gold: { type: Boolean, default: false }, // 60 إعجاب
|
| 4336 |
+
red: { type: Boolean, default: false }, // 150 إعجاب
|
| 4337 |
+
},
|
| 4338 |
+
// 💬 برونزيات التفاعل (Comments/Replies)
|
| 4339 |
+
interaction: {
|
| 4340 |
+
bronze: { type: Boolean, default: false }, // 10 ردود
|
| 4341 |
+
},
|
| 4342 |
+
unlockedAt: { type: Date, default: null },
|
| 4343 |
+
updatedAt: { type: Date, default: Date.now }
|
| 4344 |
+
},
|
| 4345 |
+
default: {
|
| 4346 |
+
views: { silver: false, gold: false, red: false },
|
| 4347 |
+
likes: { bronze: false, silver: false, gold: false, red: false },
|
| 4348 |
+
interaction: { bronze: false },
|
| 4349 |
+
unlockedAt: null,
|
| 4350 |
+
updatedAt: Date.now
|
| 4351 |
+
}
|
| 4352 |
+
},
|
| 4353 |
+
|
| 4354 |
// ✅ إعدادات إظهار/إخفاء الأقسام
|
| 4355 |
sectionVisibility: {
|
| 4356 |
about: true,
|
|
|
|
| 8151 |
});
|
| 8152 |
|
| 8153 |
|
|
|
|
| 8154 |
app.put('/api/users/:userId', authenticateToken, isAdmin, [
|
| 8155 |
param('userId').isMongoId().withMessage('Invalid user ID'),
|
| 8156 |
body('role').isIn(['User', 'Admin']).withMessage('Role must be either User or Admin')
|
|
|
|
| 9404 |
|
| 9405 |
} catch (error) {
|
| 9406 |
logger.error(`Error fetching profile/me: ${error.message}`);
|
| 9407 |
+
res.status(500).json({ error: 'Profile retrieval error' });
|
| 9408 |
}
|
| 9409 |
});
|
| 9410 |
|
|
|
|
| 10184 |
}
|
| 10185 |
});
|
| 10186 |
|
| 10187 |
+
// ============================================
|
| 10188 |
+
// GET /api/users/:userId/badges - جلب البرونزيات
|
| 10189 |
+
// ============================================
|
| 10190 |
+
app.get('/api/users/:userId/badges', async (req, res) => {
|
| 10191 |
+
try {
|
| 10192 |
+
const { userId } = req.params;
|
| 10193 |
+
|
| 10194 |
+
// ✅ جلب جميع البيانات
|
| 10195 |
+
const badges = await getUserBadges(userId);
|
| 10196 |
+
const viewCount = await getUniqueViewsCount(userId);
|
| 10197 |
+
const likeCount = await getTotalPostLikes(userId);
|
| 10198 |
+
const replyCount = await getTotalUserReplies(userId);
|
| 10199 |
+
|
| 10200 |
+
// ✅ حساب التالي
|
| 10201 |
+
let nextBadge = null;
|
| 10202 |
+
let nextThreshold = null;
|
| 10203 |
+
let progress = 100;
|
| 10204 |
+
|
| 10205 |
+
// ✅ برونزيات المشاهدات
|
| 10206 |
+
if (!badges.views.silver && viewCount < VIEW_BADGE_THRESHOLDS.SILVER) {
|
| 10207 |
+
nextBadge = 'Views Silver';
|
| 10208 |
+
nextThreshold = VIEW_BADGE_THRESHOLDS.SILVER;
|
| 10209 |
+
progress = (viewCount / VIEW_BADGE_THRESHOLDS.SILVER) * 100;
|
| 10210 |
+
} else if (!badges.views.gold && viewCount < VIEW_BADGE_THRESHOLDS.GOLD) {
|
| 10211 |
+
nextBadge = 'Views Gold';
|
| 10212 |
+
nextThreshold = VIEW_BADGE_THRESHOLDS.GOLD;
|
| 10213 |
+
progress = (viewCount / VIEW_BADGE_THRESHOLDS.GOLD) * 100;
|
| 10214 |
+
} else if (!badges.views.red && viewCount < VIEW_BADGE_THRESHOLDS.RED) {
|
| 10215 |
+
nextBadge = 'Views Red';
|
| 10216 |
+
nextThreshold = VIEW_BADGE_THRESHOLDS.RED;
|
| 10217 |
+
progress = (viewCount / VIEW_BADGE_THRESHOLDS.RED) * 100;
|
| 10218 |
+
}
|
| 10219 |
+
|
| 10220 |
+
// ✅ برونزيات الإعجابات (إذا لم يكن هناك برونزية مشاهدات تالية)
|
| 10221 |
+
if (!nextBadge) {
|
| 10222 |
+
if (!badges.likes.bronze && likeCount < LIKE_BADGE_THRESHOLDS.BRONZE) {
|
| 10223 |
+
nextBadge = 'Likes Bronze';
|
| 10224 |
+
nextThreshold = LIKE_BADGE_THRESHOLDS.BRONZE;
|
| 10225 |
+
progress = (likeCount / LIKE_BADGE_THRESHOLDS.BRONZE) * 100;
|
| 10226 |
+
} else if (!badges.likes.red && likeCount < LIKE_BADGE_THRESHOLDS.RED) {
|
| 10227 |
+
nextBadge = 'Likes Red';
|
| 10228 |
+
nextThreshold = LIKE_BADGE_THRESHOLDS.RED;
|
| 10229 |
+
progress = (likeCount / LIKE_BADGE_THRESHOLDS.RED) * 100;
|
| 10230 |
+
} else if (!badges.likes.silver && likeCount < LIKE_BADGE_THRESHOLDS.SILVER) {
|
| 10231 |
+
nextBadge = 'Likes Silver';
|
| 10232 |
+
nextThreshold = LIKE_BADGE_THRESHOLDS.SILVER;
|
| 10233 |
+
progress = (likeCount / LIKE_BADGE_THRESHOLDS.SILVER) * 100;
|
| 10234 |
+
} else if (!badges.likes.gold && likeCount < LIKE_BADGE_THRESHOLDS.GOLD) {
|
| 10235 |
+
nextBadge = 'Likes Gold';
|
| 10236 |
+
nextThreshold = LIKE_BADGE_THRESHOLDS.GOLD;
|
| 10237 |
+
progress = (likeCount / LIKE_BADGE_THRESHOLDS.GOLD) * 100;
|
| 10238 |
+
}
|
| 10239 |
+
}
|
| 10240 |
+
|
| 10241 |
+
// ✅ برونزيات التفاعل (إذا لم يكن هناك برونزية تالية)
|
| 10242 |
+
if (!nextBadge) {
|
| 10243 |
+
if (!badges.interaction.bronze && replyCount < INTERACTION_BADGE_THRESHOLDS.BRONZE) {
|
| 10244 |
+
nextBadge = 'Interaction Bronze';
|
| 10245 |
+
nextThreshold = INTERACTION_BADGE_THRESHOLDS.BRONZE;
|
| 10246 |
+
progress = (replyCount / INTERACTION_BADGE_THRESHOLDS.BRONZE) * 100;
|
| 10247 |
+
}
|
| 10248 |
+
}
|
| 10249 |
+
|
| 10250 |
+
// ✅ إذا اكتمل كل شيء
|
| 10251 |
+
if (!nextBadge) {
|
| 10252 |
+
nextBadge = 'All badges collected! 🎉';
|
| 10253 |
+
nextThreshold = null;
|
| 10254 |
+
progress = 100;
|
| 10255 |
+
}
|
| 10256 |
+
|
| 10257 |
+
res.json({
|
| 10258 |
+
userId,
|
| 10259 |
+
viewCount,
|
| 10260 |
+
likeCount,
|
| 10261 |
+
replyCount,
|
| 10262 |
+
badges,
|
| 10263 |
+
thresholds: {
|
| 10264 |
+
views: VIEW_BADGE_THRESHOLDS,
|
| 10265 |
+
likes: LIKE_BADGE_THRESHOLDS,
|
| 10266 |
+
interaction: INTERACTION_BADGE_THRESHOLDS
|
| 10267 |
+
},
|
| 10268 |
+
nextBadge,
|
| 10269 |
+
nextThreshold,
|
| 10270 |
+
progress: Math.min(progress, 100),
|
| 10271 |
+
totalBadges: badges.totalCount || 0
|
| 10272 |
+
});
|
| 10273 |
+
|
| 10274 |
+
} catch (error) {
|
| 10275 |
+
console.error('Error getting badges:', error);
|
| 10276 |
+
res.status(500).json({ error: 'Failed to get badges' });
|
| 10277 |
+
}
|
| 10278 |
+
});
|
| 10279 |
+
|
| 10280 |
+
|
| 10281 |
|
| 10282 |
// ============================================
|
| 10283 |
// GET /api/users/:userId - جلب بيانات مستخدم معين للوسام
|
|
|
|
| 19336 |
);
|
| 19337 |
} else {
|
| 19338 |
// ✅ زائر (غير مسجل) - نستخدم IP
|
|
|
|
| 19339 |
const existingView = await AlsoViewed.findOne({
|
| 19340 |
targetUserId,
|
| 19341 |
viewerIp: viewerIp,
|
|
|
|
| 19352 |
}
|
| 19353 |
}
|
| 19354 |
|
| 19355 |
+
// ============================
|
| 19356 |
+
// 🏆 3. تحديث البرونزيات للمستخدم المستهدف
|
| 19357 |
+
// ============================
|
| 19358 |
+
const badgeResult = await updateUserBadges(targetUserId);
|
| 19359 |
+
|
| 19360 |
+
// ✅ الرد مع بيانات البرونزيات
|
| 19361 |
+
res.json({
|
| 19362 |
+
success: true,
|
| 19363 |
+
message: 'View tracked successfully',
|
| 19364 |
+
badges: badgeResult,
|
| 19365 |
+
// ✅ أضف الإحصائيات للـ Frontend
|
| 19366 |
+
stats: {
|
| 19367 |
+
viewCount: badgeResult?.viewCount || 0,
|
| 19368 |
+
likeCount: badgeResult?.likeCount || 0,
|
| 19369 |
+
replyCount: badgeResult?.replyCount || 0,
|
| 19370 |
+
totalBadges: badgeResult?.totalBadgeCount || 0
|
| 19371 |
+
}
|
| 19372 |
+
});
|
| 19373 |
|
| 19374 |
} catch (error) {
|
| 19375 |
logger.error(`Error tracking profile view: ${error.message}`);
|
|
|
|
| 19377 |
res.json({ success: true });
|
| 19378 |
}
|
| 19379 |
});
|
|
|
|
|
|
|
| 19380 |
// ============================================
|
| 19381 |
// POST /api/ratings - تقييم مستخدم
|
| 19382 |
// ============================================
|
|
|
|
| 19802 |
|
| 19803 |
|
| 19804 |
|
| 19805 |
+
// ============================================
|
| 19806 |
+
// ============================================
|
| 19807 |
+
// 🥇 برونزيات
|
| 19808 |
+
// ============================================
|
| 19809 |
+
// ============================================
|
| 19810 |
+
// ============================================
|
| 19811 |
+
// 🥇 برونزيات المشاهدات (Profile Views)
|
| 19812 |
+
// ============================================
|
| 19813 |
+
const VIEW_BADGE_THRESHOLDS = {
|
| 19814 |
+
SILVER: 20,
|
| 19815 |
+
GOLD: 100,
|
| 19816 |
+
RED: 200
|
| 19817 |
+
};
|
| 19818 |
+
|
| 19819 |
+
const VIEW_BADGE_ICONS = {
|
| 19820 |
+
silver: '/assets/img/fire.svg',
|
| 19821 |
+
gold: '/assets/img/firegolden.svg',
|
| 19822 |
+
red: '/assets/img/firered.svg'
|
| 19823 |
+
};
|
| 19824 |
+
|
| 19825 |
+
// ============================================
|
| 19826 |
+
// ❤️ برونزيات الإعجابات (Post Likes)
|
| 19827 |
+
// ============================================
|
| 19828 |
+
const LIKE_BADGE_THRESHOLDS = {
|
| 19829 |
+
BRONZE: 10,
|
| 19830 |
+
RED: 128,
|
| 19831 |
+
SILVER: 512,
|
| 19832 |
+
GOLD: 4096
|
| 19833 |
+
};
|
| 19834 |
+
|
| 19835 |
+
const LIKE_BADGE_ICONS = {
|
| 19836 |
+
bronze: '/assets/img/StarStruck_SkinTone1.png',
|
| 19837 |
+
red: '/assets/img/StarStruck_Bronze.png',
|
| 19838 |
+
silver: '/assets/img/StarStruck_Silver.png',
|
| 19839 |
+
gold: '/assets/img/StarStruck_Gold.png'
|
| 19840 |
+
};
|
| 19841 |
+
|
| 19842 |
+
// ============================================
|
| 19843 |
+
// 💬 برونزيات التفاعل (Comments/Replies)
|
| 19844 |
+
// ============================================
|
| 19845 |
+
const INTERACTION_BADGE_THRESHOLDS = {
|
| 19846 |
+
BRONZE: 10 // 10 ردود على تعليقات الآخرين
|
| 19847 |
+
};
|
| 19848 |
+
|
| 19849 |
+
const INTERACTION_BADGE_ICONS = {
|
| 19850 |
+
bronze: '/assets/img/QuickDraw_SkinTone1.png'
|
| 19851 |
+
};
|
| 19852 |
+
|
| 19853 |
+
/**
|
| 19854 |
+
* حساب عدد المشاهدات الفريدة للمستخدم
|
| 19855 |
+
*/
|
| 19856 |
+
async function getUniqueViewsCount(userId) {
|
| 19857 |
+
const analytics = await ProfileAnalytics.findOne({ userId });
|
| 19858 |
+
if (!analytics || !analytics.profileViews) return 0;
|
| 19859 |
+
|
| 19860 |
+
// ✅ عدد المشاهدين الفريدين (منع تكرار المشاهدات)
|
| 19861 |
+
const uniqueViewers = new Set();
|
| 19862 |
+
analytics.profileViews.forEach(view => {
|
| 19863 |
+
if (view.viewerId) {
|
| 19864 |
+
uniqueViewers.add(view.viewerId.toString());
|
| 19865 |
+
} else if (view.viewerIp) {
|
| 19866 |
+
uniqueViewers.add(view.viewerIp);
|
| 19867 |
+
}
|
| 19868 |
+
});
|
| 19869 |
+
|
| 19870 |
+
return uniqueViewers.size;
|
| 19871 |
+
}
|
| 19872 |
+
|
| 19873 |
+
/**
|
| 19874 |
+
* حساب عدد الإعجابات التي حصلت عليها منشورات المستخدم
|
| 19875 |
+
*/
|
| 19876 |
+
async function getTotalPostLikes(userId) {
|
| 19877 |
+
const posts = await Post.find({ userId }).select('likes').lean();
|
| 19878 |
+
let totalLikes = 0;
|
| 19879 |
+
posts.forEach(post => {
|
| 19880 |
+
totalLikes += post.likes?.length || 0;
|
| 19881 |
+
});
|
| 19882 |
+
return totalLikes;
|
| 19883 |
+
}
|
| 19884 |
+
|
| 19885 |
+
/**
|
| 19886 |
+
* حساب عدد ردود المستخدم على تعليقات الآخرين
|
| 19887 |
+
* (تشمل ردود على تعليقات المنشورات + ردود على تعليقات المشاريع)
|
| 19888 |
+
*/
|
| 19889 |
+
async function getTotalUserReplies(userId) {
|
| 19890 |
+
let totalReplies = 0;
|
| 19891 |
+
|
| 19892 |
+
try {
|
| 19893 |
+
// ✅ 1. ردود المستخدم على تعليقات الآخرين في المنشورات (Post.comments.replies)
|
| 19894 |
+
const posts = await Post.find({}).select('comments').lean();
|
| 19895 |
+
posts.forEach(post => {
|
| 19896 |
+
post.comments?.forEach(comment => {
|
| 19897 |
+
comment.replies?.forEach(reply => {
|
| 19898 |
+
if (reply.userId && reply.userId.toString() === userId.toString()) {
|
| 19899 |
+
totalReplies++;
|
| 19900 |
+
}
|
| 19901 |
+
});
|
| 19902 |
+
});
|
| 19903 |
+
});
|
| 19904 |
+
|
| 19905 |
+
// ✅ 2. ردود المستخدم على تعليقات الآخرين في المشاريع (Comment.replies)
|
| 19906 |
+
const projectComments = await Comment.find({}).select('replies').lean();
|
| 19907 |
+
projectComments.forEach(comment => {
|
| 19908 |
+
comment.replies?.forEach(reply => {
|
| 19909 |
+
if (reply.userId && reply.userId.toString() === userId.toString()) {
|
| 19910 |
+
totalReplies++;
|
| 19911 |
+
}
|
| 19912 |
+
});
|
| 19913 |
+
});
|
| 19914 |
+
|
| 19915 |
+
} catch (error) {
|
| 19916 |
+
console.error('Error calculating total user replies:', error);
|
| 19917 |
+
}
|
| 19918 |
+
|
| 19919 |
+
return totalReplies;
|
| 19920 |
+
}
|
| 19921 |
+
|
| 19922 |
+
/**
|
| 19923 |
+
* حساب جميع البرونزيات المستحقة
|
| 19924 |
+
*/
|
| 19925 |
+
function calculateAllBadges(viewCount, likeCount, replyCount) {
|
| 19926 |
+
const result = {
|
| 19927 |
+
views: { silver: false, gold: false, red: false, count: 0 },
|
| 19928 |
+
likes: { bronze: false, silver: false, gold: false, red: false, count: 0 },
|
| 19929 |
+
interaction: { bronze: false, count: 0 }
|
| 19930 |
+
};
|
| 19931 |
+
|
| 19932 |
+
// ✅ برونزيات المشاهدات
|
| 19933 |
+
if (viewCount >= VIEW_BADGE_THRESHOLDS.RED) {
|
| 19934 |
+
result.views.red = true;
|
| 19935 |
+
result.views.gold = true;
|
| 19936 |
+
result.views.silver = true;
|
| 19937 |
+
result.views.count = 3;
|
| 19938 |
+
} else if (viewCount >= VIEW_BADGE_THRESHOLDS.GOLD) {
|
| 19939 |
+
result.views.gold = true;
|
| 19940 |
+
result.views.silver = true;
|
| 19941 |
+
result.views.count = 2;
|
| 19942 |
+
} else if (viewCount >= VIEW_BADGE_THRESHOLDS.SILVER) {
|
| 19943 |
+
result.views.silver = true;
|
| 19944 |
+
result.views.count = 1;
|
| 19945 |
+
}
|
| 19946 |
+
|
| 19947 |
+
// ✅ برونزيات الإعجابات
|
| 19948 |
+
if (likeCount >= LIKE_BADGE_THRESHOLDS.GOLD) {
|
| 19949 |
+
result.likes.gold = true;
|
| 19950 |
+
result.likes.silver = true;
|
| 19951 |
+
result.likes.red = true;
|
| 19952 |
+
result.likes.bronze = true;
|
| 19953 |
+
result.likes.count = 4;
|
| 19954 |
+
} else if (likeCount >= LIKE_BADGE_THRESHOLDS.SILVER) {
|
| 19955 |
+
result.likes.silver = true;
|
| 19956 |
+
result.likes.red = true;
|
| 19957 |
+
result.likes.bronze = true;
|
| 19958 |
+
result.likes.count = 3;
|
| 19959 |
+
} else if (likeCount >= LIKE_BADGE_THRESHOLDS.RED) {
|
| 19960 |
+
result.likes.red = true;
|
| 19961 |
+
result.likes.bronze = true;
|
| 19962 |
+
result.likes.count = 2;
|
| 19963 |
+
} else if (likeCount >= LIKE_BADGE_THRESHOLDS.BRONZE) {
|
| 19964 |
+
result.likes.bronze = true;
|
| 19965 |
+
result.likes.count = 1;
|
| 19966 |
+
}
|
| 19967 |
+
|
| 19968 |
+
// ✅ برونزيات التفاعل
|
| 19969 |
+
if (replyCount >= INTERACTION_BADGE_THRESHOLDS.BRONZE) {
|
| 19970 |
+
result.interaction.bronze = true;
|
| 19971 |
+
result.interaction.count = 1;
|
| 19972 |
+
}
|
| 19973 |
+
|
| 19974 |
+
return result;
|
| 19975 |
+
}
|
| 19976 |
+
|
| 19977 |
+
/**
|
| 19978 |
+
* تحديث البرونزيات للمستخدم
|
| 19979 |
+
*/
|
| 19980 |
+
async function updateUserBadges(userId) {
|
| 19981 |
+
try {
|
| 19982 |
+
// ✅ جلب جميع البيانات
|
| 19983 |
+
const viewCount = await getUniqueViewsCount(userId);
|
| 19984 |
+
const likeCount = await getTotalPostLikes(userId);
|
| 19985 |
+
const replyCount = await getTotalUserReplies(userId);
|
| 19986 |
+
|
| 19987 |
+
const badges = calculateAllBadges(viewCount, likeCount, replyCount);
|
| 19988 |
+
|
| 19989 |
+
const user = await User.findById(userId);
|
| 19990 |
+
if (!user) return null;
|
| 19991 |
+
|
| 19992 |
+
// ✅ تهيئة البرونزيات
|
| 19993 |
+
if (!user.profile.badges) {
|
| 19994 |
+
user.profile.badges = {
|
| 19995 |
+
views: { silver: false, gold: false, red: false },
|
| 19996 |
+
likes: { bronze: false, silver: false, gold: false, red: false },
|
| 19997 |
+
interaction: { bronze: false },
|
| 19998 |
+
unlockedAt: null,
|
| 19999 |
+
updatedAt: Date.now()
|
| 20000 |
+
};
|
| 20001 |
+
}
|
| 20002 |
+
|
| 20003 |
+
let newBadgeUnlocked = false;
|
| 20004 |
+
let unlockedBadges = [];
|
| 20005 |
+
|
| 20006 |
+
// ✅ تحقق من برونزيات المشاهدات
|
| 20007 |
+
const viewBadgeMap = [
|
| 20008 |
+
{ key: 'silver', value: badges.views.silver, existing: user.profile.badges.views.silver, name: 'Views Silver', icon: '🥈' },
|
| 20009 |
+
{ key: 'gold', value: badges.views.gold, existing: user.profile.badges.views.gold, name: 'Views Gold', icon: '🥇' },
|
| 20010 |
+
{ key: 'red', value: badges.views.red, existing: user.profile.badges.views.red, name: 'Views Red', icon: '🔥' }
|
| 20011 |
+
];
|
| 20012 |
+
|
| 20013 |
+
viewBadgeMap.forEach(b => {
|
| 20014 |
+
if (b.value && !b.existing) {
|
| 20015 |
+
user.profile.badges.views[b.key] = true;
|
| 20016 |
+
newBadgeUnlocked = true;
|
| 20017 |
+
unlockedBadges.push(`${b.icon} ${b.name}`);
|
| 20018 |
+
}
|
| 20019 |
+
});
|
| 20020 |
+
|
| 20021 |
+
// ✅ تحقق من برونزيات الإعجابات
|
| 20022 |
+
const likeBadgeMap = [
|
| 20023 |
+
{ key: 'bronze', value: badges.likes.bronze, existing: user.profile.badges.likes.bronze, name: 'Likes Bronze', icon: '🟤' },
|
| 20024 |
+
{ key: 'red', value: badges.likes.red, existing: user.profile.badges.likes.red, name: 'Likes Red', icon: '🔴' },
|
| 20025 |
+
{ key: 'silver', value: badges.likes.silver, existing: user.profile.badges.likes.silver, name: 'Likes Silver', icon: '🥈' },
|
| 20026 |
+
{ key: 'gold', value: badges.likes.gold, existing: user.profile.badges.likes.gold, name: 'Likes Gold', icon: '🥇' }
|
| 20027 |
+
];
|
| 20028 |
+
|
| 20029 |
+
likeBadgeMap.forEach(b => {
|
| 20030 |
+
if (b.value && !b.existing) {
|
| 20031 |
+
user.profile.badges.likes[b.key] = true;
|
| 20032 |
+
newBadgeUnlocked = true;
|
| 20033 |
+
unlockedBadges.push(`${b.icon} ${b.name}`);
|
| 20034 |
+
}
|
| 20035 |
+
});
|
| 20036 |
+
|
| 20037 |
+
// ✅ تحقق من برونزيات التفاعل
|
| 20038 |
+
if (badges.interaction.bronze && !user.profile.badges.interaction.bronze) {
|
| 20039 |
+
user.profile.badges.interaction.bronze = true;
|
| 20040 |
+
newBadgeUnlocked = true;
|
| 20041 |
+
unlockedBadges.push('💬 Interaction Bronze');
|
| 20042 |
+
}
|
| 20043 |
+
|
| 20044 |
+
user.profile.badges.updatedAt = Date.now();
|
| 20045 |
+
|
| 20046 |
+
if (newBadgeUnlocked) {
|
| 20047 |
+
user.profile.badges.unlockedAt = Date.now();
|
| 20048 |
+
const badgeList = unlockedBadges.join(', ');
|
| 20049 |
+
|
| 20050 |
+
// ✅ 1. إشعار في User.notifications (جوه الـ User)
|
| 20051 |
+
user.notifications = user.notifications || [];
|
| 20052 |
+
user.notifications.push({
|
| 20053 |
+
message: `🎉 Congratulations! You unlocked new badges: ${badgeList}!`,
|
| 20054 |
+
type: 'success',
|
| 20055 |
+
link: '/profile/me',
|
| 20056 |
+
read: false,
|
| 20057 |
+
createdAt: new Date(),
|
| 20058 |
+
metadata: {
|
| 20059 |
+
badges: unlockedBadges,
|
| 20060 |
+
viewCount,
|
| 20061 |
+
likeCount,
|
| 20062 |
+
replyCount
|
| 20063 |
+
}
|
| 20064 |
+
});
|
| 20065 |
+
|
| 20066 |
+
// ✅ 2. إشعار في Notification model (منفصل)
|
| 20067 |
+
try {
|
| 20068 |
+
await Notification.create({
|
| 20069 |
+
userId: userId,
|
| 20070 |
+
type: 'system',
|
| 20071 |
+
actorId: userId,
|
| 20072 |
+
actorName: 'System',
|
| 20073 |
+
content: `🎉 You unlocked ${unlockedBadges.length} new badge(s): ${badgeList}!`,
|
| 20074 |
+
read: false,
|
| 20075 |
+
createdAt: new Date(),
|
| 20076 |
+
metadata: {
|
| 20077 |
+
badges: unlockedBadges,
|
| 20078 |
+
viewCount,
|
| 20079 |
+
likeCount,
|
| 20080 |
+
replyCount
|
| 20081 |
+
}
|
| 20082 |
+
});
|
| 20083 |
+
} catch (notifError) {
|
| 20084 |
+
console.error('Error creating notification in Notification model:', notifError);
|
| 20085 |
+
}
|
| 20086 |
+
|
| 20087 |
+
console.log(`🏆 Badges unlocked for user ${userId}: ${badgeList}`);
|
| 20088 |
+
}
|
| 20089 |
+
|
| 20090 |
+
await user.save();
|
| 20091 |
+
|
| 20092 |
+
return {
|
| 20093 |
+
badges: user.profile.badges,
|
| 20094 |
+
viewCount,
|
| 20095 |
+
likeCount,
|
| 20096 |
+
replyCount,
|
| 20097 |
+
newBadgeUnlocked,
|
| 20098 |
+
unlockedBadges,
|
| 20099 |
+
totalBadgeCount: badges.views.count + badges.likes.count + badges.interaction.count
|
| 20100 |
+
};
|
| 20101 |
+
|
| 20102 |
+
} catch (error) {
|
| 20103 |
+
console.error('Error updating badges:', error);
|
| 20104 |
+
return null;
|
| 20105 |
+
}
|
| 20106 |
+
}
|
| 20107 |
+
|
| 20108 |
+
/**
|
| 20109 |
+
* الحصول على أيقونة البرونزية المناسبة (للمشاهدات)
|
| 20110 |
+
*/
|
| 20111 |
+
function getBadgeIcon(badges) {
|
| 20112 |
+
if (badges.red) return VIEW_BADGE_ICONS.red;
|
| 20113 |
+
if (badges.gold) return VIEW_BADGE_ICONS.gold;
|
| 20114 |
+
if (badges.silver) return VIEW_BADGE_ICONS.silver;
|
| 20115 |
+
return null;
|
| 20116 |
+
}
|
| 20117 |
+
|
| 20118 |
+
/**
|
| 20119 |
+
* جلب حالة البرونزيات للمستخدم
|
| 20120 |
+
*/
|
| 20121 |
+
async function getUserBadges(userId) {
|
| 20122 |
+
try {
|
| 20123 |
+
const user = await User.findById(userId).select('profile.badges').lean();
|
| 20124 |
+
if (!user || !user.profile?.badges) {
|
| 20125 |
+
return {
|
| 20126 |
+
views: { silver: false, gold: false, red: false, count: 0 },
|
| 20127 |
+
likes: { bronze: false, silver: false, gold: false, red: false, count: 0 },
|
| 20128 |
+
interaction: { bronze: false, count: 0 },
|
| 20129 |
+
totalCount: 0
|
| 20130 |
+
};
|
| 20131 |
+
}
|
| 20132 |
+
|
| 20133 |
+
const badges = user.profile.badges;
|
| 20134 |
+
|
| 20135 |
+
let viewCount = 0;
|
| 20136 |
+
if (badges.views?.silver) viewCount++;
|
| 20137 |
+
if (badges.views?.gold) viewCount++;
|
| 20138 |
+
if (badges.views?.red) viewCount++;
|
| 20139 |
+
|
| 20140 |
+
let likeCount = 0;
|
| 20141 |
+
if (badges.likes?.bronze) likeCount++;
|
| 20142 |
+
if (badges.likes?.red) likeCount++;
|
| 20143 |
+
if (badges.likes?.silver) likeCount++;
|
| 20144 |
+
if (badges.likes?.gold) likeCount++;
|
| 20145 |
+
|
| 20146 |
+
let interactionCount = 0;
|
| 20147 |
+
if (badges.interaction?.bronze) interactionCount++;
|
| 20148 |
+
|
| 20149 |
+
const totalCount = viewCount + likeCount + interactionCount;
|
| 20150 |
+
|
| 20151 |
+
return {
|
| 20152 |
+
views: { ...badges.views, count: viewCount },
|
| 20153 |
+
likes: { ...badges.likes, count: likeCount },
|
| 20154 |
+
interaction: { ...badges.interaction, count: interactionCount },
|
| 20155 |
+
totalCount,
|
| 20156 |
+
unlockedAt: badges.unlockedAt || null
|
| 20157 |
+
};
|
| 20158 |
+
} catch (error) {
|
| 20159 |
+
console.error('Error getting badges:', error);
|
| 20160 |
+
return {
|
| 20161 |
+
views: { silver: false, gold: false, red: false, count: 0 },
|
| 20162 |
+
likes: { bronze: false, silver: false, gold: false, red: false, count: 0 },
|
| 20163 |
+
interaction: { bronze: false, count: 0 },
|
| 20164 |
+
totalCount: 0
|
| 20165 |
+
};
|
| 20166 |
+
}
|
| 20167 |
+
}
|
| 20168 |
+
|
| 20169 |
|
| 20170 |
// ============================================
|
| 20171 |
// ============================================
|