Initial commit
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user