294 lines
8.7 KiB
JavaScript
294 lines
8.7 KiB
JavaScript
const express = require('express');
|
|
const Games = require('../models/games');
|
|
const { DeveloperEarnings } = require('../models/plays');
|
|
const { pool } = require('../models/db');
|
|
const { authenticate } = require('../middleware/auth');
|
|
|
|
const router = express.Router();
|
|
|
|
// Get developer dashboard
|
|
router.get('/dashboard',
|
|
authenticate,
|
|
async (req, res, next) => {
|
|
try {
|
|
const games = await Games.findByCreator(req.user.id);
|
|
const earnings = await DeveloperEarnings.getByDeveloper(req.user.id);
|
|
|
|
const totalPlays = games.reduce((sum, g) => sum + g.play_count, 0);
|
|
const publishedGames = games.filter(g => g.status === 'published');
|
|
const pendingGames = games.filter(g => g.status === 'pending_review');
|
|
const draftGames = games.filter(g => g.status === 'draft');
|
|
|
|
res.json({
|
|
stats: {
|
|
totalGames: games.length,
|
|
publishedGames: publishedGames.length,
|
|
pendingReview: pendingGames.length,
|
|
drafts: draftGames.length,
|
|
totalPlays: totalPlays,
|
|
tokensEarned: parseInt(earnings.total_earned) || 0,
|
|
currentBalance: req.user.tokens_balance
|
|
}
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
);
|
|
|
|
// Get developer's games with detailed stats
|
|
router.get('/games',
|
|
authenticate,
|
|
async (req, res, next) => {
|
|
try {
|
|
const games = await Games.findByCreator(req.user.id);
|
|
|
|
res.json({
|
|
games: games.map(g => ({
|
|
id: g.id,
|
|
title: g.title,
|
|
description: g.description,
|
|
status: g.status,
|
|
playCount: g.play_count,
|
|
tokensEarned: g.tokens_earned,
|
|
averageRating: g.total_ratings > 0
|
|
? (g.rating_sum / g.total_ratings).toFixed(1)
|
|
: null,
|
|
totalRatings: g.total_ratings,
|
|
createdAt: g.created_at,
|
|
publishedAt: g.published_at,
|
|
rejectionReason: g.rejection_reason
|
|
}))
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
);
|
|
|
|
// Request payout (placeholder - would integrate with payment system)
|
|
router.post('/request-payout',
|
|
authenticate,
|
|
async (req, res, next) => {
|
|
try {
|
|
const minPayout = 100;
|
|
|
|
if (req.user.tokens_balance < minPayout) {
|
|
return res.status(400).json({
|
|
error: `Minimum payout is ${minPayout} tokens`,
|
|
currentBalance: req.user.tokens_balance
|
|
});
|
|
}
|
|
|
|
// TODO: Integrate with payment system
|
|
// For now, just acknowledge the request
|
|
|
|
res.json({
|
|
message: 'Payout request received',
|
|
amount: req.user.tokens_balance,
|
|
note: 'Payout processing coming soon!'
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
);
|
|
|
|
// Get game analytics (creator only)
|
|
router.get('/games/:gameId/analytics',
|
|
authenticate,
|
|
async (req, res, next) => {
|
|
try {
|
|
const { gameId } = req.params;
|
|
|
|
// Get the game and verify ownership
|
|
const game = await Games.findById(gameId);
|
|
|
|
if (!game) {
|
|
return res.status(404).json({ error: 'Game not found' });
|
|
}
|
|
|
|
if (game.creator_id !== req.user.id) {
|
|
return res.status(403).json({ error: 'You can only view analytics for your own games' });
|
|
}
|
|
|
|
// Run all analytics queries in parallel for performance
|
|
const [
|
|
dailyPlaysResult,
|
|
ratingDistributionResult,
|
|
sessionStatsResult,
|
|
uniquePlayersResult,
|
|
highScoreDistributionResult,
|
|
peakHoursResult,
|
|
highestScoreResult
|
|
] = await Promise.all([
|
|
// Daily play counts for last 30 days
|
|
pool.query(
|
|
`SELECT DATE(started_at) as date, COUNT(*) as count
|
|
FROM plays
|
|
WHERE game_id = $1
|
|
AND started_at >= CURRENT_DATE - INTERVAL '30 days'
|
|
GROUP BY DATE(started_at)
|
|
ORDER BY date ASC`,
|
|
[gameId]
|
|
),
|
|
|
|
// Rating distribution (1-5 stars breakdown)
|
|
pool.query(
|
|
`SELECT rating, COUNT(*) as count
|
|
FROM ratings
|
|
WHERE game_id = $1
|
|
GROUP BY rating
|
|
ORDER BY rating ASC`,
|
|
[gameId]
|
|
),
|
|
|
|
// Average session duration
|
|
pool.query(
|
|
`SELECT
|
|
AVG(duration_seconds) as avg_duration,
|
|
MIN(duration_seconds) as min_duration,
|
|
MAX(duration_seconds) as max_duration,
|
|
COUNT(*) as total_sessions
|
|
FROM plays
|
|
WHERE game_id = $1 AND duration_seconds IS NOT NULL`,
|
|
[gameId]
|
|
),
|
|
|
|
// Total unique players
|
|
pool.query(
|
|
`SELECT COUNT(DISTINCT player_id) as unique_players
|
|
FROM plays
|
|
WHERE game_id = $1`,
|
|
[gameId]
|
|
),
|
|
|
|
// High score distribution (buckets)
|
|
pool.query(
|
|
`SELECT
|
|
CASE
|
|
WHEN score < 100 THEN '0-99'
|
|
WHEN score < 500 THEN '100-499'
|
|
WHEN score < 1000 THEN '500-999'
|
|
WHEN score < 5000 THEN '1000-4999'
|
|
WHEN score < 10000 THEN '5000-9999'
|
|
ELSE '10000+'
|
|
END as score_range,
|
|
COUNT(*) as count
|
|
FROM high_scores
|
|
WHERE game_id = $1
|
|
GROUP BY
|
|
CASE
|
|
WHEN score < 100 THEN '0-99'
|
|
WHEN score < 500 THEN '100-499'
|
|
WHEN score < 1000 THEN '500-999'
|
|
WHEN score < 5000 THEN '1000-4999'
|
|
WHEN score < 10000 THEN '5000-9999'
|
|
ELSE '10000+'
|
|
END
|
|
ORDER BY MIN(score) ASC`,
|
|
[gameId]
|
|
),
|
|
|
|
// Peak play hours (hour of day distribution)
|
|
pool.query(
|
|
`SELECT EXTRACT(HOUR FROM started_at) as hour, COUNT(*) as count
|
|
FROM plays
|
|
WHERE game_id = $1
|
|
GROUP BY EXTRACT(HOUR FROM started_at)
|
|
ORDER BY hour ASC`,
|
|
[gameId]
|
|
),
|
|
|
|
// Highest score
|
|
pool.query(
|
|
`SELECT MAX(score) as highest_score FROM high_scores WHERE game_id = $1`,
|
|
[gameId]
|
|
)
|
|
]);
|
|
|
|
// Build daily plays with date labels (last 30 days filled in)
|
|
const today = new Date();
|
|
const thirtyDaysAgo = new Date(today);
|
|
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
|
|
|
const dailyPlaysMap = new Map(
|
|
dailyPlaysResult.rows.map(row => [
|
|
row.date.toISOString().split('T')[0],
|
|
parseInt(row.count)
|
|
])
|
|
);
|
|
|
|
const dailyPlays = [];
|
|
for (let d = new Date(thirtyDaysAgo); d <= today; d.setDate(d.getDate() + 1)) {
|
|
const dateStr = d.toISOString().split('T')[0];
|
|
const dayNum = d.getDate();
|
|
dailyPlays.push({
|
|
date: dateStr,
|
|
plays: dailyPlaysMap.get(dateStr) || 0,
|
|
label: dayNum.toString()
|
|
});
|
|
}
|
|
|
|
// Build rating distribution as object {1: count, 2: count, ...}
|
|
const ratingDistribution = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
|
|
ratingDistributionResult.rows.forEach(row => {
|
|
ratingDistribution[row.rating] = parseInt(row.count);
|
|
});
|
|
|
|
// Build peak hours as object {0: count, 1: count, ...23: count}
|
|
const peakHours = {};
|
|
for (let hour = 0; hour < 24; hour++) {
|
|
peakHours[hour] = 0;
|
|
}
|
|
peakHoursResult.rows.forEach(row => {
|
|
peakHours[parseInt(row.hour)] = parseInt(row.count);
|
|
});
|
|
|
|
// Build score distribution as object with range labels
|
|
const scoreRanges = ['0-99', '100-499', '500-999', '1000-4999', '5000-9999', '10000+'];
|
|
const scoreDistribution = {};
|
|
scoreRanges.forEach(range => { scoreDistribution[range] = 0; });
|
|
highScoreDistributionResult.rows.forEach(row => {
|
|
scoreDistribution[row.score_range] = parseInt(row.count);
|
|
});
|
|
|
|
const sessionStats = sessionStatsResult.rows[0];
|
|
const avgSessionDuration = sessionStats.avg_duration
|
|
? Math.round(parseFloat(sessionStats.avg_duration))
|
|
: 0;
|
|
|
|
const uniquePlayers = parseInt(uniquePlayersResult.rows[0].unique_players);
|
|
const highestScore = highestScoreResult.rows[0].highest_score
|
|
? parseInt(highestScoreResult.rows[0].highest_score)
|
|
: null;
|
|
|
|
const avgRating = game.total_ratings > 0
|
|
? parseFloat((game.rating_sum / game.total_ratings).toFixed(2))
|
|
: null;
|
|
|
|
res.json({
|
|
dailyPlays,
|
|
ratingDistribution,
|
|
peakHours,
|
|
scoreDistribution,
|
|
summary: {
|
|
totalPlays: game.play_count || 0,
|
|
uniquePlayers,
|
|
avgSessionDuration,
|
|
favoriteCount: game.favorite_count || 0,
|
|
avgRating,
|
|
totalRatings: game.total_ratings || 0,
|
|
highestScore,
|
|
remixCount: game.remix_count || 0,
|
|
tokensEarned: game.tokens_earned || 0
|
|
}
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
);
|
|
|
|
module.exports = router;
|